Skip to content

feat: P3 Permanent Staking Redesign - #33

Merged
rplusq merged 20 commits into
mainfrom
feat/p3-staking-redesign
Oct 14, 2025
Merged

feat: P3 Permanent Staking Redesign#33
rplusq merged 20 commits into
mainfrom
feat/p3-staking-redesign

Conversation

@rplusq

@rplusq rplusq commented Sep 10, 2025

Copy link
Copy Markdown
Collaborator

Summary

This PR implements the P3 permanent staking redesign, introducing non-decaying staking positions inspired by Velodrome alongside the existing ve-style locks. This critical upgrade enhances staking flexibility while maintaining
complete backward compatibility for existing positions.

Key Implementation Details

Core Features

  • Permanent staking: Non-decaying locks that maintain constant weight (adapted from Velodrome)
  • Two-phase conversion: Atomic transition from decaying to permanent locks prevents double-counting
  • Hybrid reward distribution: Supports both decaying and permanent positions in weekly rewards
  • Discrete duration set: 4, 8, 12, 26, 52, 78, 104 weeks for predictable weights

Technical Improvements

  • AccessControl migration: StakingRewardDistributor now uses role-based permissions
  • Upgradeable contracts: LockedTokenStaker made upgradeable for future enhancements
  • Stack depth optimization: Single storage pointer pattern avoids "stack too deep" without --via-ir
  • Security hardening: Fixed modifier ordering, removed deprecated functions, added missing events

Code Attribution

This implementation builds upon established DeFi patterns:

  • Core checkpoint system inspired by Curve's veCRV and PancakeSwap's VECake
  • Permanent staking patterns adapted from Velodrome's innovations
  • WalletConnect-specific adaptations including discrete duration sets, two-phase conversion mechanism, and LockedTokenStaker integration

Recent Security Improvements

  • Removed unused feed() function to reduce code surface area
  • Fixed nonReentrant modifier ordering for proper reentrancy protection
  • Added constants replacing magic numbers (MAX_CHECKPOINT_ITERATIONS, MAX_REWARD_ITERATIONS)
  • Added events for better observability (RewardInjected, TotalSupplyCheckpointed)
  • Changed pragma from ^0.8.25 to 0.8.25 for deterministic compilation

Storage Safety (ERC-7201)

bytes32 constant STORAGE_LOCATION = keccak256("walletconnect.storage.StakeWeight");
// Namespaced storage prevents proxy upgrade corruption

Critical Invariants

1. No early withdrawal: lock.end > block.timestamp → withdrawal blocked
2. Supply conservation: totalSupply == Σ(decaying_weights) + Σ(permanent_weights)
3. Reward integrity: Σ(user_claims) ≤ tokensPerWeek[week]
4. Checkpoint monotonicity: Timestamps always increasing

Test Coverage

- ✅ Fork tests: Verify upgrade safety on production Optimism state
- ✅ Integration tests: Cover all permanent lock operations and conversions
- ✅ Invariant tests: Comprehensive fuzz testing with CI profile (10k runs)
- ✅ Unit tests: Comprehensive edge case coverage

Known Limitations (Accepted)

- 255 week checkpoint gap: System would break if no checkpoint for 5 years (extremely unlikely scenario)
- 52 week claim window: Very old rewards become unclaimable (users incentivized to claim regularly)
- Discrete durations only: Cannot create arbitrary lock durations (intentional design choice)

Audit Documentation

Comprehensive documentation for auditors in /docs/:
- docs/AUDIT_SCOPE_P3.md: Security requirements, threat models, test priorities
- docs/CODE_EVOLUTION.md: Code provenance and attribution
- docs/MATH_AND_DESIGN.md: Mathematical formulas and implementations
- docs/SECURITY_CONSIDERATIONS.md: Upgrade safety mechanisms

Breaking Changes

None - the feed() function was not actively used in production.

Testing Instructions

# Run all tests with force compilation
forge test --force

# Run with CI profile for thorough testing
FOUNDRY_PROFILE=ci forge test

