Skip to content
Draft
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
15 changes: 15 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -49,3 +49,18 @@ jobs:
env:
ETH_RPC_URL: ${{ secrets.ETH_RPC_URL || vars.ETH_RPC_URL || 'https://rpc.gnosis.gateway.fm' }}
id: test

fuzz:
name: Fuzz tests
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
submodules: recursive

- name: Install Foundry
uses: foundry-rs/foundry-toolchain@v1

- name: Run fuzz tests
run: |
forge test --match-path 'test/fuzz/**' -vvv
3 changes: 3 additions & 0 deletions foundry.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,7 @@ src = "src"
out = "out"
libs = ["lib"]

# If `forge test` gets slow, lower this and/or add a dedicated CI profile with fewer runs.
fuzz = { runs = 1024 }

# See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options
17 changes: 8 additions & 9 deletions src/abstract/AbstractDistributionStrategy.sol
Original file line number Diff line number Diff line change
Expand Up @@ -92,19 +92,18 @@ abstract contract AbstractDistributionStrategy is Initializable, IDistributionSt
/// @param _yieldToken Address of the yield token to distribute
/// @param _distributionManager Address of the distribution manager authorized to call distribute
/// @param _owner Address that will own this contract (receives onlyOwner privileges)
function __AbstractDistributionStrategy_init(
address _yieldToken,
address _distributionManager,
address _owner
) internal onlyInitializing {
function __AbstractDistributionStrategy_init(address _yieldToken, address _distributionManager, address _owner)
internal
onlyInitializing
{
__Ownable_init(_owner);
__AbstractDistributionStrategy_init_unchained(_yieldToken, _distributionManager);
}

function __AbstractDistributionStrategy_init_unchained(
address _yieldToken,
address _distributionManager
) internal onlyInitializing {
function __AbstractDistributionStrategy_init_unchained(address _yieldToken, address _distributionManager)
internal
onlyInitializing
{
if (_yieldToken == address(0)) revert ZeroAddress();
if (_distributionManager == address(0)) revert ZeroAddress();

Expand Down
2 changes: 1 addition & 1 deletion src/base/BasisPointsVotingModule.sol
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,7 @@ contract BasisPointsVotingModule is AbstractVotingModule {
/// @param voter Address of the voter
/// @param points Array of points allocated to each recipient
/// @param votingPower Total voting power of the voter
function _processVote(address voter, uint256[] calldata points, uint256 votingPower) internal override virtual {
function _processVote(address voter, uint256[] calldata points, uint256 votingPower) internal virtual override {
AbstractVotingModuleStorage storage base = _getAbstractVotingModuleStorage();
BasisPointsVotingModuleStorage storage $ = _getBasisPointsVotingModuleStorage();
uint256 currentCycle = base.cycleModule.getCurrentCycle();
Expand Down
18 changes: 3 additions & 15 deletions src/implementation/VotingStreakNFTModule.sol
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,7 @@ contract VotingStreakNFTModule is BasisPointsVotingModule {
bytes32 private constant VOTING_STREAK_NFT_MODULE_STORAGE =
0xa65dee7b045e43a11600ba41b419f3aad18025b3c1b3b7c19daa6c12d6462c00;

function _getVotingStreakNFTModuleStorage()
internal
pure
returns (VotingStreakNFTModuleStorage storage $)
{
function _getVotingStreakNFTModuleStorage() internal pure returns (VotingStreakNFTModuleStorage storage $) {
assembly {
$.slot := VOTING_STREAK_NFT_MODULE_STORAGE
}
Expand Down Expand Up @@ -68,11 +64,7 @@ contract VotingStreakNFTModule is BasisPointsVotingModule {
/// @param user The user address to query
/// @return streak Current consecutive voting streak
/// @return lastVoteCycle Last cycle in which the user successfully voted
function userActivity(address user)
external
view
returns (uint256 streak, uint256 lastVoteCycle)
{
function userActivity(address user) external view returns (uint256 streak, uint256 lastVoteCycle) {
VotingStreakNFTModuleStorage storage $ = _getVotingStreakNFTModuleStorage();
UserActivity storage activity = $.userActivity[user];
return (activity.streak, activity.lastVoteCycle);
Expand Down Expand Up @@ -135,10 +127,7 @@ contract VotingStreakNFTModule is BasisPointsVotingModule {
/// @param voter Address of the voter
/// @param points Array of basis points for allocation across recipients
/// @param votingPower Total voting power of the voter
function _processVote(address voter, uint256[] calldata points, uint256 votingPower)
internal
override
{
function _processVote(address voter, uint256[] calldata points, uint256 votingPower) internal override {
// Step 1: Execute standard voting math via parent
super._processVote(voter, points, votingPower);

Expand Down Expand Up @@ -186,6 +175,5 @@ contract VotingStreakNFTModule is BasisPointsVotingModule {
$.nftContract.mint(user);
}
}

}

5 changes: 1 addition & 4 deletions src/implementation/strategies/EqualDistributionStrategy.sol
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,7 @@ contract EqualDistributionStrategy is AbstractDistributionStrategy {
/// @param _yieldToken Address of the yield token to distribute
/// @param _distributionManager Address of the distribution manager
/// @param _owner Address that will own this contract (receives onlyOwner privileges)
function initialize(address _yieldToken, address _distributionManager, address _owner)
external
initializer
{
function initialize(address _yieldToken, address _distributionManager, address _owner) external initializer {
__AbstractDistributionStrategy_init(_yieldToken, _distributionManager, _owner);
}

Expand Down
6 changes: 1 addition & 5 deletions src/implementation/strategies/VotingDistributionStrategy.sol
Original file line number Diff line number Diff line change
Expand Up @@ -44,11 +44,7 @@ contract VotingDistributionStrategy is AbstractDistributionStrategy {
/// @param _yieldToken Address of the yield token to distribute
/// @param _distributionManager Address of the distribution manager
/// @param _owner Address that will own this contract (receives onlyOwner privileges)
function initialize(
address _yieldToken,
address _distributionManager,
address _owner
) external initializer {
function initialize(address _yieldToken, address _distributionManager, address _owner) external initializer {
__AbstractDistributionStrategy_init(_yieldToken, _distributionManager, _owner);
}

Expand Down
3 changes: 1 addition & 2 deletions src/interfaces/ICrowdstakeNFT.sol
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,4 @@ interface ICrowdstakeNFT is IERC721 {
/// @param to The address that will receive the NFT.
/// @return tokenId The unique ID of the newly minted NFT.
function mint(address to) external returns (uint256);

}
}
17 changes: 4 additions & 13 deletions test/DistributionCycleIntegration.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,7 @@ contract DistributionCycleIntegrationTest is Test {

// Deploy cycle module
CycleModule cycleImpl = new CycleModule();
bytes memory cycleInit =
abi.encodeWithSelector(AbstractCycleModule.initialize.selector, CYCLE_LENGTH, owner);
bytes memory cycleInit = abi.encodeWithSelector(AbstractCycleModule.initialize.selector, CYCLE_LENGTH, owner);
cycleModule = CycleModule(address(new ERC1967Proxy(address(cycleImpl), cycleInit)));

// Deploy base distribution manager
Expand Down Expand Up @@ -83,14 +82,10 @@ contract DistributionCycleIntegrationTest is Test {
abi.encode(dist)
);
vm.mockCall(
mockRegistry,
abi.encodeWithSelector(IRecipientRegistry.getRecipientCount.selector),
abi.encode(uint256(1))
mockRegistry, abi.encodeWithSelector(IRecipientRegistry.getRecipientCount.selector), abi.encode(uint256(1))
);
vm.mockCall(
mockBaseToken,
abi.encodeWithSelector(IYieldModule.yieldAccrued.selector),
abi.encode(uint256(1000))
mockBaseToken, abi.encodeWithSelector(IYieldModule.yieldAccrued.selector), abi.encode(uint256(1000))
);
vm.mockCall(
mockBaseToken,
Expand All @@ -102,11 +97,7 @@ contract DistributionCycleIntegrationTest is Test {
abi.encodeWithSelector(IERC20.transfer.selector, mockStrategy, uint256(1000)),
abi.encode(true)
);
vm.mockCall(
mockStrategy,
abi.encodeWithSelector(IDistributionStrategy.distribute.selector, uint256(1000)),
""
);
vm.mockCall(mockStrategy, abi.encodeWithSelector(IDistributionStrategy.distribute.selector, uint256(1000)), "");

// Execute
baseManager.claimAndDistribute();
Expand Down
23 changes: 4 additions & 19 deletions test/FactoryModuleDeployment.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -132,11 +132,7 @@ contract FactoryModuleDeploymentTest is Test {
vm.etch(mockDistManager, hex"00");

// Mock the distribution manager to return the registry
vm.mockCall(
mockDistManager,
abi.encodeWithSignature("recipientRegistry()"),
abi.encode(registry)
);
vm.mockCall(mockDistManager, abi.encodeWithSignature("recipientRegistry()"), abi.encode(registry));

bytes memory payload = abi.encodeWithSelector(
EqualDistributionStrategy.initialize.selector, mockYieldToken, mockDistManager, owner
Expand All @@ -162,22 +158,11 @@ contract FactoryModuleDeploymentTest is Test {
vm.etch(mockDistManager, hex"00");

// Mock the distribution manager to return registry and voting module
vm.mockCall(
mockDistManager,
abi.encodeWithSignature("recipientRegistry()"),
abi.encode(registry)
);
vm.mockCall(
mockDistManager,
abi.encodeWithSignature("votingModule()"),
abi.encode(mockVotingModule)
);
vm.mockCall(mockDistManager, abi.encodeWithSignature("recipientRegistry()"), abi.encode(registry));
vm.mockCall(mockDistManager, abi.encodeWithSignature("votingModule()"), abi.encode(mockVotingModule));

bytes memory payload = abi.encodeWithSelector(
VotingDistributionStrategy.initialize.selector,
mockYieldToken,
mockDistManager,
owner
VotingDistributionStrategy.initialize.selector, mockYieldToken, mockDistManager, owner
);
address module = factory.create(votingStrategyBeacon, payload, keccak256("voting-strat-salt"));

Expand Down
39 changes: 12 additions & 27 deletions test/VotingStreakNFTModule.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@ contract VotingStreakNFTModuleTest is Test {

// Act - Cycle 1: User votes
harness.exposed_processVote(user, points, VOTING_POWER);
(uint256 streak1, ) = harness.userActivity(user);
(uint256 streak1,) = harness.userActivity(user);
assertEq(streak1, 1, "Streak should be 1 after first vote");

// Act - Cycle 2: Advance cycle but don't vote (user misses this cycle)
Expand Down Expand Up @@ -180,22 +180,12 @@ contract VotingStreakNFTModuleTest is Test {
}

// Assert
assertEq(
mockNft.balanceOf(user),
1,
"User should have 1 NFT after 10 consecutive votes"
);
assertEq(mockNft.balanceOf(user), 1, "User should have 1 NFT after 10 consecutive votes");
(uint256 streak, uint256 lastVoteCycle) = harness.userActivity(user);
assertEq(streak, 10, "Streak should be 10");
assertEq(lastVoteCycle, 10, "lastVoteCycle should be 10");
}







/// @notice Test: Recasting vote in same cycle does not increment streak
/// @dev User votes twice in cycle 1; streak should remain 1
function test_ReCastVoteDoesNotIncrementStreak() public {
Expand All @@ -217,11 +207,7 @@ contract VotingStreakNFTModuleTest is Test {

// Assert after recast - streak should NOT increment
(uint256 streakAfterRecast, uint256 lastVoteCycleAfterRecast) = harness.userActivity(user);
assertEq(
streakAfterRecast,
1,
"Streak should remain 1 after recasting vote in same cycle"
);
assertEq(streakAfterRecast, 1, "Streak should remain 1 after recasting vote in same cycle");
assertEq(lastVoteCycleAfterRecast, 1, "lastVoteCycle should still be 1");
}

Expand Down Expand Up @@ -260,7 +246,7 @@ contract VotingStreakNFTModuleTest is Test {
harness.exposed_processVote(user, points, VOTING_POWER);
if (i < 2) cycleModule.advanceCycle();
}
(uint256 streak1, ) = harness.userActivity(user);
(uint256 streak1,) = harness.userActivity(user);
assertEq(streak1, 3, "Streak should be 3");

// Act - Miss a cycle to break streak
Expand All @@ -269,15 +255,15 @@ contract VotingStreakNFTModuleTest is Test {

// Act - Vote again, streak resets to 1
harness.exposed_processVote(user, points, VOTING_POWER);
(uint256 streak2, ) = harness.userActivity(user);
(uint256 streak2,) = harness.userActivity(user);
assertEq(streak2, 1, "Streak should reset to 1 after gap");

// Act - Build streak again to 5
for (uint256 i = 0; i < 4; i++) {
cycleModule.advanceCycle();
harness.exposed_processVote(user, points, VOTING_POWER);
}
(uint256 streak3, ) = harness.userActivity(user);
(uint256 streak3,) = harness.userActivity(user);
assertEq(streak3, 5, "Streak should build to 5 again");
}

Expand All @@ -291,31 +277,30 @@ contract VotingStreakNFTModuleTest is Test {

// Act & Assert - user1 votes in cycle 1
harness.exposed_processVote(user1, points, VOTING_POWER);
(uint256 streak1_c1, ) = harness.userActivity(user1);
(uint256 streak1_c1,) = harness.userActivity(user1);
assertEq(streak1_c1, 1, "user1 streak should be 1 in cycle 1");

// Act & Assert - user2 doesn't vote in cycle 1
(uint256 streak2_c1, ) = harness.userActivity(user2);
(uint256 streak2_c1,) = harness.userActivity(user2);
assertEq(streak2_c1, 0, "user2 streak should be 0 if never voted");

// Act - Advance to cycle 2
cycleModule.advanceCycle();

// Act & Assert - user1 votes again in cycle 2 (builds streak to 2)
harness.exposed_processVote(user1, points, VOTING_POWER);
(uint256 streak1_c2, ) = harness.userActivity(user1);
(uint256 streak1_c2,) = harness.userActivity(user1);
assertEq(streak1_c2, 2, "user1 streak should be 2 in cycle 2");

// Act & Assert - user2 votes for first time in cycle 2 (starts at 1)
harness.exposed_processVote(user2, points, VOTING_POWER);
(uint256 streak2_c2, ) = harness.userActivity(user2);
(uint256 streak2_c2,) = harness.userActivity(user2);
assertEq(streak2_c2, 1, "user2 streak should be 1 (first vote)");

// Assert final state
(uint256 finalStreak1, ) = harness.userActivity(user1);
(uint256 finalStreak2, ) = harness.userActivity(user2);
(uint256 finalStreak1,) = harness.userActivity(user1);
(uint256 finalStreak2,) = harness.userActivity(user2);
assertEq(finalStreak1, 2, "user1 should maintain streak of 2");
assertEq(finalStreak2, 1, "user2 should have streak of 1");
}

}
Loading