From 77389e7d0496da33838199dfef6a582f11bd492e Mon Sep 17 00:00:00 2001 From: Ivan P Date: Thu, 23 Apr 2026 21:19:04 -0600 Subject: [PATCH 1/4] feat: add EqualDistributionStrategy fuzz tests and parallel CI job Introduce test/fuzz with MockERC20, MockDistributionManagerForFuzz, and fuzz cases for equal split, dust, and revert paths. Made-with: Cursor --- .github/workflows/test.yml | 15 +++ foundry.toml | 3 + test/fuzz/EqualDistributionStrategy.t.sol | 111 ++++++++++++++++++ test/fuzz/README.md | 22 ++++ test/fuzz/helpers/MockDistributionManager.sol | 17 +++ test/fuzz/helpers/MockERC20.sol | 13 ++ 6 files changed, 181 insertions(+) create mode 100644 test/fuzz/EqualDistributionStrategy.t.sol create mode 100644 test/fuzz/README.md create mode 100644 test/fuzz/helpers/MockDistributionManager.sol create mode 100644 test/fuzz/helpers/MockERC20.sol diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 2f29732..23ea649 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -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 diff --git a/foundry.toml b/foundry.toml index 25b918f..5c2ea6b 100644 --- a/foundry.toml +++ b/foundry.toml @@ -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 diff --git a/test/fuzz/EqualDistributionStrategy.t.sol b/test/fuzz/EqualDistributionStrategy.t.sol new file mode 100644 index 0000000..f4a904e --- /dev/null +++ b/test/fuzz/EqualDistributionStrategy.t.sol @@ -0,0 +1,111 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import {Test} from "forge-std/Test.sol"; +import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; +import {EqualDistributionStrategy} from "../../src/implementation/strategies/EqualDistributionStrategy.sol"; +import {AbstractDistributionStrategy} from "../../src/abstract/AbstractDistributionStrategy.sol"; +import {AdminRecipientRegistry} from "../../src/implementation/registries/AdminRecipientRegistry.sol"; +import {IRecipientRegistry} from "../../src/interfaces/IRecipientRegistry.sol"; +import {MockERC20} from "./helpers/MockERC20.sol"; +import {MockDistributionManagerForFuzz} from "./helpers/MockDistributionManager.sol"; + +/// @notice Phase 1 smoke fuzzing: equal split math, dust retention, and revert conditions (no RPC fork). +contract EqualDistributionStrategy_FuzzTest is Test { + function testFuzz_distribute_equalSplitAndDust(uint8 recipientCountRaw, uint256 amountRaw) public { + uint256 n = bound(uint256(recipientCountRaw), 1, 48); + uint256 amount = bound(amountRaw, n, n * 1e24); + + ( + MockERC20 token, + AdminRecipientRegistry registry, + MockDistributionManagerForFuzz distManager, + EqualDistributionStrategy strategy + ) = _deploySystem(n); + + token.mint(address(strategy), amount); + uint256 idBefore = strategy.distributionId(); + + vm.prank(address(distManager)); + strategy.distribute(amount); + + assertEq(strategy.distributionId(), idBefore + 1); + + uint256 per = amount / n; + uint256 dust = amount % n; + address[] memory recipients = registry.getRecipients(); + assertEq(recipients.length, n); + + uint256 paidOut; + for (uint256 i; i < n; i++) { + uint256 b = token.balanceOf(recipients[i]); + assertEq(b, per); + paidOut += b; + } + assertEq(paidOut, per * n); + assertEq(token.balanceOf(address(strategy)), dust); + } + + function testFuzz_distribute_reverts_insufficientYield(uint256 nRaw, uint256 amountRaw) public { + uint256 n = bound(nRaw, 2, 48); + uint256 amount = bound(amountRaw, 1, n - 1); + + (MockERC20 token,, MockDistributionManagerForFuzz distManager, EqualDistributionStrategy strategy) = + _deploySystem(n); + + token.mint(address(strategy), amount); + + vm.prank(address(distManager)); + vm.expectRevert(AbstractDistributionStrategy.InsufficientYieldForRecipients.selector); + strategy.distribute(amount); + } + + function testFuzz_distribute_reverts_zeroAmount(uint256 nRaw) public { + uint256 n = bound(nRaw, 1, 48); + + (,, MockDistributionManagerForFuzz distManager, EqualDistributionStrategy strategy) = _deploySystem(n); + + vm.prank(address(distManager)); + vm.expectRevert(AbstractDistributionStrategy.ZeroAmount.selector); + strategy.distribute(0); + } + + function _deploySystem(uint256 n) + internal + returns ( + MockERC20 token, + AdminRecipientRegistry registry, + MockDistributionManagerForFuzz distManager, + EqualDistributionStrategy strategy + ) + { + address admin = address(this); + token = new MockERC20(); + + AdminRecipientRegistry regImpl = new AdminRecipientRegistry(); + bytes memory regInit = abi.encodeWithSelector(AdminRecipientRegistry.initialize.selector, admin); + registry = AdminRecipientRegistry(address(new ERC1967Proxy(address(regImpl), regInit))); + + address[] memory batch = new address[](n); + for (uint256 i; i < n; i++) { + // Deterministic unique EOAs for this deployment (avoids duplicate queue reverts). + batch[i] = address(uint160(uint256(0x100000 + i + 1))); + } + + registry.queueRecipientsAddition(batch); + registry.processQueue(); + + distManager = new MockDistributionManagerForFuzz(IRecipientRegistry(address(registry))); + + EqualDistributionStrategy stratImpl = new EqualDistributionStrategy(); + bytes memory stratInit = abi.encodeWithSelector( + EqualDistributionStrategy.initialize.selector, address(token), address(distManager), admin + ); + strategy = EqualDistributionStrategy(address(new ERC1967Proxy(address(stratImpl), stratInit))); + + assertEq(registry.getRecipientCount(), n); + assertEq(address(strategy.recipientRegistry()), address(registry)); + assertEq(strategy.distributionManager(), address(distManager)); + assertEq(address(strategy.yieldToken()), address(token)); + } +} diff --git a/test/fuzz/README.md b/test/fuzz/README.md new file mode 100644 index 0000000..9de42dd --- /dev/null +++ b/test/fuzz/README.md @@ -0,0 +1,22 @@ +# Fuzz tests (Phase 1) + +These tests use Foundry `testFuzz_*` against real protocol contracts with **local mocks only** (no `vm.createSelectFork` here). + +## Run + +```bash +# Fuzz tests only — uses root `foundry.toml` `[profile.default]` fuzz settings (same locally and in CI) +forge test --match-path 'test/fuzz/**' -vv +``` + +## CI + +Workflow **`env: FOUNDRY_PROFILE: ci`** is kept for the repo’s convention. Root **`foundry.toml` has no `[profile.ci]`**, so jobs use the same **`[profile.default]`** fuzz run count as local. + +The parallel **`fuzz`** job runs only `forge test --match-path 'test/fuzz/**'` (same profile as `check`). + +Vendored `lib/**/foundry.toml` files are not modified. + +## Fork tests + +Fork-based suites live in `test/TestWrapper.sol` and friends, not under `test/fuzz/`. diff --git a/test/fuzz/helpers/MockDistributionManager.sol b/test/fuzz/helpers/MockDistributionManager.sol new file mode 100644 index 0000000..8200645 --- /dev/null +++ b/test/fuzz/helpers/MockDistributionManager.sol @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import {IRecipientRegistry} from "../../../src/interfaces/IRecipientRegistry.sol"; + +/// @notice Minimal stub so `EqualDistributionStrategy` can read `recipientRegistry()` at init. +contract MockDistributionManagerForFuzz { + IRecipientRegistry public immutable recipientRegistry_; + + constructor(IRecipientRegistry registry_) { + recipientRegistry_ = registry_; + } + + function recipientRegistry() external view returns (IRecipientRegistry) { + return recipientRegistry_; + } +} diff --git a/test/fuzz/helpers/MockERC20.sol b/test/fuzz/helpers/MockERC20.sol new file mode 100644 index 0000000..827e8d8 --- /dev/null +++ b/test/fuzz/helpers/MockERC20.sol @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; + +/// @notice Minimal mintable ERC20 for fuzz tests (no fork). +contract MockERC20 is ERC20 { + constructor() ERC20("MockYield", "MYLD") {} + + function mint(address to, uint256 amount) external { + _mint(to, amount); + } +} From 1c75bba9db6f95df98a25393e1f302078f1f4e82 Mon Sep 17 00:00:00 2001 From: Ivan P Date: Thu, 23 Apr 2026 21:28:35 -0600 Subject: [PATCH 2/4] refactor: strengthen EqualDistributionStrategy fuzz tests Add access-control revert test, rename mock to avoid filename clash with test/mocks/, use makeAddr for labeled recipients, drop redundant mint in revert path, convert zero-amount case to a plain unit test, and add inline comments documenting invariants. Expand foundry.toml comment on the ci-profile convention. Made-with: Cursor --- foundry.toml | 3 +- test/fuzz/EqualDistributionStrategy.t.sol | 75 +++++++++++++------ test/fuzz/helpers/MockDistManagerForFuzz.sol | 14 ++++ test/fuzz/helpers/MockDistributionManager.sol | 17 ----- 4 files changed, 67 insertions(+), 42 deletions(-) create mode 100644 test/fuzz/helpers/MockDistManagerForFuzz.sol delete mode 100644 test/fuzz/helpers/MockDistributionManager.sol diff --git a/foundry.toml b/foundry.toml index 5c2ea6b..4f35bbe 100644 --- a/foundry.toml +++ b/foundry.toml @@ -3,7 +3,8 @@ src = "src" out = "out" libs = ["lib"] -# If `forge test` gets slow, lower this and/or add a dedicated CI profile with fewer runs +# If `forge test` gets slow, lower this and/or add a dedicated CI profile with fewer runs. +# Workflow sets FOUNDRY_PROFILE=ci by convention; [profile.ci] is intentionally absent so it merges to a no-op. fuzz = { runs = 1024 } # See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options diff --git a/test/fuzz/EqualDistributionStrategy.t.sol b/test/fuzz/EqualDistributionStrategy.t.sol index f4a904e..6483fb3 100644 --- a/test/fuzz/EqualDistributionStrategy.t.sol +++ b/test/fuzz/EqualDistributionStrategy.t.sol @@ -8,101 +8,128 @@ import {AbstractDistributionStrategy} from "../../src/abstract/AbstractDistribut import {AdminRecipientRegistry} from "../../src/implementation/registries/AdminRecipientRegistry.sol"; import {IRecipientRegistry} from "../../src/interfaces/IRecipientRegistry.sol"; import {MockERC20} from "./helpers/MockERC20.sol"; -import {MockDistributionManagerForFuzz} from "./helpers/MockDistributionManager.sol"; +import {MockDistManagerForFuzz} from "./helpers/MockDistManagerForFuzz.sol"; -/// @notice Phase 1 smoke fuzzing: equal split math, dust retention, and revert conditions (no RPC fork). +/// @notice Fuzz tests for EqualDistributionStrategy — equal split math, dust retention, and revert paths (no fork). contract EqualDistributionStrategy_FuzzTest is Test { + uint256 internal constant MAX_RECIPIENTS = 48; + + /// @notice Happy-path invariants: + /// - each recipient receives exactly `amount / n` + /// - total paid out equals `(amount / n) * n` + /// - dust (`amount % n`) remains on the strategy + /// - `distributionId` increments by exactly 1 function testFuzz_distribute_equalSplitAndDust(uint8 recipientCountRaw, uint256 amountRaw) public { - uint256 n = bound(uint256(recipientCountRaw), 1, 48); + uint256 n = bound(uint256(recipientCountRaw), 1, MAX_RECIPIENTS); + // Lower bound = n so the strategy has enough yield for 1 wei per recipient. uint256 amount = bound(amountRaw, n, n * 1e24); ( MockERC20 token, AdminRecipientRegistry registry, - MockDistributionManagerForFuzz distManager, + MockDistManagerForFuzz distManager, EqualDistributionStrategy strategy ) = _deploySystem(n); + // Pre-fund the strategy. In production the manager calls `safeTransfer` before `distribute`; + // we shortcut by minting directly since `distribute` is what we are fuzzing. token.mint(address(strategy), amount); uint256 idBefore = strategy.distributionId(); + // Act vm.prank(address(distManager)); strategy.distribute(amount); - assertEq(strategy.distributionId(), idBefore + 1); + // Invariant: distributionId incremented by 1 + assertEq(strategy.distributionId(), idBefore + 1, "distributionId must increment"); uint256 per = amount / n; uint256 dust = amount % n; address[] memory recipients = registry.getRecipients(); assertEq(recipients.length, n); + // Invariant: each recipient got `per`; sum equals `per * n` uint256 paidOut; for (uint256 i; i < n; i++) { - uint256 b = token.balanceOf(recipients[i]); - assertEq(b, per); - paidOut += b; + assertEq(token.balanceOf(recipients[i]), per, "recipient share must be amount / n"); + paidOut += token.balanceOf(recipients[i]); } - assertEq(paidOut, per * n); - assertEq(token.balanceOf(address(strategy)), dust); + assertEq(paidOut, per * n, "sum of shares must equal per * n"); + + // Invariant: leftover dust stays on the strategy + assertEq(token.balanceOf(address(strategy)), dust, "dust must remain on strategy"); } + /// @notice Revert when `amount < n` — cannot fund at least 1 wei per recipient. function testFuzz_distribute_reverts_insufficientYield(uint256 nRaw, uint256 amountRaw) public { - uint256 n = bound(nRaw, 2, 48); + uint256 n = bound(nRaw, 2, MAX_RECIPIENTS); uint256 amount = bound(amountRaw, 1, n - 1); - (MockERC20 token,, MockDistributionManagerForFuzz distManager, EqualDistributionStrategy strategy) = - _deploySystem(n); - - token.mint(address(strategy), amount); + (,, MockDistManagerForFuzz distManager, EqualDistributionStrategy strategy) = _deploySystem(n); + // Revert fires before any transfer, so no pre-fund needed. vm.prank(address(distManager)); vm.expectRevert(AbstractDistributionStrategy.InsufficientYieldForRecipients.selector); strategy.distribute(amount); } - function testFuzz_distribute_reverts_zeroAmount(uint256 nRaw) public { - uint256 n = bound(nRaw, 1, 48); - - (,, MockDistributionManagerForFuzz distManager, EqualDistributionStrategy strategy) = _deploySystem(n); + /// @notice Revert on zero amount regardless of recipient count. + function test_distribute_reverts_zeroAmount() public { + (,, MockDistManagerForFuzz distManager, EqualDistributionStrategy strategy) = _deploySystem(1); vm.prank(address(distManager)); vm.expectRevert(AbstractDistributionStrategy.ZeroAmount.selector); strategy.distribute(0); } + /// @notice Only the configured distribution manager can call `distribute`. + function testFuzz_distribute_reverts_notDistributionManager(address caller, uint256 amountRaw) public { + uint256 amount = bound(amountRaw, 1, 1e24); + + (,, MockDistManagerForFuzz distManager, EqualDistributionStrategy strategy) = _deploySystem(2); + vm.assume(caller != address(distManager)); + + vm.prank(caller); + vm.expectRevert(AbstractDistributionStrategy.OnlyDistributionManager.selector); + strategy.distribute(amount); + } + function _deploySystem(uint256 n) internal returns ( MockERC20 token, AdminRecipientRegistry registry, - MockDistributionManagerForFuzz distManager, + MockDistManagerForFuzz distManager, EqualDistributionStrategy strategy ) { address admin = address(this); token = new MockERC20(); + // Deploy registry behind ERC1967 proxy (matches production tests). AdminRecipientRegistry regImpl = new AdminRecipientRegistry(); bytes memory regInit = abi.encodeWithSelector(AdminRecipientRegistry.initialize.selector, admin); registry = AdminRecipientRegistry(address(new ERC1967Proxy(address(regImpl), regInit))); + // Labeled, deterministic recipient addresses — avoids duplicate-queue reverts and reads nicely in traces. address[] memory batch = new address[](n); for (uint256 i; i < n; i++) { - // Deterministic unique EOAs for this deployment (avoids duplicate queue reverts). - batch[i] = address(uint160(uint256(0x100000 + i + 1))); + batch[i] = makeAddr(string.concat("recipient-", vm.toString(i))); } - registry.queueRecipientsAddition(batch); registry.processQueue(); - distManager = new MockDistributionManagerForFuzz(IRecipientRegistry(address(registry))); + // Mock manager exists only to satisfy `initialize()` and be the `onlyDistributionManager` caller. + distManager = new MockDistManagerForFuzz(IRecipientRegistry(address(registry))); + // Deploy strategy behind ERC1967 proxy (matches production tests). EqualDistributionStrategy stratImpl = new EqualDistributionStrategy(); bytes memory stratInit = abi.encodeWithSelector( EqualDistributionStrategy.initialize.selector, address(token), address(distManager), admin ); strategy = EqualDistributionStrategy(address(new ERC1967Proxy(address(stratImpl), stratInit))); + // Sanity-check wiring before test body runs. assertEq(registry.getRecipientCount(), n); assertEq(address(strategy.recipientRegistry()), address(registry)); assertEq(strategy.distributionManager(), address(distManager)); diff --git a/test/fuzz/helpers/MockDistManagerForFuzz.sol b/test/fuzz/helpers/MockDistManagerForFuzz.sol new file mode 100644 index 0000000..7977c9a --- /dev/null +++ b/test/fuzz/helpers/MockDistManagerForFuzz.sol @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import {IRecipientRegistry} from "../../../src/interfaces/IRecipientRegistry.sol"; + +/// @notice Minimal stub so `EqualDistributionStrategy` can read `recipientRegistry()` at init and act as caller. +contract MockDistManagerForFuzz { + /// @notice Auto-generated getter matches IDistributionManager.recipientRegistry() selector. + IRecipientRegistry public immutable recipientRegistry; + + constructor(IRecipientRegistry registry_) { + recipientRegistry = registry_; + } +} diff --git a/test/fuzz/helpers/MockDistributionManager.sol b/test/fuzz/helpers/MockDistributionManager.sol deleted file mode 100644 index 8200645..0000000 --- a/test/fuzz/helpers/MockDistributionManager.sol +++ /dev/null @@ -1,17 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.20; - -import {IRecipientRegistry} from "../../../src/interfaces/IRecipientRegistry.sol"; - -/// @notice Minimal stub so `EqualDistributionStrategy` can read `recipientRegistry()` at init. -contract MockDistributionManagerForFuzz { - IRecipientRegistry public immutable recipientRegistry_; - - constructor(IRecipientRegistry registry_) { - recipientRegistry_ = registry_; - } - - function recipientRegistry() external view returns (IRecipientRegistry) { - return recipientRegistry_; - } -} From 68384c514f4d92729076e67240ef379fb903c4e8 Mon Sep 17 00:00:00 2001 From: Ivan P Date: Thu, 23 Apr 2026 21:29:59 -0600 Subject: [PATCH 3/4] Remove comment --- foundry.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/foundry.toml b/foundry.toml index 4f35bbe..ea1eeb8 100644 --- a/foundry.toml +++ b/foundry.toml @@ -4,7 +4,6 @@ out = "out" libs = ["lib"] # If `forge test` gets slow, lower this and/or add a dedicated CI profile with fewer runs. -# Workflow sets FOUNDRY_PROFILE=ci by convention; [profile.ci] is intentionally absent so it merges to a no-op. fuzz = { runs = 1024 } # See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options From 8632a80062291d40a145184f84ed7b4eab2bdb2b Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 24 Apr 2026 23:01:37 +0000 Subject: [PATCH 4/4] chore: auto-format code with forge fmt [auto-format] --- src/abstract/AbstractDistributionStrategy.sol | 17 ++++---- src/base/BasisPointsVotingModule.sol | 2 +- src/implementation/VotingStreakNFTModule.sol | 18 ++------- .../strategies/EqualDistributionStrategy.sol | 5 +-- .../strategies/VotingDistributionStrategy.sol | 6 +-- src/interfaces/ICrowdstakeNFT.sol | 3 +- test/DistributionCycleIntegration.t.sol | 17 ++------ test/FactoryModuleDeployment.t.sol | 23 ++--------- test/VotingStreakNFTModule.t.sol | 39 ++++++------------- test/mocks/MockCrowdstakeNFT.sol | 3 +- 10 files changed, 36 insertions(+), 97 deletions(-) diff --git a/src/abstract/AbstractDistributionStrategy.sol b/src/abstract/AbstractDistributionStrategy.sol index 619f292..e9f9370 100644 --- a/src/abstract/AbstractDistributionStrategy.sol +++ b/src/abstract/AbstractDistributionStrategy.sol @@ -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(); diff --git a/src/base/BasisPointsVotingModule.sol b/src/base/BasisPointsVotingModule.sol index b6c805f..277c2d8 100644 --- a/src/base/BasisPointsVotingModule.sol +++ b/src/base/BasisPointsVotingModule.sol @@ -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(); diff --git a/src/implementation/VotingStreakNFTModule.sol b/src/implementation/VotingStreakNFTModule.sol index a51aecb..69663de 100644 --- a/src/implementation/VotingStreakNFTModule.sol +++ b/src/implementation/VotingStreakNFTModule.sol @@ -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 } @@ -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); @@ -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); @@ -186,6 +175,5 @@ contract VotingStreakNFTModule is BasisPointsVotingModule { $.nftContract.mint(user); } } - } diff --git a/src/implementation/strategies/EqualDistributionStrategy.sol b/src/implementation/strategies/EqualDistributionStrategy.sol index 52c3035..c98a695 100644 --- a/src/implementation/strategies/EqualDistributionStrategy.sol +++ b/src/implementation/strategies/EqualDistributionStrategy.sol @@ -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); } diff --git a/src/implementation/strategies/VotingDistributionStrategy.sol b/src/implementation/strategies/VotingDistributionStrategy.sol index b9f460f..eec9c36 100644 --- a/src/implementation/strategies/VotingDistributionStrategy.sol +++ b/src/implementation/strategies/VotingDistributionStrategy.sol @@ -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); } diff --git a/src/interfaces/ICrowdstakeNFT.sol b/src/interfaces/ICrowdstakeNFT.sol index 8b69273..77ef962 100644 --- a/src/interfaces/ICrowdstakeNFT.sol +++ b/src/interfaces/ICrowdstakeNFT.sol @@ -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); - -} \ No newline at end of file +} diff --git a/test/DistributionCycleIntegration.t.sol b/test/DistributionCycleIntegration.t.sol index 5c3b61a..33c4a8a 100644 --- a/test/DistributionCycleIntegration.t.sol +++ b/test/DistributionCycleIntegration.t.sol @@ -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 @@ -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, @@ -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(); diff --git a/test/FactoryModuleDeployment.t.sol b/test/FactoryModuleDeployment.t.sol index 04943e5..a2ce003 100644 --- a/test/FactoryModuleDeployment.t.sol +++ b/test/FactoryModuleDeployment.t.sol @@ -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 @@ -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")); diff --git a/test/VotingStreakNFTModule.t.sol b/test/VotingStreakNFTModule.t.sol index a9b2556..e847874 100644 --- a/test/VotingStreakNFTModule.t.sol +++ b/test/VotingStreakNFTModule.t.sol @@ -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) @@ -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 { @@ -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"); } @@ -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 @@ -269,7 +255,7 @@ 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 @@ -277,7 +263,7 @@ contract VotingStreakNFTModuleTest is Test { 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"); } @@ -291,11 +277,11 @@ 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 @@ -303,19 +289,18 @@ contract VotingStreakNFTModuleTest is Test { // 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"); } - } diff --git a/test/mocks/MockCrowdstakeNFT.sol b/test/mocks/MockCrowdstakeNFT.sol index 32e671b..052ae89 100644 --- a/test/mocks/MockCrowdstakeNFT.sol +++ b/test/mocks/MockCrowdstakeNFT.sol @@ -18,5 +18,4 @@ contract MockCrowdstakeNFT is ERC721, ICrowdstakeNFT { _safeMint(to, tokenId); return tokenId; } - -} \ No newline at end of file +}