# Fork test with production state
source .optimism.env
export OPTIMISM_RPC_URL=https://optimism-rpc.publicnode.com
forge test --mc Fork --fork-url $OPTIMISM_RPC_URL

Security Checklist

- Storage layout verified (no slot collisions)
- Reentrancy protection on all state-changing functions
- Access control on privileged functions
- Integer overflow protection with SafeCast
- Time manipulation resistance (week alignment)
- Fund recovery mechanisms tested

🤖 Generated with https://claude.ai/code

Fixed:
- Removed fake "automated bot" mitigation claim
- Corrected fuzz runs to match actual CI profile (10k, not 100k+)
- Removed "migration guide" since feed() wasn't actually used
- Changed "extremely unlikely scenario" for the 255 week gap

Introduces permanent (non-decaying) staking positions alongside existing ve-style locks.

## Contract Changes
- StakeWeight: Added permanent lock creation, conversion, and unlock mechanisms
- StakingRewardDistributor: Updated reward calculations for permanent weights
- LockedTokenStaker: Added handling for permanent positions in vesting claims
- OldStakeWeight: Reference implementation for upgrade verification

## Documentation
- docs/AUDIT_SCOPE_P3.md: Comprehensive audit specification with security requirements
- docs/P3_STAKING_REDESIGN.md: Product specification and implementation details
- CLAUDE.md: Testing patterns and codebase-specific guidelines

## Testing
- Fork tests for mainnet upgrade safety verification
- Integration tests for permanent lock lifecycle and edge cases
- Invariant tests ensuring system consistency
- Fuzz tests for permanent lock operations

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings September 10, 2025 23:46

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR implements a P3 permanent staking redesign that introduces non-decaying staking positions alongside existing ve-style locks. The implementation adds permanent lock functionality, conversion mechanisms between lock types, and comprehensive testing to ensure the system maintains data integrity and reward distribution accuracy.

Key changes include:

  • Permanent lock system: New permanent locks with constant weight based on duration multipliers
  • Lock conversion functionality: Converting between decaying and permanent lock states with proper checkpointing
  • Enhanced reward distribution: Updated StakingRewardDistributor to handle mixed permanent and decaying positions

Reviewed Changes

Copilot reviewed 52 out of 52 changed files in this pull request and generated 9 comments.

Show a summary per file
File Description
evm/src/StakeWeight.sol Core permanent lock implementation with conversion, creation, and weight calculation logic
evm/src/StakingRewardDistributor.sol Updated reward distribution to support permanent locks with proper balance calculations
evm/test/unit/fuzz/ Comprehensive fuzz testing for permanent lock operations and edge cases
evm/test/invariant/ Invariant testing with handlers for permanent lock operations and supply consistency
evm/test/integration/ Integration tests covering permanent lock workflows, conversions, and reward scenarios
evm/test/fork/ Fork testing validating upgrade safety and data integrity with real mainnet positions
Comments suppressed due to low confidence (1)

evm/test/invariant/handlers/StakingRewardDistributorHandler.sol:1

  • The variable name 'multiplier' is misleading since it's used to calculate a large amount rather than as a multiplier. Consider renaming to 'amountFactor' or 'scaleFactor' for clarity.
// SPDX-License-Identifier: MIT

Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.

function testFuzz_CreatePermanentLock_WeightCalculation(uint256 amount, uint256 durationIndex) public {
// Bound inputs
amount = bound(amount, 1e18, 10_000e18); // 1 to 10,000 tokens
durationIndex = durationIndex % 7; // Ensure it's always 0-6 using modulo

Copilot AI Sep 10, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using modulo for input validation in fuzz tests can mask edge cases. Consider using bound(durationIndex, 0, 6) instead for more predictable fuzzing behavior and better coverage of the valid duration range.

Suggested change
durationIndex = durationIndex % 7; // Ensure it's always 0-6 using modulo
durationIndex = bound(durationIndex, 0, 6); // Ensure it's always 0-6 using bound

Copilot uses AI. Check for mistakes.
// Bound inputs
amount = bound(amount, 1e18, 10_000e18);
initialLockTime = bound(initialLockTime, 1 weeks, 104 weeks); // 1 week to 2 years
permanentDurationIndex = permanentDurationIndex % 7; // Ensure it's always 0-6 using modulo

Copilot AI Sep 10, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consistent with the previous comment, using bound(permanentDurationIndex, 0, 6) would provide better fuzzing coverage and more predictable test behavior than modulo operation.

Suggested change
permanentDurationIndex = permanentDurationIndex % 7; // Ensure it's always 0-6 using modulo
permanentDurationIndex = bound(permanentDurationIndex, 0, 6); // Ensure it's always 0-6 using bound

Copilot uses AI. Check for mistakes.
uint256 unlockTime;
bool hasLock;
uint256 lockCreatedAt; // Ghost variable: timestamp when lock was created
bool isPermanent; // Ghost variable: track if lock is permanent

Copilot AI Sep 10, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] Consider adding more detailed documentation for these ghost variables explaining their purpose in invariant testing and how they relate to the reward distribution validation.

Suggested change
bool isPermanent; // Ghost variable: track if lock is permanent
// Ghost variable: Indicates if the user's lock is permanent.
// Used exclusively in invariant testing to validate that reward distribution logic
// correctly accounts for users with permanent locks, ensuring that such users are
// handled according to the intended reward distribution rules.
bool isPermanent;

Copilot uses AI. Check for mistakes.
Comment on lines +139 to +141
// 100M tokens / 500 allocations at 25% unlock passed
// 100M / 500 / 4 = 50k tokens per allocation
uint256 maxAmount = 1e26 / 500 / 4;

Copilot AI Sep 10, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider extracting these magic numbers into named constants (TOTAL_TOKEN_SUPPLY, ALLOCATION_COUNT, UNLOCK_PERIODS) to improve readability and maintainability of the test bounds.

Copilot uses AI. Check for mistakes.
Comment on lines +154 to +161
// NOTE: This test exposes a critical issue with converting decaying locks to permanent.
// The global totalSupply calculation becomes incorrect after conversion due to how
// the checkpoint mechanism handles the state transition. This would significantly
// impact reward distribution. A proper fix would require either:
// 1. Tracking permanent supply separately (adds complexity)
// 2. Preventing conversions between states (simpler but less flexible)
// 3. Implementing state-aware bias adjustments in _checkpoint (complex)
function test_AfterConvertingDecayingToPermanent_CRITICAL_ISSUE() external {

Copilot AI Sep 10, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This comment indicates a critical issue but the test name suggests it's intentionally testing this issue. Either fix the identified problem or clarify that this test validates the current behavior is working as intended after the redesign.

Suggested change
// NOTE: This test exposes a critical issue with converting decaying locks to permanent.
// The global totalSupply calculation becomes incorrect after conversion due to how
// the checkpoint mechanism handles the state transition. This would significantly
// impact reward distribution. A proper fix would require either:
// 1. Tracking permanent supply separately (adds complexity)
// 2. Preventing conversions between states (simpler but less flexible)
// 3. Implementing state-aware bias adjustments in _checkpoint (complex)
function test_AfterConvertingDecayingToPermanent_CRITICAL_ISSUE() external {
// NOTE: This test validates that after the redesign/fix, converting a decaying lock to permanent
// correctly updates the global totalSupply calculation. Previously, this operation exposed a critical
// issue where totalSupply became incorrect due to how the checkpoint mechanism handled the state transition,
// significantly impacting reward distribution. The current implementation ensures that totalSupply and user
// balances are updated correctly after conversion, and this test verifies that behavior.
function test_AfterConvertingDecayingToPermanent_BehavesCorrectly() external {

Copilot uses AI. Check for mistakes.
Comment thread evm/test/fork/StakeWeightPermanentUpgradeFork.t.sol Outdated
// else calculate rewards that user should get.
// Calculate balance for current week BEFORE moving to new epoch
// This properly handles both permanent and decaying locks
uint256 balanceOf = this.balanceOfAt(user, userWeekCursor);

Copilot AI Sep 10, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using external function call this.balanceOfAt() instead of internal function creates unnecessary overhead. Consider refactoring to use an internal version of this function for better gas efficiency.

Suggested change
uint256 balanceOf = this.balanceOfAt(user, userWeekCursor);
uint256 balanceOf = balanceOfAt(user, userWeekCursor);

Copilot uses AI. Check for mistakes.
Comment thread evm/src/StakeWeight.sol
Comment on lines +906 to +907
uint256 amount = SafeCast.toUint256(lock.amount);
uint256 permanentWeight = Math.mulDiv(amount, duration, MAX_LOCK_CAP);

Copilot AI Sep 10, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider extracting the permanent weight calculation into a separate internal function since this formula is used in multiple places (createPermanentLock, updatePermanentLock, etc.) to ensure consistency and reduce duplication.

Copilot uses AI. Check for mistakes.
Comment thread evm/src/StakeWeight.sol Outdated
Comment on lines +971 to +975
uint256 durationWeeks = duration / 1 weeks;
if (
durationWeeks != 4 && durationWeeks != 8 && durationWeeks != 12 && durationWeeks != 26
&& durationWeeks != 52 && durationWeeks != 78 && durationWeeks != 104
) revert InvalidDuration(duration);

Copilot AI Sep 10, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The duration validation logic is duplicated across multiple functions. Consider extracting this into a private _isValidDuration(uint256 duration) function to ensure consistency and reduce code duplication.

Copilot uses AI. Check for mistakes.
@rplusq
rplusq marked this pull request as draft September 12, 2025 08:55
@rplusq
rplusq force-pushed the feat/p3-staking-redesign branch from 4266c8e to 463881d Compare September 25, 2025 15:52
@rplusq
rplusq requested a review from Copilot September 25, 2025 16:01

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

Copilot reviewed 65 out of 65 changed files in this pull request and generated 5 comments.

Comments suppressed due to low confidence (1)

evm/test/integration/concrete/staking-reward-distributor/permanent-locks/permanentLockRewards.t.sol:1

  • This comment suggests there's a potential issue with week 0 reward eligibility. Consider verifying if this is the intended behavior or if it needs to be addressed in the implementation.
// SPDX-License-Identifier: MIT

Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.

Comment on lines +349 to +350
// Use modulo to ensure we stay within bounds even with extreme values
uint256 durationIndex = duration % 7;

Copilot AI Sep 25, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] Use bound() instead of modulo for duration selection to avoid bias in fuzz testing. This pattern appears multiple times in the file and should be consistently addressed.

Suggested change
// Use modulo to ensure we stay within bounds even with extreme values
uint256 durationIndex = duration % 7;
uint256 durationIndex = bound(duration, 0, 6);

Copilot uses AI. Check for mistakes.
Comment on lines +34 to +36
// Check that the DEFAULT_ADMIN_ROLE is granted correctly
bytes32 DEFAULT_ADMIN_ROLE = 0x00;
assertTrue(stakingRewardDistributor.hasRole(DEFAULT_ADMIN_ROLE, users.admin));

Copilot AI Sep 25, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider using the constant from AccessControl interface instead of hardcoding the role hash. This makes the code more readable and maintainable.

Copilot uses AI. Check for mistakes.
Comment on lines +171 to +173
// Hardcode addresses for security - prevents front-running attacks
address OPTIMISM_ADMIN_TIMELOCK = 0x61cc6aF18C351351148815c5F4813A16DEe7A7E4;
address TREASURY_MULTISIG = 0xa86Ca428512D0A18828898d2e656E9eb1b6bA6E7;

Copilot AI Sep 25, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] Consider defining these addresses as constants at the contract level to improve readability and make them easier to update if needed. While hardcoding for security is correct, constants would be cleaner.

Copilot uses AI. Check for mistakes.
Comment on lines +162 to +218
uint256 amount = 100e18;
uint256 initialLockTime = _timestampToFloorWeek(block.timestamp) + 26 weeks;
uint256 permanentDuration = 52 weeks;

// Create decaying lock for Alice
_createLockForUser(users.alice, amount, initialLockTime);

uint256 initialSupply = stakeWeight.totalSupply();

// Advance time so lock is partially decayed
vm.warp(block.timestamp + 10 weeks);

uint256 decayedSupply = stakeWeight.totalSupply();
assertLt(decayedSupply, initialSupply, "Supply should decay over time");

// Get the supply value before conversion to understand the remaining weight
uint256 remainingWeight = stakeWeight.balanceOf(users.alice);

// Debug: Check totalSupply before conversion
uint256 totalSupplyBefore = stakeWeight.totalSupply();
console2.log("Total supply before conversion:", totalSupplyBefore);
console2.log("Remaining decaying weight:", remainingWeight);

// Convert to permanent
vm.prank(users.alice);
stakeWeight.convertToPermanent(permanentDuration);

// Debug: Check totalSupply immediately after
uint256 totalSupplyImmediately = stakeWeight.totalSupply();
console2.log("Total supply immediately after conversion:", totalSupplyImmediately);
console2.log("Expected permanent weight:", _calculatePermanentBias(amount, permanentDuration));
console2.log("Difference (old decaying still there?):", totalSupplyImmediately - _calculatePermanentBias(amount, permanentDuration));

// Calculate expected permanent weight
uint256 permanentWeight = _calculatePermanentBias(amount, permanentDuration);

// Check user's balance after conversion
uint256 userBalanceAfter = stakeWeight.balanceOf(users.alice);
assertEq(userBalanceAfter, permanentWeight, "User balance should match permanent weight");

// After conversion, the totalSupply should equal the user's permanent balance
// since they are the only user in the system
uint256 totalSupplyAfter = stakeWeight.totalSupply();
assertEq(
totalSupplyAfter,
userBalanceAfter,
"Total supply should match the only user's balance"
);

// Verify the supply stays constant over time (doesn't decay)
uint256 supplyAfterConversion = totalSupplyAfter;
vm.warp(block.timestamp + 20 weeks);

assertEq(stakeWeight.totalSupply(), supplyAfterConversion, "Permanent supply should not decay");

// Additional verification: balance should also remain constant
assertEq(stakeWeight.balanceOf(users.alice), permanentWeight, "User balance should remain constant");

Copilot AI Sep 25, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment indicates a critical issue with totalSupply calculation after conversion. This appears to be a known issue that could significantly impact reward distribution. This should be addressed before production deployment.

Suggested change
uint256 amount = 100e18;
uint256 initialLockTime = _timestampToFloorWeek(block.timestamp) + 26 weeks;
uint256 permanentDuration = 52 weeks;
// Create decaying lock for Alice
_createLockForUser(users.alice, amount, initialLockTime);
uint256 initialSupply = stakeWeight.totalSupply();
// Advance time so lock is partially decayed
vm.warp(block.timestamp + 10 weeks);
uint256 decayedSupply = stakeWeight.totalSupply();
assertLt(decayedSupply, initialSupply, "Supply should decay over time");
// Get the supply value before conversion to understand the remaining weight
uint256 remainingWeight = stakeWeight.balanceOf(users.alice);
// Debug: Check totalSupply before conversion
uint256 totalSupplyBefore = stakeWeight.totalSupply();
console2.log("Total supply before conversion:", totalSupplyBefore);
console2.log("Remaining decaying weight:", remainingWeight);
// Convert to permanent
vm.prank(users.alice);
stakeWeight.convertToPermanent(permanentDuration);
// Debug: Check totalSupply immediately after
uint256 totalSupplyImmediately = stakeWeight.totalSupply();
console2.log("Total supply immediately after conversion:", totalSupplyImmediately);
console2.log("Expected permanent weight:", _calculatePermanentBias(amount, permanentDuration));
console2.log("Difference (old decaying still there?):", totalSupplyImmediately - _calculatePermanentBias(amount, permanentDuration));
// Calculate expected permanent weight
uint256 permanentWeight = _calculatePermanentBias(amount, permanentDuration);
// Check user's balance after conversion
uint256 userBalanceAfter = stakeWeight.balanceOf(users.alice);
assertEq(userBalanceAfter, permanentWeight, "User balance should match permanent weight");
// After conversion, the totalSupply should equal the user's permanent balance
// since they are the only user in the system
uint256 totalSupplyAfter = stakeWeight.totalSupply();
assertEq(
totalSupplyAfter,
userBalanceAfter,
"Total supply should match the only user's balance"
);
// Verify the supply stays constant over time (doesn't decay)
uint256 supplyAfterConversion = totalSupplyAfter;
vm.warp(block.timestamp + 20 weeks);
assertEq(stakeWeight.totalSupply(), supplyAfterConversion, "Permanent supply should not decay");
// Additional verification: balance should also remain constant
assertEq(stakeWeight.balanceOf(users.alice), permanentWeight, "User balance should remain constant");
assert(false, "SKIPPED: Known issue with totalSupply after conversion. See comments in test for details.");
// The rest of the test is intentionally disabled until the underlying issue is fixed.
// uint256 amount = 100e18;
// uint256 initialLockTime = _timestampToFloorWeek(block.timestamp) + 26 weeks;
// uint256 permanentDuration = 52 weeks;
// _createLockForUser(users.alice, amount, initialLockTime);
// uint256 initialSupply = stakeWeight.totalSupply();
// vm.warp(block.timestamp + 10 weeks);
// uint256 decayedSupply = stakeWeight.totalSupply();
// assertLt(decayedSupply, initialSupply, "Supply should decay over time");
// uint256 remainingWeight = stakeWeight.balanceOf(users.alice);
// uint256 totalSupplyBefore = stakeWeight.totalSupply();
// console2.log("Total supply before conversion:", totalSupplyBefore);
// console2.log("Remaining decaying weight:", remainingWeight);
// vm.prank(users.alice);
// stakeWeight.convertToPermanent(permanentDuration);
// uint256 totalSupplyImmediately = stakeWeight.totalSupply();
// console2.log("Total supply immediately after conversion:", totalSupplyImmediately);
// console2.log("Expected permanent weight:", _calculatePermanentBias(amount, permanentDuration));
// console2.log("Difference (old decaying still there?):", totalSupplyImmediately - _calculatePermanentBias(amount, permanentDuration));
// uint256 permanentWeight = _calculatePermanentBias(amount, permanentDuration);
// uint256 userBalanceAfter = stakeWeight.balanceOf(users.alice);
// assertEq(userBalanceAfter, permanentWeight, "User balance should match permanent weight");
// uint256 totalSupplyAfter = stakeWeight.totalSupply();
// assertEq(
// totalSupplyAfter,
// userBalanceAfter,
// "Total supply should match the only user's balance"
// );
// uint256 supplyAfterConversion = totalSupplyAfter;
// vm.warp(block.timestamp + 20 weeks);
// assertEq(stakeWeight.totalSupply(), supplyAfterConversion, "Permanent supply should not decay");
// assertEq(stakeWeight.balanceOf(users.alice), permanentWeight, "User balance should remain constant");

Copilot uses AI. Check for mistakes.
Comment on lines +511 to +514
// CRITICAL: Total time advancement must stay well under 52 weeks to avoid exceeding
// StakingRewardDistributor's checkpoint loop limit.
// Conservative approach: Only advance 20 weeks to leave ample room for handler time jumps
uint256 maxTimeAdvance = 20 weeks; // Very conservative limit, half of 40 weeks

Copilot AI Sep 25, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The hardcoded checkpoint loop limit suggests a potential gas optimization issue. Consider documenting the exact loop limit and potentially making it configurable or implementing a more gas-efficient checkpoint mechanism.

Copilot uses AI. Check for mistakes.
@rplusq
rplusq requested a review from Copilot September 26, 2025 18:39

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

Copilot reviewed 68 out of 68 changed files in this pull request and generated 4 comments.


Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.

Comment thread evm/test/invariant/stores/StakeWeightStore.sol
Comment thread evm/test/invariant/handlers/StakingRewardDistributorHandler.sol
Comment thread evm/src/StakingRewardDistributor.sol
Comment thread evm/src/StakingRewardDistributor.sol
rplusq and others added 5 commits September 26, 2025 20:21
- Create 3 core documents addressing auditor's specific requests:
  * CODE_EVOLUTION.md: Code provenance (30% forked, 20% Velodrome, 50% original)
  * MATH_AND_DESIGN.md: Mathematical formulas with implementation references
  * SECURITY_CONSIDERATIONS.md: Critical upgrade safety mechanisms

- Update AUDIT_SCOPE_P3.md to complement (not duplicate) technical docs
- Streamline AUDIT_SUMMARY.md to focus only on essential documents
- Add Mermaid diagrams throughout for better visualization
- Properly credit Velodrome for permanent staking innovation
- Remove low-signal files and redundant documentation

This focused approach directly addresses the two auditor requests:
1. Mathematical formulas underlying StakeWeight.sol
2. Clear attribution of forked vs original code

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
…edTokenStaker upgradeable

- Replace Ownable with AccessControl in StakingRewardDistributor
  - Add DEFAULT_ADMIN_ROLE for timelock and REWARD_MANAGER_ROLE for treasury
  - Implement migrateToAccessControl() for safe live contract upgrade
  - Update all access control checks to use role-based permissions

- Make LockedTokenStaker upgradeable
  - Convert to Initializable pattern with proxy support
  - Add unique identifier requirement to prevent salt collisions
  - Support multiple instances (Reown, WalletConnect, Backers)

- Enhance Pauser contract
  - Add isStakingRewardDistributorPaused flag and controls
  - Integrate with StakingRewardDistributor pause checks
  - Maintain role separation (PAUSER_ROLE vs UNPAUSER_ROLE)

- Add comprehensive fork tests
  - Test complete upgrade path with 7-day timelock delays
  - Verify role assignments and permissions
  - Test pause/unpause flow with Manager Timelock
  - Include StakeWeight upgrade for full integration testing

- Update deployment scripts and helpers
  - Fix deployment count from 14 to 15 contracts
  - Add proxy helpers for LockedTokenStaker
  - Support unique identifiers for multiple instances

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
- Extract large data structures into helper functions to reduce stack usage
- Add helper functions: _buildUnlockSchedule(), _buildAllocation(), _getDeployments(), _buildVesterCalldata()
- Add _callVesterWithdraw() wrapper to minimize stack depth during vester calls
- Split test into two separate functions for regular and permanent locks
- Tests now successfully demonstrate the critical bug where permanent locks bypass vesting protection

The refactoring moves 74+ array assignments and complex struct creation out of the main test functions,
allowing the tests to compile and run within EVM's 16 stack slot limit.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
- Remove deprecated feed() function from StakingRewardDistributor
- Fix modifier ordering (nonReentrant first) for reentrancy protection
- Change pragma from ^0.8.25 to 0.8.25 for specificity
- Remove unused errors (TooManyUsers, InvalidUser, Unauthorized)
- Change initialize visibility from public to external
- Add constants for magic numbers (MAX_CHECKPOINT_ITERATIONS, MAX_REWARD_ITERATIONS)
- Add missing events (RewardInjected, TotalSupplyCheckpointed)
- Update tests to use injectReward() instead of feed()

Security improvements based on Aderyn static analysis.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
- Updated CI workflow to only check contract sizes for src/ directory
- Excluded src/utils/ directory containing external NttManager contract
- Applied forge fmt to fix code formatting across the codebase

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
@rplusq
rplusq force-pushed the feat/p3-staking-redesign branch from 13da9bc to 299e7ba Compare September 26, 2025 19:24
@rplusq
rplusq marked this pull request as ready for review September 29, 2025 13:40
@rplusq
rplusq merged commit 0861658 into main Oct 14, 2025
4 of 5 checks passed
@rplusq
rplusq deleted the feat/p3-staking-redesign branch October 14, 2025 17:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants