From a20b0873d6f16769a179a02fa38c20cf32c521c4 Mon Sep 17 00:00:00 2001 From: Zuhaib Mohammed Date: Thu, 6 Mar 2025 22:08:47 +0530 Subject: [PATCH 001/130] init: create the funding pot v1 template --- .../logicModule/LM_PC_FundingPot_v1.sol | 187 ++++++++++++++++++ .../interfaces/ILM_PC_FundingPot_v1.sol | 74 +++++++ .../logicModule/LM_PC_FundingPot_v1.t.sol | 177 +++++++++++++++++ .../logicModule/PP_FundingPot_v1_Exposed.sol | 30 +++ 4 files changed, 468 insertions(+) create mode 100644 src/modules/logicModule/LM_PC_FundingPot_v1.sol create mode 100644 src/modules/logicModule/interfaces/ILM_PC_FundingPot_v1.sol create mode 100644 test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol create mode 100644 test/unit/modules/logicModule/PP_FundingPot_v1_Exposed.sol diff --git a/src/modules/logicModule/LM_PC_FundingPot_v1.sol b/src/modules/logicModule/LM_PC_FundingPot_v1.sol new file mode 100644 index 000000000..2991b6536 --- /dev/null +++ b/src/modules/logicModule/LM_PC_FundingPot_v1.sol @@ -0,0 +1,187 @@ +// SPDX-License-Identifier: LGPL-3.0-only +pragma solidity 0.8.23; + +// Internal +import {IOrchestrator_v1} from + "src/orchestrator/interfaces/IOrchestrator_v1.sol"; +import { + IERC20PaymentClientBase_v2, + IPaymentProcessor_v2 +} from "@lm/abstracts/ERC20PaymentClientBase_v2.sol"; +import { + ERC20PaymentClientBase_v2, + Module_v1 +} from "@lm/abstracts/ERC20PaymentClientBase_v2.sol"; + +// External +import {IERC20} from "@oz/token/ERC20/IERC20.sol"; +import {SafeERC20} from "@oz/token/ERC20/utils/SafeERC20.sol"; +import {ERC165Upgradeable} from + "@oz-up/utils/introspection/ERC165Upgradeable.sol"; + +// System under Test (SuT) +import {ILM_PC_FundingPot_v1} from + "src/modules/logicModule/interfaces/ILM_PC_FundingPot_v1.sol"; + +/** + * @title Inverter Template Logic Module Payment Client + * + * @notice A template logic module payment client that handles deposits and payment processing. + * Users can deposit tokens up to a maximum amount, and authorized admins can process + * these deposits into payment orders. + * + * @dev This contract implements the following key functionality: + * - Deposit handling with maximum amount validation + * - Payment order creation and processing through the Orchestrator + * - Role-based access control for deposit processing + * - ERC20 token integration with SafeERC20 + * - Interface compliance checks via ERC165 + * + * @custom:security-contact security@inverter.network + * In case of any concerns or findings, please refer + * to our Security Policy at security.inverter.network + * or email us directly! + * + * @custom:version 1.0.0 + * + * @author Inverter Network + */ +contract LM_PC_FundingPot_v1 is + ILM_PC_FundingPot_v1, + ERC20PaymentClientBase_v2 +{ + // ========================================================================= + // Libraries + + using SafeERC20 for IERC20; + + // ========================================================================= + // ERC165 + + /// @inheritdoc ERC165Upgradeable + function supportsInterface(bytes4 interfaceId_) + public + view + virtual + override(ERC20PaymentClientBase_v2) + returns (bool) + { + return interfaceId_ == type(ILM_PC_FundingPot_v1).interfaceId + || super.supportsInterface(interfaceId_); + } + + //-------------------------------------------------------------------------- + // Constants + + uint internal constant _maxDepositAmount = 100 ether; + + //-------------------------------------------------------------------------- + // State + + /// @dev The role that allows processing deposits + bytes32 public constant DEPOSIT_ADMIN_ROLE = "DEPOSIT_ADMIN"; + + /// @notice Mapping of user addresses to their deposited token amounts. + mapping(address user => uint amount) internal _depositedAmounts; + + /// @notice Payment token. + IERC20 internal _paymentToken; + + /// @notice Storage gap for future upgrades. + uint[50] private __gap; + + // ========================================================================= + // Modifiers + + modifier onlyValidDepositAmount(uint amount_) { + _ensureValidDepositAmount(amount_); + _; + } + + // ========================================================================= + // Constructor & Init + + /// @inheritdoc Module_v1 + function init( + IOrchestrator_v1 orchestrator_, + Metadata memory metadata_, + bytes memory configData_ + ) external override(Module_v1) initializer { + __Module_init(orchestrator_, metadata_); + + // Decode module specific init data through use of configData bytes. + // This value is an example value used to showcase the setters/getters + // and internal functions/state formatting style. + (address paymentToken) = abi.decode(configData_, (address)); + + // Set init state. + _paymentToken = IERC20(paymentToken); + } + + // ========================================================================= + // Public - Mutating + + /// @inheritdoc ILM_PC_FundingPot_v1 + function deposit(uint amount_) + external + virtual + onlyValidDepositAmount(amount_) + { + // Update state. + _depositedAmounts[_msgSender()] += amount_; + + // Transfer tokens. + _paymentToken.safeTransferFrom(_msgSender(), address(this), amount_); + + // Emit event. + emit Deposited(_msgSender(), amount_); + } + + /// @inheritdoc ILM_PC_FundingPot_v1 + function processDeposit(address user_) + external + onlyModuleRole(DEPOSIT_ADMIN_ROLE) + { + uint amount = _depositedAmounts[user_]; + + // Clear the deposit amount before processing. + _depositedAmounts[user_] = 0; + + // Create and add payment order. + PaymentOrder memory order = PaymentOrder({ + recipient: user_, + paymentToken: address(_paymentToken), + amount: amount, + originChainId: block.chainid, + targetChainId: block.chainid, + flags: 0, + data: new bytes32[](0) + }); + + _addPaymentOrder(order); + + // Process the payment. + __Module_orchestrator.paymentProcessor().processPayments( + IERC20PaymentClientBase_v2(address(this)) + ); + } + + // ========================================================================= + // Public - Getters + + /// @inheritdoc ILM_PC_FundingPot_v1 + function getDepositedAmount(address user_) external view returns (uint) { + return _depositedAmounts[user_]; + } + + //-------------------------------------------------------------------------- + // Internal + + /// @dev Ensures the deposit amount is valid. + /// @param amount_ The amount to validate. + function _ensureValidDepositAmount(uint amount_) internal pure { + if (amount_ > _maxDepositAmount) { + revert Module__LM_PC_FundingPot_InvalidDepositAmount(); + } + } +} diff --git a/src/modules/logicModule/interfaces/ILM_PC_FundingPot_v1.sol b/src/modules/logicModule/interfaces/ILM_PC_FundingPot_v1.sol new file mode 100644 index 000000000..05209efea --- /dev/null +++ b/src/modules/logicModule/interfaces/ILM_PC_FundingPot_v1.sol @@ -0,0 +1,74 @@ +// SPDX-License-Identifier: LGPL-3.0-only +pragma solidity ^0.8.0; + +// Internal +import {IERC20PaymentClientBase_v2} from + "@lm/interfaces/IERC20PaymentClientBase_v2.sol"; + +/** + * @title Inverter Template Logic Module Payment Client + * + * @notice A template logic module payment client that handles deposits and payment processing. + * Users can deposit tokens up to a maximum amount, and authorized admins can process + * these deposits into payment orders. + * + * @dev This contract implements the following key functionality: + * - Deposit handling with maximum amount validation + * - Payment order creation and processing through the Orchestrator + * - Role-based access control for deposit processing + * - ERC20 token integration with SafeERC20 + * - Interface compliance checks via ERC165 + * + * Key components: + * - Inherits ERC20PaymentClientBase_v2 for payment client functionality + * - Uses DEPOSIT_ADMIN_ROLE for authorized payment processing + * - Tracks user deposits in _depositedAmounts mapping + * - Enforces maximum deposit limit of 100 ether + * - Processes payments through Orchestrator's payment processor + * + * @custom:security-contact security@inverter.network + * In case of any concerns or findings, please refer + * to our Security Policy at security.inverter.network + * or email us directly! + * + * @custom:version 1.0.0 + * + * @author Inverter Network + */ +interface ILM_PC_FundingPot_v1 is IERC20PaymentClientBase_v2 { + // ========================================================================= + // Events + + /// @notice Emit when the token amount has been deposited. + /// @param sender_ The address of the depositor. + /// @param amount_ The amount of tokens deposited. + event Deposited(address indexed sender_, uint amount_); + + // ========================================================================= + // Errors + + /// @notice Amount can not be zero. + error Module__LM_PC_FundingPot_InvalidDepositAmount(); + + // ========================================================================= + // Public - Getters + + /// @notice Returns the deposited balance of a specific address. + /// @param user_ The address of the user. + /// @return amount_ Deposited amount of the user. + function getDepositedAmount(address user_) + external + view + returns (uint amount_); + + // ========================================================================= + // Public - Mutating + + /// @notice Deposits tokens to the funding manager. + /// @param amount_ The amount of tokens to deposit. + function deposit(uint amount_) external; + + /// @notice Process a specific deposit by calling processPayments on the payment processor + /// @param user_ The address of the user whose deposit to process + function processDeposit(address user_) external; +} diff --git a/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol b/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol new file mode 100644 index 000000000..d1c99c877 --- /dev/null +++ b/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol @@ -0,0 +1,177 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.0; + +// Internal +import { + ModuleTest, + IModule_v1, + IOrchestrator_v1 +} from "test/modules/ModuleTest.sol"; +import {OZErrors} from "test/utils/errors/OZErrors.sol"; +import {ERC20Mock} from "test/utils/mocks/ERC20Mock.sol"; + +// External +import {Clones} from "@oz/proxy/Clones.sol"; + +// Tests and Mocks +import {LM_PC_FundingPot_v1_Exposed} from + "test/modules/logicModule/PP_FundingPot_v1_Exposed.sol"; +import { + IERC20PaymentClientBase_v2, + ERC20PaymentClientBaseV2Mock, + ERC20Mock +} from "test/utils/mocks/modules/paymentClient/ERC20PaymentClientBaseV2Mock.sol"; + +// System under Test (SuT) +import {ILM_PC_FundingPot_v1} from + "src/modules/logicModule/interfaces/ILM_PC_FundingPot_v1.sol"; + +/** + * @title Inverter Template Logic Module Payment Client Tests + * + * @notice Tests for the template logic module payment client + * + * @dev This test contract follows the standard testing pattern showing: + * - Initialization tests + * - External function tests + * - Internal function tests through exposed functions + * - Use of Gherkin for test documentation + * + * @author Inverter Network + */ +contract LM_PC_FundingPot_v1_Test is ModuleTest { + // ========================================================================= + // State + + // SuT + LM_PC_FundingPot_v1_Exposed paymentClient; + + // Mocks + ERC20Mock paymentToken; + + // ========================================================================= + // Setup + + function setUp() public { + // Setup the payment token + paymentToken = new ERC20Mock("Payment Token", "PT"); + + // Deploy the SuT + address impl = address(new LM_PC_FundingPot_v1_Exposed()); + paymentClient = LM_PC_FundingPot_v1_Exposed(Clones.clone(impl)); + + // Setup the module to test + _setUpOrchestrator(paymentClient); + + // Initiate the Logic Module with the metadata and config data + paymentClient.init( + _orchestrator, _METADATA, abi.encode(address(paymentToken)) + ); + } + + // ========================================================================= + // Test: Initialization + + // Test if the orchestrator is correctly set + function testInit() public override(ModuleTest) { + assertEq(address(paymentClient.orchestrator()), address(_orchestrator)); + } + + // Test the interface support + function testSupportsInterface() public { + assertTrue( + paymentClient.supportsInterface( + type(IERC20PaymentClientBase_v2).interfaceId + ) + ); + assertTrue( + paymentClient.supportsInterface( + type(ILM_PC_FundingPot_v1).interfaceId + ) + ); + } + + // Test the reinit function + function testReinitFails() public override(ModuleTest) { + vm.expectRevert(OZErrors.Initializable__InvalidInitialization); + paymentClient.init(_orchestrator, _METADATA, abi.encode("")); + } + + /* Test external deposit function + ├── Given valid deposit amount + │ └── When user deposits tokens + │ ├── Then their deposit balance should increase + │ └── Then tokens should be transferred to contract + └── Given invalid deposit amount + └── When user tries to deposit > maxDepositAmount + └── Then it should revert with InvalidDepositAmount + */ + function testDeposit_modifierInPlace() public { + uint invalidAmount = 101 ether; + + paymentToken.mint(address(this), invalidAmount); + paymentToken.approve(address(paymentClient), invalidAmount); + + vm.expectRevert( + ILM_PC_FundingPot_v1 + .Module__LM_PC_FundingPot_InvalidDepositAmount + .selector + ); + paymentClient.deposit(invalidAmount); + } + + /* Test external processDeposit function + ├── Given caller has DEPOSIT_ADMIN_ROLE + │ └── When processing a user's deposit + │ ├── Then their deposit balance should be cleared + │ └── Then a payment order should be created and processed + └── Given caller doesn't have DEPOSIT_ADMIN_ROLE + └── When trying to process a deposit + └── Then it should revert with CallerNotAuthorized (not done here) + */ + function testProcessDeposit() public { + // Grant DEPOSIT_ADMIN_ROLE to this test contract + bytes32 roleId = _authorizer.generateRoleId( + address(paymentClient), paymentClient.DEPOSIT_ADMIN_ROLE() + ); + _authorizer.grantRole(roleId, address(this)); + + address user = makeAddr("user"); + uint depositAmount = 50 ether; + + paymentToken.mint(user, depositAmount); + vm.prank(user); + paymentToken.approve(address(paymentClient), depositAmount); + + vm.prank(user); + paymentClient.deposit(depositAmount); + + paymentClient.processDeposit(user); + + assertEq(paymentClient.getDepositedAmount(user), 0); + } + + // Test external getDepositedAmount function + + // ========================================================================= + // Test: Internal (tested through exposed_ functions) + + /* Test internal _ensureValidDepositAmount() + ├── Given amount <= maxDepositAmount + │ └── When validating the amount + │ └── Then it should not revert (not done here) + └── Given amount > maxDepositAmount + └── When validating the amount + └── Then it should revert with InvalidDepositAmount + */ + // function testEnsureValidDepositAmount_revertsWhenAmountTooHigh() public { + // uint invalidAmount = 101 ether; + + // vm.expectRevert( + // ILM_PC_FundingPot_v1 + // .Module__LM_PC_FundingPot_InvalidDepositAmount + // .selector + // ); + // paymentClient.exposed_ensureValidDepositAmount(invalidAmount); + // } +} diff --git a/test/unit/modules/logicModule/PP_FundingPot_v1_Exposed.sol b/test/unit/modules/logicModule/PP_FundingPot_v1_Exposed.sol new file mode 100644 index 000000000..02a5f8d6c --- /dev/null +++ b/test/unit/modules/logicModule/PP_FundingPot_v1_Exposed.sol @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: LGPL-3.0-only +pragma solidity ^0.8.0; + +// Internal +import {LM_PC_FundingPot_v1} from + "src/modules/logicModule/LM_PC_FundingPot_v1.sol"; + +// Access Mock of the PP_FundingPot_v1 contract for Testing. +contract LM_PC_FundingPot_v1_Exposed is LM_PC_FundingPot_v1 { +// Use the `exposed_` prefix for functions to expose internal contract for +// testing. + +// function exposed_setPayoutAmountMultiplier(uint newPayoutAmountMultiplier_) +// external +// { +// _setPayoutAmountMultiplier(newPayoutAmountMultiplier_); +// } + +// function exposed_validPaymentReceiver(address receiver_) +// external +// view +// returns (bool validPaymentReceiver_) +// { +// validPaymentReceiver_ = _validPaymentReceiver(receiver_); +// } + +// function exposed_ensureValidClient(address client_) external view { +// _ensureValidClient(client_); +// } +} From fdec7884be6418a48e05795e811efcc35b6bafe1 Mon Sep 17 00:00:00 2001 From: Zuhaib Mohammed Date: Thu, 6 Mar 2025 22:25:46 +0530 Subject: [PATCH 002/130] feat: funding pot admin authorization --- .../logicModule/LM_PC_FundingPot_v1.sol | 21 +++++++++++++++++++ .../interfaces/ILM_PC_FundingPot_v1.sol | 8 +++++++ 2 files changed, 29 insertions(+) diff --git a/src/modules/logicModule/LM_PC_FundingPot_v1.sol b/src/modules/logicModule/LM_PC_FundingPot_v1.sol index 2991b6536..0eb6fcd5a 100644 --- a/src/modules/logicModule/LM_PC_FundingPot_v1.sol +++ b/src/modules/logicModule/LM_PC_FundingPot_v1.sol @@ -81,6 +81,9 @@ contract LM_PC_FundingPot_v1 is /// @dev The role that allows processing deposits bytes32 public constant DEPOSIT_ADMIN_ROLE = "DEPOSIT_ADMIN"; + /// @dev The role for the funding pot admin. + bytes32 public constant FUNDING_POT_ADMIN_ROLE = "FUNDING_POT_ADMIN"; + /// @notice Mapping of user addresses to their deposited token amounts. mapping(address user => uint amount) internal _depositedAmounts; @@ -166,6 +169,24 @@ contract LM_PC_FundingPot_v1 is ); } + function grantFundingPotAdminRole(address admin_) + external + onlyOrchestratorAdmin + { + __Module_orchestrator.authorizer().grantRoleFromModule( + FUNDING_POT_ADMIN_ROLE, admin_ + ); + } + + function revokeFundingPotAdminRole(address admin_) + external + onlyOrchestratorAdmin + { + __Module_orchestrator.authorizer().revokeRoleFromModule( + FUNDING_POT_ADMIN_ROLE, admin_ + ); + } + // ========================================================================= // Public - Getters diff --git a/src/modules/logicModule/interfaces/ILM_PC_FundingPot_v1.sol b/src/modules/logicModule/interfaces/ILM_PC_FundingPot_v1.sol index 05209efea..e0cd4eabb 100644 --- a/src/modules/logicModule/interfaces/ILM_PC_FundingPot_v1.sol +++ b/src/modules/logicModule/interfaces/ILM_PC_FundingPot_v1.sol @@ -71,4 +71,12 @@ interface ILM_PC_FundingPot_v1 is IERC20PaymentClientBase_v2 { /// @notice Process a specific deposit by calling processPayments on the payment processor /// @param user_ The address of the user whose deposit to process function processDeposit(address user_) external; + + /// @notice Grants the funding pot admin role to an address. + /// @param admin_ The address to grant the funding pot admin role to. + function grantFundingPotAdminRole(address admin_) external; + + /// @notice Revokes the funding pot admin role from an address. + /// @param admin_ The address to revoke the funding pot admin role from. + function revokeFundingPotAdminRole(address admin_) external; } From 5fea6e6fba7fb33e309d611d27329b06fd3093af Mon Sep 17 00:00:00 2001 From: JeffreyJoel Date: Fri, 7 Mar 2025 04:56:33 +0100 Subject: [PATCH 003/130] test: add tests for funding pot contract --- .../logicModule/LM_PC_FundingPot_v1.t.sol | 83 ++++++++++++++----- 1 file changed, 61 insertions(+), 22 deletions(-) diff --git a/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol b/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol index d1c99c877..d6f7544e0 100644 --- a/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol +++ b/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol @@ -23,8 +23,10 @@ import { } from "test/utils/mocks/modules/paymentClient/ERC20PaymentClientBaseV2Mock.sol"; // System under Test (SuT) -import {ILM_PC_FundingPot_v1} from - "src/modules/logicModule/interfaces/ILM_PC_FundingPot_v1.sol"; +import { + LM_PC_FundingPot_v1, + ILM_PC_FundingPot_v1 +} from "src/modules/logicModule/LM_PC_FundingPot_v1.sol"; /** * @title Inverter Template Logic Module Payment Client Tests @@ -39,16 +41,19 @@ import {ILM_PC_FundingPot_v1} from * * @author Inverter Network */ -contract LM_PC_FundingPot_v1_Test is ModuleTest { +contract LM_PC_FundingPot_v1Test is ModuleTest { // ========================================================================= // State // SuT - LM_PC_FundingPot_v1_Exposed paymentClient; + LM_PC_FundingPot_v1 fundingPot; // Mocks ERC20Mock paymentToken; + // Variables + address orchestratorAdmin = makeAddr("orchestratorAdmin"); + // ========================================================================= // Setup @@ -57,14 +62,16 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { paymentToken = new ERC20Mock("Payment Token", "PT"); // Deploy the SuT - address impl = address(new LM_PC_FundingPot_v1_Exposed()); - paymentClient = LM_PC_FundingPot_v1_Exposed(Clones.clone(impl)); + address impl = address(new LM_PC_FundingPot_v1()); + fundingPot = LM_PC_FundingPot_v1(Clones.clone(impl)); // Setup the module to test - _setUpOrchestrator(paymentClient); + _setUpOrchestrator(fundingPot); + _authorizer.grantRole(_authorizer.getAdminRole(), orchestratorAdmin); + _authorizer.grantRole(_authorizer.getAdminRole(), address(fundingPot)); // Initiate the Logic Module with the metadata and config data - paymentClient.init( + fundingPot.init( _orchestrator, _METADATA, abi.encode(address(paymentToken)) ); } @@ -74,27 +81,25 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { // Test if the orchestrator is correctly set function testInit() public override(ModuleTest) { - assertEq(address(paymentClient.orchestrator()), address(_orchestrator)); + assertEq(address(fundingPot.orchestrator()), address(_orchestrator)); } // Test the interface support function testSupportsInterface() public { assertTrue( - paymentClient.supportsInterface( + fundingPot.supportsInterface( type(IERC20PaymentClientBase_v2).interfaceId ) ); assertTrue( - paymentClient.supportsInterface( - type(ILM_PC_FundingPot_v1).interfaceId - ) + fundingPot.supportsInterface(type(ILM_PC_FundingPot_v1).interfaceId) ); } // Test the reinit function function testReinitFails() public override(ModuleTest) { vm.expectRevert(OZErrors.Initializable__InvalidInitialization); - paymentClient.init(_orchestrator, _METADATA, abi.encode("")); + fundingPot.init(_orchestrator, _METADATA, abi.encode("")); } /* Test external deposit function @@ -110,14 +115,14 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { uint invalidAmount = 101 ether; paymentToken.mint(address(this), invalidAmount); - paymentToken.approve(address(paymentClient), invalidAmount); + paymentToken.approve(address(fundingPot), invalidAmount); vm.expectRevert( ILM_PC_FundingPot_v1 .Module__LM_PC_FundingPot_InvalidDepositAmount .selector ); - paymentClient.deposit(invalidAmount); + fundingPot.deposit(invalidAmount); } /* Test external processDeposit function @@ -132,7 +137,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { function testProcessDeposit() public { // Grant DEPOSIT_ADMIN_ROLE to this test contract bytes32 roleId = _authorizer.generateRoleId( - address(paymentClient), paymentClient.DEPOSIT_ADMIN_ROLE() + address(fundingPot), fundingPot.DEPOSIT_ADMIN_ROLE() ); _authorizer.grantRole(roleId, address(this)); @@ -141,14 +146,14 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { paymentToken.mint(user, depositAmount); vm.prank(user); - paymentToken.approve(address(paymentClient), depositAmount); + paymentToken.approve(address(fundingPot), depositAmount); vm.prank(user); - paymentClient.deposit(depositAmount); + fundingPot.deposit(depositAmount); - paymentClient.processDeposit(user); + fundingPot.processDeposit(user); - assertEq(paymentClient.getDepositedAmount(user), 0); + assertEq(fundingPot.getDepositedAmount(user), 0); } // Test external getDepositedAmount function @@ -172,6 +177,40 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { // .Module__LM_PC_FundingPot_InvalidDepositAmount // .selector // ); - // paymentClient.exposed_ensureValidDepositAmount(invalidAmount); + // fundingPot.exposed_ensureValidDepositAmount(invalidAmount); // } + + function testFuzz_GrantFundingPotAdminRole(address admin_) public { + vm.assume(admin_ != address(0) && admin_ != orchestratorAdmin); + + vm.prank(orchestratorAdmin); + fundingPot.grantFundingPotAdminRole(admin_); + + assertEq( + _authorizer.hasRole( + _authorizer.generateRoleId( + address(fundingPot), fundingPot.FUNDING_POT_ADMIN_ROLE() + ), + admin_ + ), + true + ); + } + + function testFuzz_RevokeFundingPotAdminRole(address admin_) public { + vm.prank(orchestratorAdmin); + fundingPot.grantFundingPotAdminRole(admin_); + + fundingPot.revokeFundingPotAdminRole(admin_); + + assertEq( + _authorizer.hasRole( + _authorizer.generateRoleId( + address(fundingPot), fundingPot.FUNDING_POT_ADMIN_ROLE() + ), + admin_ + ), + false + ); + } } From 64fdd6a5717abc73e095308657e7d0b903e403f5 Mon Sep 17 00:00:00 2001 From: JeffreyJoel Date: Fri, 7 Mar 2025 06:27:45 +0100 Subject: [PATCH 004/130] feat: add checks for granting and revoking roles --- .../logicModule/LM_PC_FundingPot_v1.sol | 14 +++++++++++ .../interfaces/ILM_PC_FundingPot_v1.sol | 2 ++ .../logicModule/LM_PC_FundingPot_v1.t.sol | 23 ++++++++++++++++--- 3 files changed, 36 insertions(+), 3 deletions(-) diff --git a/src/modules/logicModule/LM_PC_FundingPot_v1.sol b/src/modules/logicModule/LM_PC_FundingPot_v1.sol index 0eb6fcd5a..5bfe242c8 100644 --- a/src/modules/logicModule/LM_PC_FundingPot_v1.sol +++ b/src/modules/logicModule/LM_PC_FundingPot_v1.sol @@ -173,6 +173,13 @@ contract LM_PC_FundingPot_v1 is external onlyOrchestratorAdmin { + if ( + __Module_orchestrator.authorizer().checkForRole( + FUNDING_POT_ADMIN_ROLE, admin_ + ) + ) { + revert Module__LM_PC_FundingPot_FundingPotAdminAlreadySet(); + } __Module_orchestrator.authorizer().grantRoleFromModule( FUNDING_POT_ADMIN_ROLE, admin_ ); @@ -182,6 +189,13 @@ contract LM_PC_FundingPot_v1 is external onlyOrchestratorAdmin { + if ( + !__Module_orchestrator.authorizer().checkForRole( + FUNDING_POT_ADMIN_ROLE, admin_ + ) + ) { + revert Module__LM_PC_FundingPot_AddressIsNotFundingPotAdmin(); + } __Module_orchestrator.authorizer().revokeRoleFromModule( FUNDING_POT_ADMIN_ROLE, admin_ ); diff --git a/src/modules/logicModule/interfaces/ILM_PC_FundingPot_v1.sol b/src/modules/logicModule/interfaces/ILM_PC_FundingPot_v1.sol index e0cd4eabb..029c3119e 100644 --- a/src/modules/logicModule/interfaces/ILM_PC_FundingPot_v1.sol +++ b/src/modules/logicModule/interfaces/ILM_PC_FundingPot_v1.sol @@ -49,6 +49,8 @@ interface ILM_PC_FundingPot_v1 is IERC20PaymentClientBase_v2 { /// @notice Amount can not be zero. error Module__LM_PC_FundingPot_InvalidDepositAmount(); + error Module__LM_PC_FundingPot_FundingPotAdminAlreadySet(); + error Module__LM_PC_FundingPot_AddressIsNotFundingPotAdmin(); // ========================================================================= // Public - Getters diff --git a/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol b/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol index d6f7544e0..dbb3c16d1 100644 --- a/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol +++ b/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol @@ -180,7 +180,9 @@ contract LM_PC_FundingPot_v1Test is ModuleTest { // fundingPot.exposed_ensureValidDepositAmount(invalidAmount); // } - function testFuzz_GrantFundingPotAdminRole(address admin_) public { + function testFuzz_GrantFundingPotAdminRole_Succeeds(address admin_) + public + { vm.assume(admin_ != address(0) && admin_ != orchestratorAdmin); vm.prank(orchestratorAdmin); @@ -197,11 +199,26 @@ contract LM_PC_FundingPot_v1Test is ModuleTest { ); } - function testFuzz_RevokeFundingPotAdminRole(address admin_) public { - vm.prank(orchestratorAdmin); + function testFuzz_GrantFundingPotAdminRole_Fails(address admin_) public { + vm.assume(admin_ != address(0) && admin_ != orchestratorAdmin); + + vm.startPrank(orchestratorAdmin); fundingPot.grantFundingPotAdminRole(admin_); + vm.expectRevert( + ILM_PC_FundingPot_v1 + .Module__LM_PC_FundingPot_FundingPotAdminAlreadySet + .selector + ); + fundingPot.grantFundingPotAdminRole(admin_); + vm.stopPrank(); + } + + function testFuzz_RevokeFundingPotAdminRole(address admin_) public { + vm.startPrank(orchestratorAdmin); + fundingPot.grantFundingPotAdminRole(admin_); fundingPot.revokeFundingPotAdminRole(admin_); + vm.stopPrank(); assertEq( _authorizer.hasRole( From c641ddd7ea209314057665e9f5b87810deb05f0b Mon Sep 17 00:00:00 2001 From: Zuhaib Mohammed Date: Fri, 7 Mar 2025 12:30:24 +0530 Subject: [PATCH 005/130] feat: add funding pot admin role check --- .../logicModule/LM_PC_FundingPot_v1.sol | 34 +++++++++++++------ .../interfaces/ILM_PC_FundingPot_v1.sol | 4 +++ 2 files changed, 28 insertions(+), 10 deletions(-) diff --git a/src/modules/logicModule/LM_PC_FundingPot_v1.sol b/src/modules/logicModule/LM_PC_FundingPot_v1.sol index 5bfe242c8..5b5fc83b8 100644 --- a/src/modules/logicModule/LM_PC_FundingPot_v1.sol +++ b/src/modules/logicModule/LM_PC_FundingPot_v1.sol @@ -169,15 +169,15 @@ contract LM_PC_FundingPot_v1 is ); } + /// @inheritdoc ILM_PC_FundingPot_v1 + /// @notice Grants the funding pot admin role to the given address. + /// @dev This function is only callable by the orchestrator admin. + /// @param admin_ The address to grant the funding pot admin role to. function grantFundingPotAdminRole(address admin_) external onlyOrchestratorAdmin { - if ( - __Module_orchestrator.authorizer().checkForRole( - FUNDING_POT_ADMIN_ROLE, admin_ - ) - ) { + if (_checkForFundingPotAdminRole(admin_)) { revert Module__LM_PC_FundingPot_FundingPotAdminAlreadySet(); } __Module_orchestrator.authorizer().grantRoleFromModule( @@ -185,15 +185,15 @@ contract LM_PC_FundingPot_v1 is ); } + /// @inheritdoc ILM_PC_FundingPot_v1 + /// @notice Revokes the funding pot admin role from the given address. + /// @dev This function is only callable by the orchestrator admin. + /// @param admin_ The address to revoke the funding pot admin role from. function revokeFundingPotAdminRole(address admin_) external onlyOrchestratorAdmin { - if ( - !__Module_orchestrator.authorizer().checkForRole( - FUNDING_POT_ADMIN_ROLE, admin_ - ) - ) { + if (!_checkForFundingPotAdminRole(admin_)) { revert Module__LM_PC_FundingPot_AddressIsNotFundingPotAdmin(); } __Module_orchestrator.authorizer().revokeRoleFromModule( @@ -219,4 +219,18 @@ contract LM_PC_FundingPot_v1 is revert Module__LM_PC_FundingPot_InvalidDepositAmount(); } } + + /// @dev Checks if the given address has the funding pot admin role. + /// @param admin_ The address to check for the funding pot admin role. + /// @return bool True if the address has the funding pot admin role, false otherwise. + function _checkForFundingPotAdminRole(address admin_) + internal + view + returns (bool) + { + bytes32 roleId = __Module_orchestrator.authorizer().generateRoleId( + address(this), FUNDING_POT_ADMIN_ROLE + ); + return __Module_orchestrator.authorizer().checkForRole(roleId, admin_); + } } diff --git a/src/modules/logicModule/interfaces/ILM_PC_FundingPot_v1.sol b/src/modules/logicModule/interfaces/ILM_PC_FundingPot_v1.sol index 029c3119e..5318817c7 100644 --- a/src/modules/logicModule/interfaces/ILM_PC_FundingPot_v1.sol +++ b/src/modules/logicModule/interfaces/ILM_PC_FundingPot_v1.sol @@ -49,7 +49,11 @@ interface ILM_PC_FundingPot_v1 is IERC20PaymentClientBase_v2 { /// @notice Amount can not be zero. error Module__LM_PC_FundingPot_InvalidDepositAmount(); + + /// @notice Funding pot admin role is already set. error Module__LM_PC_FundingPot_FundingPotAdminAlreadySet(); + + /// @notice Address is not funding pot admin. error Module__LM_PC_FundingPot_AddressIsNotFundingPotAdmin(); // ========================================================================= From 5da93522af8a5ad5c5abfb3a9412cda0ad4be62d Mon Sep 17 00:00:00 2001 From: Zuhaib Mohammed Date: Fri, 7 Mar 2025 15:53:09 +0530 Subject: [PATCH 006/130] test: add gherkin guide and unit tests --- .../logicModule/LM_PC_FundingPot_v1.sol | 19 +++-- .../interfaces/ILM_PC_FundingPot_v1.sol | 4 + .../logicModule/LM_PC_FundingPot_v1.t.sol | 80 ++++++++++++++++--- 3 files changed, 85 insertions(+), 18 deletions(-) diff --git a/src/modules/logicModule/LM_PC_FundingPot_v1.sol b/src/modules/logicModule/LM_PC_FundingPot_v1.sol index 5b5fc83b8..aec7d1401 100644 --- a/src/modules/logicModule/LM_PC_FundingPot_v1.sol +++ b/src/modules/logicModule/LM_PC_FundingPot_v1.sol @@ -180,8 +180,8 @@ contract LM_PC_FundingPot_v1 is if (_checkForFundingPotAdminRole(admin_)) { revert Module__LM_PC_FundingPot_FundingPotAdminAlreadySet(); } - __Module_orchestrator.authorizer().grantRoleFromModule( - FUNDING_POT_ADMIN_ROLE, admin_ + __Module_orchestrator.authorizer().grantRole( + getFundingPotAdminRoleId(), admin_ ); } @@ -196,8 +196,8 @@ contract LM_PC_FundingPot_v1 is if (!_checkForFundingPotAdminRole(admin_)) { revert Module__LM_PC_FundingPot_AddressIsNotFundingPotAdmin(); } - __Module_orchestrator.authorizer().revokeRoleFromModule( - FUNDING_POT_ADMIN_ROLE, admin_ + __Module_orchestrator.authorizer().revokeRole( + getFundingPotAdminRoleId(), admin_ ); } @@ -209,6 +209,12 @@ contract LM_PC_FundingPot_v1 is return _depositedAmounts[user_]; } + /// @notice Generates a role id for the funding pot admin role. + function getFundingPotAdminRoleId() public view returns (bytes32) { + return __Module_orchestrator.authorizer().generateRoleId( + address(this), FUNDING_POT_ADMIN_ROLE + ); + } //-------------------------------------------------------------------------- // Internal @@ -228,9 +234,8 @@ contract LM_PC_FundingPot_v1 is view returns (bool) { - bytes32 roleId = __Module_orchestrator.authorizer().generateRoleId( - address(this), FUNDING_POT_ADMIN_ROLE + return __Module_orchestrator.authorizer().checkForRole( + getFundingPotAdminRoleId(), admin_ ); - return __Module_orchestrator.authorizer().checkForRole(roleId, admin_); } } diff --git a/src/modules/logicModule/interfaces/ILM_PC_FundingPot_v1.sol b/src/modules/logicModule/interfaces/ILM_PC_FundingPot_v1.sol index 5318817c7..393ef228e 100644 --- a/src/modules/logicModule/interfaces/ILM_PC_FundingPot_v1.sol +++ b/src/modules/logicModule/interfaces/ILM_PC_FundingPot_v1.sol @@ -85,4 +85,8 @@ interface ILM_PC_FundingPot_v1 is IERC20PaymentClientBase_v2 { /// @notice Revokes the funding pot admin role from an address. /// @param admin_ The address to revoke the funding pot admin role from. function revokeFundingPotAdminRole(address admin_) external; + + /// @notice Returns the funding pot admin role id. + /// @return roleId_ The funding pot admin role id. + function getFundingPotAdminRoleId() external view returns (bytes32); } diff --git a/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol b/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol index dbb3c16d1..b509fa004 100644 --- a/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol +++ b/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol @@ -180,9 +180,12 @@ contract LM_PC_FundingPot_v1Test is ModuleTest { // fundingPot.exposed_ensureValidDepositAmount(invalidAmount); // } - function testFuzz_GrantFundingPotAdminRole_Succeeds(address admin_) - public - { + /* Test external grantFundingPotAdminRole() + ├── Given an address has the orchestrator admin role + │ └── When granting the funding pot admin role + │ └── Then it should be granted + */ + function testFuzz_GrantFundingPotAdminRole(address admin_) public { vm.assume(admin_ != address(0) && admin_ != orchestratorAdmin); vm.prank(orchestratorAdmin); @@ -199,7 +202,14 @@ contract LM_PC_FundingPot_v1Test is ModuleTest { ); } - function testFuzz_GrantFundingPotAdminRole_Fails(address admin_) public { + /* Test external grantFundingPotAdminRole() + ├── Given an address already has the funding pot admin role + │ └── When granting the funding pot admin role + │ └── Then it should revert with FundingPotAdminAlreadySet + */ + function testFuzz_GrantFundingPotAdminRole_failsWhenGrantedTwice( + address admin_ + ) public { vm.assume(admin_ != address(0) && admin_ != orchestratorAdmin); vm.startPrank(orchestratorAdmin); @@ -214,20 +224,68 @@ contract LM_PC_FundingPot_v1Test is ModuleTest { vm.stopPrank(); } + /* Test external grantFundingPotAdminRole() + ├── Given an address doesn't have the orchestrator admin role + │ └── When granting the funding pot admin role + │ └── Then it should revert with CallerNotAuthorized + */ + function testFuzz_GrantFundingPotAdminRole_failsWhenNotOrchestratorAdmin( + address caller_, + address admin_ + ) public { + vm.assume(caller_ != address(0) && caller_ != orchestratorAdmin); + vm.assume(admin_ != address(0) && admin_ != orchestratorAdmin); + vm.startPrank(caller_); + vm.expectRevert( + abi.encodeWithSelector( + IModule_v1.Module__CallerNotAuthorized.selector, + _orchestrator.authorizer().getAdminRole(), + caller_ + ) + ); + fundingPot.grantFundingPotAdminRole(admin_); + vm.stopPrank(); + } + + /* Test external revokeFundingPotAdminRole() + ├── Given an address has the funding pot admin role + │ └── When revoking the funding pot admin role + │ └── Then it should be revoked + */ function testFuzz_RevokeFundingPotAdminRole(address admin_) public { + testFuzz_GrantFundingPotAdminRole(admin_); + vm.startPrank(orchestratorAdmin); - fundingPot.grantFundingPotAdminRole(admin_); fundingPot.revokeFundingPotAdminRole(admin_); vm.stopPrank(); assertEq( - _authorizer.hasRole( - _authorizer.generateRoleId( - address(fundingPot), fundingPot.FUNDING_POT_ADMIN_ROLE() - ), - admin_ - ), + _authorizer.hasRole(fundingPot.getFundingPotAdminRoleId(), admin_), false ); } + + /* Test external revokeFundingPotAdminRole() + ├── Given an address doesn't have the funding pot admin role + │ └── When revoking the funding pot admin role + │ └── Then it should revert with CallerNotAuthorized + */ + function testFuzz_RevokeFundingPotAdminRole_failsWhenNotFundingPotAdmin( + address caller_, + address admin_ + ) public { + vm.assume(caller_ != address(0) && caller_ != orchestratorAdmin); + testFuzz_GrantFundingPotAdminRole(admin_); + + vm.startPrank(caller_); + vm.expectRevert( + abi.encodeWithSelector( + IModule_v1.Module__CallerNotAuthorized.selector, + _orchestrator.authorizer().getAdminRole(), + caller_ + ) + ); + fundingPot.revokeFundingPotAdminRole(admin_); + vm.stopPrank(); + } } From a07596a522c0089a7f531236183b4a1b0482c031 Mon Sep 17 00:00:00 2001 From: Zuhaib Mohammed Date: Fri, 7 Mar 2025 16:03:30 +0530 Subject: [PATCH 007/130] fix: add missing internal function to exposed_ --- .../logicModule/LM_PC_FundingPot_v1.t.sol | 6 +-- .../logicModule/PP_FundingPot_v1_Exposed.sol | 41 +++++++++++-------- 2 files changed, 27 insertions(+), 20 deletions(-) diff --git a/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol b/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol index b509fa004..96ba58b46 100644 --- a/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol +++ b/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol @@ -46,7 +46,7 @@ contract LM_PC_FundingPot_v1Test is ModuleTest { // State // SuT - LM_PC_FundingPot_v1 fundingPot; + LM_PC_FundingPot_v1_Exposed fundingPot; // Mocks ERC20Mock paymentToken; @@ -62,8 +62,8 @@ contract LM_PC_FundingPot_v1Test is ModuleTest { paymentToken = new ERC20Mock("Payment Token", "PT"); // Deploy the SuT - address impl = address(new LM_PC_FundingPot_v1()); - fundingPot = LM_PC_FundingPot_v1(Clones.clone(impl)); + address impl = address(new LM_PC_FundingPot_v1_Exposed()); + fundingPot = LM_PC_FundingPot_v1_Exposed(Clones.clone(impl)); // Setup the module to test _setUpOrchestrator(fundingPot); diff --git a/test/unit/modules/logicModule/PP_FundingPot_v1_Exposed.sol b/test/unit/modules/logicModule/PP_FundingPot_v1_Exposed.sol index 02a5f8d6c..f3ce70835 100644 --- a/test/unit/modules/logicModule/PP_FundingPot_v1_Exposed.sol +++ b/test/unit/modules/logicModule/PP_FundingPot_v1_Exposed.sol @@ -7,24 +7,31 @@ import {LM_PC_FundingPot_v1} from // Access Mock of the PP_FundingPot_v1 contract for Testing. contract LM_PC_FundingPot_v1_Exposed is LM_PC_FundingPot_v1 { -// Use the `exposed_` prefix for functions to expose internal contract for -// testing. + // Use the `exposed_` prefix for functions to expose internal contract for + // testing. -// function exposed_setPayoutAmountMultiplier(uint newPayoutAmountMultiplier_) -// external -// { -// _setPayoutAmountMultiplier(newPayoutAmountMultiplier_); -// } + // function exposed_setPayoutAmountMultiplier(uint newPayoutAmountMultiplier_) + // external + // { + // _setPayoutAmountMultiplier(newPayoutAmountMultiplier_); + // } -// function exposed_validPaymentReceiver(address receiver_) -// external -// view -// returns (bool validPaymentReceiver_) -// { -// validPaymentReceiver_ = _validPaymentReceiver(receiver_); -// } + // function exposed_validPaymentReceiver(address receiver_) + // external + // view + // returns (bool validPaymentReceiver_) + // { + // validPaymentReceiver_ = _validPaymentReceiver(receiver_); + // } -// function exposed_ensureValidClient(address client_) external view { -// _ensureValidClient(client_); -// } + // function exposed_ensureValidClient(address client_) external view { + // _ensureValidClient(client_); + // } + function exposed_checkForFundingPotAdminRole(address admin_) + external + view + returns (bool) + { + return _checkForFundingPotAdminRole(admin_); + } } From 29a697130575f1a299e8957ce82b7346b6816d4b Mon Sep 17 00:00:00 2001 From: JeffreyJoel Date: Sat, 8 Mar 2025 09:06:33 +0100 Subject: [PATCH 008/130] chore: remove redundant template code --- .../logicModule/LM_PC_FundingPot_v1.sol | 108 ------------------ .../interfaces/ILM_PC_FundingPot_v1.sol | 54 --------- .../logicModule/LM_PC_FundingPot_v1.t.sol | 80 +------------ .../LM_PC_FundingPot_v1_Exposed.sol | 20 ++++ .../logicModule/PP_FundingPot_v1_Exposed.sol | 17 --- 5 files changed, 21 insertions(+), 258 deletions(-) create mode 100644 test/unit/modules/logicModule/LM_PC_FundingPot_v1_Exposed.sol diff --git a/src/modules/logicModule/LM_PC_FundingPot_v1.sol b/src/modules/logicModule/LM_PC_FundingPot_v1.sol index aec7d1401..0d7aa4800 100644 --- a/src/modules/logicModule/LM_PC_FundingPot_v1.sol +++ b/src/modules/logicModule/LM_PC_FundingPot_v1.sol @@ -23,29 +23,6 @@ import {ERC165Upgradeable} from import {ILM_PC_FundingPot_v1} from "src/modules/logicModule/interfaces/ILM_PC_FundingPot_v1.sol"; -/** - * @title Inverter Template Logic Module Payment Client - * - * @notice A template logic module payment client that handles deposits and payment processing. - * Users can deposit tokens up to a maximum amount, and authorized admins can process - * these deposits into payment orders. - * - * @dev This contract implements the following key functionality: - * - Deposit handling with maximum amount validation - * - Payment order creation and processing through the Orchestrator - * - Role-based access control for deposit processing - * - ERC20 token integration with SafeERC20 - * - Interface compliance checks via ERC165 - * - * @custom:security-contact security@inverter.network - * In case of any concerns or findings, please refer - * to our Security Policy at security.inverter.network - * or email us directly! - * - * @custom:version 1.0.0 - * - * @author Inverter Network - */ contract LM_PC_FundingPot_v1 is ILM_PC_FundingPot_v1, ERC20PaymentClientBase_v2 @@ -73,34 +50,15 @@ contract LM_PC_FundingPot_v1 is //-------------------------------------------------------------------------- // Constants - uint internal constant _maxDepositAmount = 100 ether; - //-------------------------------------------------------------------------- // State - /// @dev The role that allows processing deposits - bytes32 public constant DEPOSIT_ADMIN_ROLE = "DEPOSIT_ADMIN"; - /// @dev The role for the funding pot admin. bytes32 public constant FUNDING_POT_ADMIN_ROLE = "FUNDING_POT_ADMIN"; - /// @notice Mapping of user addresses to their deposited token amounts. - mapping(address user => uint amount) internal _depositedAmounts; - - /// @notice Payment token. - IERC20 internal _paymentToken; - /// @notice Storage gap for future upgrades. uint[50] private __gap; - // ========================================================================= - // Modifiers - - modifier onlyValidDepositAmount(uint amount_) { - _ensureValidDepositAmount(amount_); - _; - } - // ========================================================================= // Constructor & Init @@ -111,64 +69,11 @@ contract LM_PC_FundingPot_v1 is bytes memory configData_ ) external override(Module_v1) initializer { __Module_init(orchestrator_, metadata_); - - // Decode module specific init data through use of configData bytes. - // This value is an example value used to showcase the setters/getters - // and internal functions/state formatting style. - (address paymentToken) = abi.decode(configData_, (address)); - - // Set init state. - _paymentToken = IERC20(paymentToken); } // ========================================================================= // Public - Mutating - /// @inheritdoc ILM_PC_FundingPot_v1 - function deposit(uint amount_) - external - virtual - onlyValidDepositAmount(amount_) - { - // Update state. - _depositedAmounts[_msgSender()] += amount_; - - // Transfer tokens. - _paymentToken.safeTransferFrom(_msgSender(), address(this), amount_); - - // Emit event. - emit Deposited(_msgSender(), amount_); - } - - /// @inheritdoc ILM_PC_FundingPot_v1 - function processDeposit(address user_) - external - onlyModuleRole(DEPOSIT_ADMIN_ROLE) - { - uint amount = _depositedAmounts[user_]; - - // Clear the deposit amount before processing. - _depositedAmounts[user_] = 0; - - // Create and add payment order. - PaymentOrder memory order = PaymentOrder({ - recipient: user_, - paymentToken: address(_paymentToken), - amount: amount, - originChainId: block.chainid, - targetChainId: block.chainid, - flags: 0, - data: new bytes32[](0) - }); - - _addPaymentOrder(order); - - // Process the payment. - __Module_orchestrator.paymentProcessor().processPayments( - IERC20PaymentClientBase_v2(address(this)) - ); - } - /// @inheritdoc ILM_PC_FundingPot_v1 /// @notice Grants the funding pot admin role to the given address. /// @dev This function is only callable by the orchestrator admin. @@ -204,11 +109,6 @@ contract LM_PC_FundingPot_v1 is // ========================================================================= // Public - Getters - /// @inheritdoc ILM_PC_FundingPot_v1 - function getDepositedAmount(address user_) external view returns (uint) { - return _depositedAmounts[user_]; - } - /// @notice Generates a role id for the funding pot admin role. function getFundingPotAdminRoleId() public view returns (bytes32) { return __Module_orchestrator.authorizer().generateRoleId( @@ -218,14 +118,6 @@ contract LM_PC_FundingPot_v1 is //-------------------------------------------------------------------------- // Internal - /// @dev Ensures the deposit amount is valid. - /// @param amount_ The amount to validate. - function _ensureValidDepositAmount(uint amount_) internal pure { - if (amount_ > _maxDepositAmount) { - revert Module__LM_PC_FundingPot_InvalidDepositAmount(); - } - } - /// @dev Checks if the given address has the funding pot admin role. /// @param admin_ The address to check for the funding pot admin role. /// @return bool True if the address has the funding pot admin role, false otherwise. diff --git a/src/modules/logicModule/interfaces/ILM_PC_FundingPot_v1.sol b/src/modules/logicModule/interfaces/ILM_PC_FundingPot_v1.sol index 393ef228e..f310e8e41 100644 --- a/src/modules/logicModule/interfaces/ILM_PC_FundingPot_v1.sol +++ b/src/modules/logicModule/interfaces/ILM_PC_FundingPot_v1.sol @@ -5,51 +5,13 @@ pragma solidity ^0.8.0; import {IERC20PaymentClientBase_v2} from "@lm/interfaces/IERC20PaymentClientBase_v2.sol"; -/** - * @title Inverter Template Logic Module Payment Client - * - * @notice A template logic module payment client that handles deposits and payment processing. - * Users can deposit tokens up to a maximum amount, and authorized admins can process - * these deposits into payment orders. - * - * @dev This contract implements the following key functionality: - * - Deposit handling with maximum amount validation - * - Payment order creation and processing through the Orchestrator - * - Role-based access control for deposit processing - * - ERC20 token integration with SafeERC20 - * - Interface compliance checks via ERC165 - * - * Key components: - * - Inherits ERC20PaymentClientBase_v2 for payment client functionality - * - Uses DEPOSIT_ADMIN_ROLE for authorized payment processing - * - Tracks user deposits in _depositedAmounts mapping - * - Enforces maximum deposit limit of 100 ether - * - Processes payments through Orchestrator's payment processor - * - * @custom:security-contact security@inverter.network - * In case of any concerns or findings, please refer - * to our Security Policy at security.inverter.network - * or email us directly! - * - * @custom:version 1.0.0 - * - * @author Inverter Network - */ interface ILM_PC_FundingPot_v1 is IERC20PaymentClientBase_v2 { // ========================================================================= // Events - /// @notice Emit when the token amount has been deposited. - /// @param sender_ The address of the depositor. - /// @param amount_ The amount of tokens deposited. - event Deposited(address indexed sender_, uint amount_); - // ========================================================================= // Errors - /// @notice Amount can not be zero. - error Module__LM_PC_FundingPot_InvalidDepositAmount(); - /// @notice Funding pot admin role is already set. error Module__LM_PC_FundingPot_FundingPotAdminAlreadySet(); @@ -59,25 +21,9 @@ interface ILM_PC_FundingPot_v1 is IERC20PaymentClientBase_v2 { // ========================================================================= // Public - Getters - /// @notice Returns the deposited balance of a specific address. - /// @param user_ The address of the user. - /// @return amount_ Deposited amount of the user. - function getDepositedAmount(address user_) - external - view - returns (uint amount_); - // ========================================================================= // Public - Mutating - /// @notice Deposits tokens to the funding manager. - /// @param amount_ The amount of tokens to deposit. - function deposit(uint amount_) external; - - /// @notice Process a specific deposit by calling processPayments on the payment processor - /// @param user_ The address of the user whose deposit to process - function processDeposit(address user_) external; - /// @notice Grants the funding pot admin role to an address. /// @param admin_ The address to grant the funding pot admin role to. function grantFundingPotAdminRole(address admin_) external; diff --git a/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol b/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol index 96ba58b46..389863530 100644 --- a/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol +++ b/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol @@ -15,7 +15,7 @@ import {Clones} from "@oz/proxy/Clones.sol"; // Tests and Mocks import {LM_PC_FundingPot_v1_Exposed} from - "test/modules/logicModule/PP_FundingPot_v1_Exposed.sol"; + "test/modules/logicModule/LM_PC_FundingPot_v1_Exposed.sol"; import { IERC20PaymentClientBase_v2, ERC20PaymentClientBaseV2Mock, @@ -102,84 +102,6 @@ contract LM_PC_FundingPot_v1Test is ModuleTest { fundingPot.init(_orchestrator, _METADATA, abi.encode("")); } - /* Test external deposit function - ├── Given valid deposit amount - │ └── When user deposits tokens - │ ├── Then their deposit balance should increase - │ └── Then tokens should be transferred to contract - └── Given invalid deposit amount - └── When user tries to deposit > maxDepositAmount - └── Then it should revert with InvalidDepositAmount - */ - function testDeposit_modifierInPlace() public { - uint invalidAmount = 101 ether; - - paymentToken.mint(address(this), invalidAmount); - paymentToken.approve(address(fundingPot), invalidAmount); - - vm.expectRevert( - ILM_PC_FundingPot_v1 - .Module__LM_PC_FundingPot_InvalidDepositAmount - .selector - ); - fundingPot.deposit(invalidAmount); - } - - /* Test external processDeposit function - ├── Given caller has DEPOSIT_ADMIN_ROLE - │ └── When processing a user's deposit - │ ├── Then their deposit balance should be cleared - │ └── Then a payment order should be created and processed - └── Given caller doesn't have DEPOSIT_ADMIN_ROLE - └── When trying to process a deposit - └── Then it should revert with CallerNotAuthorized (not done here) - */ - function testProcessDeposit() public { - // Grant DEPOSIT_ADMIN_ROLE to this test contract - bytes32 roleId = _authorizer.generateRoleId( - address(fundingPot), fundingPot.DEPOSIT_ADMIN_ROLE() - ); - _authorizer.grantRole(roleId, address(this)); - - address user = makeAddr("user"); - uint depositAmount = 50 ether; - - paymentToken.mint(user, depositAmount); - vm.prank(user); - paymentToken.approve(address(fundingPot), depositAmount); - - vm.prank(user); - fundingPot.deposit(depositAmount); - - fundingPot.processDeposit(user); - - assertEq(fundingPot.getDepositedAmount(user), 0); - } - - // Test external getDepositedAmount function - - // ========================================================================= - // Test: Internal (tested through exposed_ functions) - - /* Test internal _ensureValidDepositAmount() - ├── Given amount <= maxDepositAmount - │ └── When validating the amount - │ └── Then it should not revert (not done here) - └── Given amount > maxDepositAmount - └── When validating the amount - └── Then it should revert with InvalidDepositAmount - */ - // function testEnsureValidDepositAmount_revertsWhenAmountTooHigh() public { - // uint invalidAmount = 101 ether; - - // vm.expectRevert( - // ILM_PC_FundingPot_v1 - // .Module__LM_PC_FundingPot_InvalidDepositAmount - // .selector - // ); - // fundingPot.exposed_ensureValidDepositAmount(invalidAmount); - // } - /* Test external grantFundingPotAdminRole() ├── Given an address has the orchestrator admin role │ └── When granting the funding pot admin role diff --git a/test/unit/modules/logicModule/LM_PC_FundingPot_v1_Exposed.sol b/test/unit/modules/logicModule/LM_PC_FundingPot_v1_Exposed.sol new file mode 100644 index 000000000..a96804e24 --- /dev/null +++ b/test/unit/modules/logicModule/LM_PC_FundingPot_v1_Exposed.sol @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: LGPL-3.0-only +pragma solidity ^0.8.0; + +// Internal +import {LM_PC_FundingPot_v1} from + "src/modules/logicModule/LM_PC_FundingPot_v1.sol"; + +// Access Mock of the PP_FundingPot_v1 contract for Testing. +contract LM_PC_FundingPot_v1_Exposed is LM_PC_FundingPot_v1 { + // Use the `exposed_` prefix for functions to expose internal contract for + // testing. + + function exposed_checkForFundingPotAdminRole(address admin_) + external + view + returns (bool) + { + return _checkForFundingPotAdminRole(admin_); + } +} diff --git a/test/unit/modules/logicModule/PP_FundingPot_v1_Exposed.sol b/test/unit/modules/logicModule/PP_FundingPot_v1_Exposed.sol index f3ce70835..a96804e24 100644 --- a/test/unit/modules/logicModule/PP_FundingPot_v1_Exposed.sol +++ b/test/unit/modules/logicModule/PP_FundingPot_v1_Exposed.sol @@ -10,23 +10,6 @@ contract LM_PC_FundingPot_v1_Exposed is LM_PC_FundingPot_v1 { // Use the `exposed_` prefix for functions to expose internal contract for // testing. - // function exposed_setPayoutAmountMultiplier(uint newPayoutAmountMultiplier_) - // external - // { - // _setPayoutAmountMultiplier(newPayoutAmountMultiplier_); - // } - - // function exposed_validPaymentReceiver(address receiver_) - // external - // view - // returns (bool validPaymentReceiver_) - // { - // validPaymentReceiver_ = _validPaymentReceiver(receiver_); - // } - - // function exposed_ensureValidClient(address client_) external view { - // _ensureValidClient(client_); - // } function exposed_checkForFundingPotAdminRole(address admin_) external view From 307303d081d35ce64880c20d78466631b7fcbbda Mon Sep 17 00:00:00 2001 From: JeffreyJoel Date: Sat, 8 Mar 2025 09:20:54 +0100 Subject: [PATCH 009/130] test: remove unused test code --- .../logicModule/LM_PC_FundingPot_v1.t.sol | 21 +------------------ 1 file changed, 1 insertion(+), 20 deletions(-) diff --git a/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol b/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol index 389863530..e0717660f 100644 --- a/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol +++ b/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol @@ -28,19 +28,6 @@ import { ILM_PC_FundingPot_v1 } from "src/modules/logicModule/LM_PC_FundingPot_v1.sol"; -/** - * @title Inverter Template Logic Module Payment Client Tests - * - * @notice Tests for the template logic module payment client - * - * @dev This test contract follows the standard testing pattern showing: - * - Initialization tests - * - External function tests - * - Internal function tests through exposed functions - * - Use of Gherkin for test documentation - * - * @author Inverter Network - */ contract LM_PC_FundingPot_v1Test is ModuleTest { // ========================================================================= // State @@ -58,9 +45,6 @@ contract LM_PC_FundingPot_v1Test is ModuleTest { // Setup function setUp() public { - // Setup the payment token - paymentToken = new ERC20Mock("Payment Token", "PT"); - // Deploy the SuT address impl = address(new LM_PC_FundingPot_v1_Exposed()); fundingPot = LM_PC_FundingPot_v1_Exposed(Clones.clone(impl)); @@ -68,12 +52,9 @@ contract LM_PC_FundingPot_v1Test is ModuleTest { // Setup the module to test _setUpOrchestrator(fundingPot); _authorizer.grantRole(_authorizer.getAdminRole(), orchestratorAdmin); - _authorizer.grantRole(_authorizer.getAdminRole(), address(fundingPot)); // Initiate the Logic Module with the metadata and config data - fundingPot.init( - _orchestrator, _METADATA, abi.encode(address(paymentToken)) - ); + fundingPot.init(_orchestrator, _METADATA, abi.encode("")); } // ========================================================================= From 0afb10cb5684fbcbe2db52af5cff0d278a857f5f Mon Sep 17 00:00:00 2001 From: Zuhaib Mohammed Date: Mon, 10 Mar 2025 11:30:45 +0530 Subject: [PATCH 010/130] test: code format and unit tests --- .../logicModule/LM_PC_FundingPot_v1.t.sol | 37 ++++++++++++++++--- 1 file changed, 32 insertions(+), 5 deletions(-) diff --git a/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol b/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol index e0717660f..78352e3d4 100644 --- a/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol +++ b/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol @@ -89,9 +89,9 @@ contract LM_PC_FundingPot_v1Test is ModuleTest { │ └── Then it should be granted */ function testFuzz_GrantFundingPotAdminRole(address admin_) public { - vm.assume(admin_ != address(0) && admin_ != orchestratorAdmin); + _assumeValidFundingPotAdmin(admin_); - vm.prank(orchestratorAdmin); + vm.startPrank(orchestratorAdmin); fundingPot.grantFundingPotAdminRole(admin_); assertEq( @@ -103,6 +103,7 @@ contract LM_PC_FundingPot_v1Test is ModuleTest { ), true ); + vm.stopPrank(); } /* Test external grantFundingPotAdminRole() @@ -113,7 +114,7 @@ contract LM_PC_FundingPot_v1Test is ModuleTest { function testFuzz_GrantFundingPotAdminRole_failsWhenGrantedTwice( address admin_ ) public { - vm.assume(admin_ != address(0) && admin_ != orchestratorAdmin); + _assumeValidFundingPotAdmin(admin_); vm.startPrank(orchestratorAdmin); fundingPot.grantFundingPotAdminRole(admin_); @@ -137,7 +138,8 @@ contract LM_PC_FundingPot_v1Test is ModuleTest { address admin_ ) public { vm.assume(caller_ != address(0) && caller_ != orchestratorAdmin); - vm.assume(admin_ != address(0) && admin_ != orchestratorAdmin); + _assumeValidFundingPotAdmin(admin_); + vm.startPrank(caller_); vm.expectRevert( abi.encodeWithSelector( @@ -177,7 +179,7 @@ contract LM_PC_FundingPot_v1Test is ModuleTest { address caller_, address admin_ ) public { - vm.assume(caller_ != address(0) && caller_ != orchestratorAdmin); + _assumeValidFundingPotAdmin(admin_); testFuzz_GrantFundingPotAdminRole(admin_); vm.startPrank(caller_); @@ -191,4 +193,29 @@ contract LM_PC_FundingPot_v1Test is ModuleTest { fundingPot.revokeFundingPotAdminRole(admin_); vm.stopPrank(); } + + // ========================================================================= + // Test exposed_ functions + + function testFuzz_exposed_checkForFundingPotAdminRole(address admin_) + public + { + assertEq(fundingPot.exposed_checkForFundingPotAdminRole(admin_), false); + + vm.startPrank(orchestratorAdmin); + fundingPot.grantFundingPotAdminRole(admin_); + vm.stopPrank(); + + assertEq(fundingPot.exposed_checkForFundingPotAdminRole(admin_), true); + } + + // ========================================================================= + // Helper functions + + function _assumeValidFundingPotAdmin(address admin_) internal { + vm.assume( + admin_ != address(0) && admin_ != orchestratorAdmin + && admin_ != address(fundingPot) && admin_ != address(this) + ); + } } From 6d2fe122ceb8a24786027cffc383f22de4024c20 Mon Sep 17 00:00:00 2001 From: Zuhaib Mohammed Date: Tue, 11 Mar 2025 17:20:31 +0530 Subject: [PATCH 011/130] chore: apply PR review changes --- .../logicModule/LM_PC_FundingPot_v1.sol | 121 +++++------ .../interfaces/ILM_PC_FundingPot_v1.sol | 32 +-- .../logicModule/LM_PC_FundingPot_v1.t.sol | 201 +++++------------- .../LM_PC_FundingPot_v1_Exposed.sol | 14 +- 4 files changed, 119 insertions(+), 249 deletions(-) diff --git a/src/modules/logicModule/LM_PC_FundingPot_v1.sol b/src/modules/logicModule/LM_PC_FundingPot_v1.sol index 0d7aa4800..254ddcab8 100644 --- a/src/modules/logicModule/LM_PC_FundingPot_v1.sol +++ b/src/modules/logicModule/LM_PC_FundingPot_v1.sol @@ -4,6 +4,8 @@ pragma solidity 0.8.23; // Internal import {IOrchestrator_v1} from "src/orchestrator/interfaces/IOrchestrator_v1.sol"; +import {ILM_PC_FundingPot_v1} from + "src/modules/logicModule/interfaces/ILM_PC_FundingPot_v1.sol"; import { IERC20PaymentClientBase_v2, IPaymentProcessor_v2 @@ -19,20 +21,16 @@ import {SafeERC20} from "@oz/token/ERC20/utils/SafeERC20.sol"; import {ERC165Upgradeable} from "@oz-up/utils/introspection/ERC165Upgradeable.sol"; -// System under Test (SuT) -import {ILM_PC_FundingPot_v1} from - "src/modules/logicModule/interfaces/ILM_PC_FundingPot_v1.sol"; - contract LM_PC_FundingPot_v1 is ILM_PC_FundingPot_v1, ERC20PaymentClientBase_v2 { - // ========================================================================= + // ------------------------------------------------------------------------- // Libraries using SafeERC20 for IERC20; - // ========================================================================= + // ------------------------------------------------------------------------- // ERC165 /// @inheritdoc ERC165Upgradeable @@ -47,87 +45,82 @@ contract LM_PC_FundingPot_v1 is || super.supportsInterface(interfaceId_); } - //-------------------------------------------------------------------------- + // -------------------------------------------------------------------------- // Constants - //-------------------------------------------------------------------------- + /// @notice The role that allows creating funding rounds. + bytes32 internal constant FUNDING_POT_ADMIN_ROLE = "FUNDING_POT_ADMIN"; + + /// @notice The payment processor flag for the start timestamp. + uint8 internal constant FLAG_START = 1; + + /// @notice The payment processor flag for the cliff timestamp. + uint8 internal constant FLAG_CLIFF = 2; + + /// @notice The payment processor flag for the end timestamp. + uint8 internal constant FLAG_END = 3; + + // ------------------------------------------------------------------------- // State - /// @dev The role for the funding pot admin. - bytes32 public constant FUNDING_POT_ADMIN_ROLE = "FUNDING_POT_ADMIN"; + /// @notice Payment token. + IERC20 internal _paymentToken; - /// @notice Storage gap for future upgrades. + /// @notice Storage gap for future upgrades. uint[50] private __gap; - // ========================================================================= - // Constructor & Init + // ------------------------------------------------------------------------- + // Modifiers + + // ------------------------------------------------------------------------- + // Initialization - /// @inheritdoc Module_v1 + /// @notice The module's initializer function. + /// @dev CAN be overridden by downstream contract. + /// @dev MUST call `__Module_init()`. + /// @param orchestrator_ The orchestrator contract. + /// @param metadata_ The metadata of the module. + /// @param configData_ The config data of the module, comprised of: function init( IOrchestrator_v1 orchestrator_, Metadata memory metadata_, bytes memory configData_ ) external override(Module_v1) initializer { __Module_init(orchestrator_, metadata_); + + // Decode module specific init data through use of configData bytes. + // This value is an example value used to showcase the setters/getters + // and internal functions/state formatting style. + (address paymentToken) = abi.decode(configData_, (address)); + + // Set init state. + _paymentToken = IERC20(paymentToken); + + // Set the flags for the PaymentOrders (this module uses 3 flags). + bytes32 flags; + flags |= bytes32(1 << FLAG_START); + flags |= bytes32(1 << FLAG_CLIFF); + flags |= bytes32(1 << FLAG_END); + + __ERC20PaymentClientBase_v2_init(flags); } - // ========================================================================= - // Public - Mutating + // ------------------------------------------------------------------------- + // Public - Getters /// @inheritdoc ILM_PC_FundingPot_v1 - /// @notice Grants the funding pot admin role to the given address. - /// @dev This function is only callable by the orchestrator admin. - /// @param admin_ The address to grant the funding pot admin role to. - function grantFundingPotAdminRole(address admin_) - external - onlyOrchestratorAdmin - { - if (_checkForFundingPotAdminRole(admin_)) { - revert Module__LM_PC_FundingPot_FundingPotAdminAlreadySet(); - } - __Module_orchestrator.authorizer().grantRole( - getFundingPotAdminRoleId(), admin_ - ); + function getFundingPotAdminRole() external pure returns (bytes32 role_) { + return FUNDING_POT_ADMIN_ROLE; } /// @inheritdoc ILM_PC_FundingPot_v1 - /// @notice Revokes the funding pot admin role from the given address. - /// @dev This function is only callable by the orchestrator admin. - /// @param admin_ The address to revoke the funding pot admin role from. - function revokeFundingPotAdminRole(address admin_) - external - onlyOrchestratorAdmin - { - if (!_checkForFundingPotAdminRole(admin_)) { - revert Module__LM_PC_FundingPot_AddressIsNotFundingPotAdmin(); - } - __Module_orchestrator.authorizer().revokeRole( - getFundingPotAdminRoleId(), admin_ - ); + function getPaymentToken() external view returns (address token_) { + return address(_paymentToken); } - // ========================================================================= - // Public - Getters + // ------------------------------------------------------------------------- + // Public - Mutating - /// @notice Generates a role id for the funding pot admin role. - function getFundingPotAdminRoleId() public view returns (bytes32) { - return __Module_orchestrator.authorizer().generateRoleId( - address(this), FUNDING_POT_ADMIN_ROLE - ); - } - //-------------------------------------------------------------------------- + // ------------------------------------------------------------------------- // Internal - - /// @dev Checks if the given address has the funding pot admin role. - /// @param admin_ The address to check for the funding pot admin role. - /// @return bool True if the address has the funding pot admin role, false otherwise. - function _checkForFundingPotAdminRole(address admin_) - internal - view - returns (bool) - { - return __Module_orchestrator.authorizer().checkForRole( - getFundingPotAdminRoleId(), admin_ - ); - } } diff --git a/src/modules/logicModule/interfaces/ILM_PC_FundingPot_v1.sol b/src/modules/logicModule/interfaces/ILM_PC_FundingPot_v1.sol index f310e8e41..2522d45d0 100644 --- a/src/modules/logicModule/interfaces/ILM_PC_FundingPot_v1.sol +++ b/src/modules/logicModule/interfaces/ILM_PC_FundingPot_v1.sol @@ -6,33 +6,23 @@ import {IERC20PaymentClientBase_v2} from "@lm/interfaces/IERC20PaymentClientBase_v2.sol"; interface ILM_PC_FundingPot_v1 is IERC20PaymentClientBase_v2 { - // ========================================================================= + // ------------------------------------------------------------------------- // Events - // ========================================================================= + // ------------------------------------------------------------------------- // Errors - /// @notice Funding pot admin role is already set. - error Module__LM_PC_FundingPot_FundingPotAdminAlreadySet(); - - /// @notice Address is not funding pot admin. - error Module__LM_PC_FundingPot_AddressIsNotFundingPotAdmin(); - - // ========================================================================= + // ------------------------------------------------------------------------- // Public - Getters - // ========================================================================= - // Public - Mutating + /// @notice Returns the payment token. + /// @return token_ The address of the payment token. + function getPaymentToken() external view returns (address token_); - /// @notice Grants the funding pot admin role to an address. - /// @param admin_ The address to grant the funding pot admin role to. - function grantFundingPotAdminRole(address admin_) external; + /// @notice Returns the funding pot admin role. + /// @return role_ The address of the funding pot admin role. + function getFundingPotAdminRole() external pure returns (bytes32 role_); - /// @notice Revokes the funding pot admin role from an address. - /// @param admin_ The address to revoke the funding pot admin role from. - function revokeFundingPotAdminRole(address admin_) external; - - /// @notice Returns the funding pot admin role id. - /// @return roleId_ The funding pot admin role id. - function getFundingPotAdminRoleId() external view returns (bytes32); + // ------------------------------------------------------------------------- + // Public - Mutating } diff --git a/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol b/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol index 78352e3d4..1e185999d 100644 --- a/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol +++ b/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol @@ -14,8 +14,6 @@ import {ERC20Mock} from "test/utils/mocks/ERC20Mock.sol"; import {Clones} from "@oz/proxy/Clones.sol"; // Tests and Mocks -import {LM_PC_FundingPot_v1_Exposed} from - "test/modules/logicModule/LM_PC_FundingPot_v1_Exposed.sol"; import { IERC20PaymentClientBase_v2, ERC20PaymentClientBaseV2Mock, @@ -23,199 +21,96 @@ import { } from "test/utils/mocks/modules/paymentClient/ERC20PaymentClientBaseV2Mock.sol"; // System under Test (SuT) -import { - LM_PC_FundingPot_v1, - ILM_PC_FundingPot_v1 -} from "src/modules/logicModule/LM_PC_FundingPot_v1.sol"; - -contract LM_PC_FundingPot_v1Test is ModuleTest { - // ========================================================================= +import {LM_PC_FundingPot_v1_Exposed} from + "test/modules/logicModule/LM_PC_FundingPot_v1_Exposed.sol"; +import {ILM_PC_FundingPot_v1} from + "src/modules/logicModule/interfaces/ILM_PC_FundingPot_v1.sol"; + +import {console2} from "forge-std/console2.sol"; +/** + * @title Inverter Funding Pot Logic Module Tests + * + * @notice Tests for the funding pot logic module + * + * @dev This test contract follows the standard testing pattern showing: + * - Initialization tests + * - External function tests + * - Internal function tests through exposed functions + * - Use of Gherkin for test documentation + * + * @author Inverter Network + */ + +contract LM_PC_FundingPot_v1_Test is ModuleTest { + // ------------------------------------------------------------------------- + // Constants + + bytes32 internal constant FUNDING_POT_ADMIN_ROLE = "FUNDING_POT_ADMIN"; + + // ------------------------------------------------------------------------- // State // SuT LM_PC_FundingPot_v1_Exposed fundingPot; + address public fundingPotAdmin = makeAddr("FundingPotAdmin"); // Mocks - ERC20Mock paymentToken; + ERC20Mock public paymentToken; - // Variables - address orchestratorAdmin = makeAddr("orchestratorAdmin"); - - // ========================================================================= + // ------------------------------------------------------------------------- // Setup function setUp() public { + // Setup the payment token + paymentToken = new ERC20Mock("Payment Token", "PT"); + // Deploy the SuT address impl = address(new LM_PC_FundingPot_v1_Exposed()); fundingPot = LM_PC_FundingPot_v1_Exposed(Clones.clone(impl)); // Setup the module to test _setUpOrchestrator(fundingPot); - _authorizer.grantRole(_authorizer.getAdminRole(), orchestratorAdmin); // Initiate the Logic Module with the metadata and config data fundingPot.init(_orchestrator, _METADATA, abi.encode("")); + + // Give test contract the DEPOSIT_ADMIN_ROLE. + fundingPot.grantModuleRole( + fundingPot.getFundingPotAdminRole(), fundingPotAdmin + ); } - // ========================================================================= + // ------------------------------------------------------------------------- // Test: Initialization - // Test if the orchestrator is correctly set function testInit() public override(ModuleTest) { assertEq(address(fundingPot.orchestrator()), address(_orchestrator)); } - // Test the interface support function testSupportsInterface() public { assertTrue( - fundingPot.supportsInterface( - type(IERC20PaymentClientBase_v2).interfaceId - ) + fundingPot.supportsInterface(type(ILM_PC_FundingPot_v1).interfaceId) ); assertTrue( fundingPot.supportsInterface(type(ILM_PC_FundingPot_v1).interfaceId) ); } - // Test the reinit function function testReinitFails() public override(ModuleTest) { vm.expectRevert(OZErrors.Initializable__InvalidInitialization); fundingPot.init(_orchestrator, _METADATA, abi.encode("")); } - /* Test external grantFundingPotAdminRole() - ├── Given an address has the orchestrator admin role - │ └── When granting the funding pot admin role - │ └── Then it should be granted - */ - function testFuzz_GrantFundingPotAdminRole(address admin_) public { - _assumeValidFundingPotAdmin(admin_); - - vm.startPrank(orchestratorAdmin); - fundingPot.grantFundingPotAdminRole(admin_); - - assertEq( - _authorizer.hasRole( - _authorizer.generateRoleId( - address(fundingPot), fundingPot.FUNDING_POT_ADMIN_ROLE() - ), - admin_ - ), - true - ); - vm.stopPrank(); - } - - /* Test external grantFundingPotAdminRole() - ├── Given an address already has the funding pot admin role - │ └── When granting the funding pot admin role - │ └── Then it should revert with FundingPotAdminAlreadySet - */ - function testFuzz_GrantFundingPotAdminRole_failsWhenGrantedTwice( - address admin_ - ) public { - _assumeValidFundingPotAdmin(admin_); - - vm.startPrank(orchestratorAdmin); - fundingPot.grantFundingPotAdminRole(admin_); - - vm.expectRevert( - ILM_PC_FundingPot_v1 - .Module__LM_PC_FundingPot_FundingPotAdminAlreadySet - .selector - ); - fundingPot.grantFundingPotAdminRole(admin_); - vm.stopPrank(); - } - - /* Test external grantFundingPotAdminRole() - ├── Given an address doesn't have the orchestrator admin role - │ └── When granting the funding pot admin role - │ └── Then it should revert with CallerNotAuthorized - */ - function testFuzz_GrantFundingPotAdminRole_failsWhenNotOrchestratorAdmin( - address caller_, - address admin_ - ) public { - vm.assume(caller_ != address(0) && caller_ != orchestratorAdmin); - _assumeValidFundingPotAdmin(admin_); - - vm.startPrank(caller_); - vm.expectRevert( - abi.encodeWithSelector( - IModule_v1.Module__CallerNotAuthorized.selector, - _orchestrator.authorizer().getAdminRole(), - caller_ - ) - ); - fundingPot.grantFundingPotAdminRole(admin_); - vm.stopPrank(); - } - - /* Test external revokeFundingPotAdminRole() - ├── Given an address has the funding pot admin role - │ └── When revoking the funding pot admin role - │ └── Then it should be revoked - */ - function testFuzz_RevokeFundingPotAdminRole(address admin_) public { - testFuzz_GrantFundingPotAdminRole(admin_); - - vm.startPrank(orchestratorAdmin); - fundingPot.revokeFundingPotAdminRole(admin_); - vm.stopPrank(); - - assertEq( - _authorizer.hasRole(fundingPot.getFundingPotAdminRoleId(), admin_), - false - ); - } - - /* Test external revokeFundingPotAdminRole() - ├── Given an address doesn't have the funding pot admin role - │ └── When revoking the funding pot admin role - │ └── Then it should revert with CallerNotAuthorized - */ - function testFuzz_RevokeFundingPotAdminRole_failsWhenNotFundingPotAdmin( - address caller_, - address admin_ - ) public { - _assumeValidFundingPotAdmin(admin_); - testFuzz_GrantFundingPotAdminRole(admin_); - - vm.startPrank(caller_); - vm.expectRevert( - abi.encodeWithSelector( - IModule_v1.Module__CallerNotAuthorized.selector, - _orchestrator.authorizer().getAdminRole(), - caller_ - ) + function test_fundingPotAdminRoleGranted() public { + bytes32 roleId = _orchestrator.authorizer().generateRoleId( + address(fundingPot), fundingPot.getFundingPotAdminRole() ); - fundingPot.revokeFundingPotAdminRole(admin_); - vm.stopPrank(); + assertTrue(_orchestrator.authorizer().hasRole(roleId, fundingPotAdmin)); } - // ========================================================================= - // Test exposed_ functions + // ------------------------------------------------------------------------- + // Test External (public + external) - function testFuzz_exposed_checkForFundingPotAdminRole(address admin_) - public - { - assertEq(fundingPot.exposed_checkForFundingPotAdminRole(admin_), false); - - vm.startPrank(orchestratorAdmin); - fundingPot.grantFundingPotAdminRole(admin_); - vm.stopPrank(); - - assertEq(fundingPot.exposed_checkForFundingPotAdminRole(admin_), true); - } - - // ========================================================================= - // Helper functions - - function _assumeValidFundingPotAdmin(address admin_) internal { - vm.assume( - admin_ != address(0) && admin_ != orchestratorAdmin - && admin_ != address(fundingPot) && admin_ != address(this) - ); - } + // ------------------------------------------------------------------------- + // Test: Internal Functions } diff --git a/test/unit/modules/logicModule/LM_PC_FundingPot_v1_Exposed.sol b/test/unit/modules/logicModule/LM_PC_FundingPot_v1_Exposed.sol index a96804e24..cf0ae2756 100644 --- a/test/unit/modules/logicModule/LM_PC_FundingPot_v1_Exposed.sol +++ b/test/unit/modules/logicModule/LM_PC_FundingPot_v1_Exposed.sol @@ -5,16 +5,8 @@ pragma solidity ^0.8.0; import {LM_PC_FundingPot_v1} from "src/modules/logicModule/LM_PC_FundingPot_v1.sol"; -// Access Mock of the PP_FundingPot_v1 contract for Testing. +// Access Mock of the PP_Template_v1 contract for Testing. contract LM_PC_FundingPot_v1_Exposed is LM_PC_FundingPot_v1 { - // Use the `exposed_` prefix for functions to expose internal contract for - // testing. - - function exposed_checkForFundingPotAdminRole(address admin_) - external - view - returns (bool) - { - return _checkForFundingPotAdminRole(admin_); - } +// Use the `exposed_` prefix for functions to expose internal functions for +// testing. } From 3388d2f74304bd896d88afc3d98c8f26033c4e48 Mon Sep 17 00:00:00 2001 From: JeffreyJoel Date: Tue, 11 Mar 2025 20:07:36 +0100 Subject: [PATCH 012/130] Test: Add test for internal function --- .../logicModule/LM_PC_FundingPot_v1.sol | 10 ++++++++++ .../interfaces/ILM_PC_FundingPot_v1.sol | 2 ++ .../logicModule/LM_PC_FundingPot_v1.t.sol | 19 +++++++++++++++++++ .../LM_PC_FundingPot_v1_Exposed.sol | 8 ++++++-- 4 files changed, 37 insertions(+), 2 deletions(-) diff --git a/src/modules/logicModule/LM_PC_FundingPot_v1.sol b/src/modules/logicModule/LM_PC_FundingPot_v1.sol index 254ddcab8..9daf29ccb 100644 --- a/src/modules/logicModule/LM_PC_FundingPot_v1.sol +++ b/src/modules/logicModule/LM_PC_FundingPot_v1.sol @@ -48,6 +48,8 @@ contract LM_PC_FundingPot_v1 is // -------------------------------------------------------------------------- // Constants + uint internal constant _maxDepositAmount = 100 ether; + /// @notice The role that allows creating funding rounds. bytes32 internal constant FUNDING_POT_ADMIN_ROLE = "FUNDING_POT_ADMIN"; @@ -123,4 +125,12 @@ contract LM_PC_FundingPot_v1 is // ------------------------------------------------------------------------- // Internal + + /// @dev Ensures the deposit amount is valid. + /// @param amount_ The amount to validate. + function _ensureValidDepositAmount(uint amount_) internal pure { + if (amount_ > _maxDepositAmount) { + revert Module__LM_PC_FundingPot_InvalidDepositAmount(); + } + } } diff --git a/src/modules/logicModule/interfaces/ILM_PC_FundingPot_v1.sol b/src/modules/logicModule/interfaces/ILM_PC_FundingPot_v1.sol index 2522d45d0..d458811f6 100644 --- a/src/modules/logicModule/interfaces/ILM_PC_FundingPot_v1.sol +++ b/src/modules/logicModule/interfaces/ILM_PC_FundingPot_v1.sol @@ -12,6 +12,8 @@ interface ILM_PC_FundingPot_v1 is IERC20PaymentClientBase_v2 { // ------------------------------------------------------------------------- // Errors + /// @notice Amount can not be zero. + error Module__LM_PC_FundingPot_InvalidDepositAmount(); // ------------------------------------------------------------------------- // Public - Getters diff --git a/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol b/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol index 1e185999d..dea6e24d4 100644 --- a/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol +++ b/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol @@ -113,4 +113,23 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { // ------------------------------------------------------------------------- // Test: Internal Functions + + /* Test internal _ensureValidDepositAmount() + ├── Given amount <= maxDepositAmount + │ └── When validating the amount + │ └── Then it should not revert (not done here) + └── Given amount > maxDepositAmount + └── When validating the amount + └── Then it should revert with InvalidDepositAmount + */ + function testEnsureValidDepositAmount_revertsWhenAmountTooHigh() public { + uint invalidAmount = 101 ether; + + vm.expectRevert( + ILM_PC_FundingPot_v1 + .Module__LM_PC_FundingPot_InvalidDepositAmount + .selector + ); + fundingPot.exposed_ensureValidDepositAmount(invalidAmount); + } } diff --git a/test/unit/modules/logicModule/LM_PC_FundingPot_v1_Exposed.sol b/test/unit/modules/logicModule/LM_PC_FundingPot_v1_Exposed.sol index cf0ae2756..0b4318c74 100644 --- a/test/unit/modules/logicModule/LM_PC_FundingPot_v1_Exposed.sol +++ b/test/unit/modules/logicModule/LM_PC_FundingPot_v1_Exposed.sol @@ -7,6 +7,10 @@ import {LM_PC_FundingPot_v1} from // Access Mock of the PP_Template_v1 contract for Testing. contract LM_PC_FundingPot_v1_Exposed is LM_PC_FundingPot_v1 { -// Use the `exposed_` prefix for functions to expose internal functions for -// testing. + // Use the `exposed_` prefix for functions to expose internal functions for + // testing. + + function exposed_ensureValidDepositAmount(uint amount_) external pure { + _ensureValidDepositAmount(amount_); + } } From edadf3acfa892b467ac9f3dc7b5520313e73203a Mon Sep 17 00:00:00 2001 From: Zuhaib Mohammed Date: Wed, 12 Mar 2025 15:01:21 +0530 Subject: [PATCH 013/130] chore: remove redundant code --- .../logicModule/LM_PC_FundingPot_v1.sol | 10 -------- .../interfaces/ILM_PC_FundingPot_v1.sol | 2 -- .../logicModule/LM_PC_FundingPot_v1.t.sol | 25 ------------------- .../LM_PC_FundingPot_v1_Exposed.sol | 8 ++---- 4 files changed, 2 insertions(+), 43 deletions(-) diff --git a/src/modules/logicModule/LM_PC_FundingPot_v1.sol b/src/modules/logicModule/LM_PC_FundingPot_v1.sol index 9daf29ccb..254ddcab8 100644 --- a/src/modules/logicModule/LM_PC_FundingPot_v1.sol +++ b/src/modules/logicModule/LM_PC_FundingPot_v1.sol @@ -48,8 +48,6 @@ contract LM_PC_FundingPot_v1 is // -------------------------------------------------------------------------- // Constants - uint internal constant _maxDepositAmount = 100 ether; - /// @notice The role that allows creating funding rounds. bytes32 internal constant FUNDING_POT_ADMIN_ROLE = "FUNDING_POT_ADMIN"; @@ -125,12 +123,4 @@ contract LM_PC_FundingPot_v1 is // ------------------------------------------------------------------------- // Internal - - /// @dev Ensures the deposit amount is valid. - /// @param amount_ The amount to validate. - function _ensureValidDepositAmount(uint amount_) internal pure { - if (amount_ > _maxDepositAmount) { - revert Module__LM_PC_FundingPot_InvalidDepositAmount(); - } - } } diff --git a/src/modules/logicModule/interfaces/ILM_PC_FundingPot_v1.sol b/src/modules/logicModule/interfaces/ILM_PC_FundingPot_v1.sol index d458811f6..2522d45d0 100644 --- a/src/modules/logicModule/interfaces/ILM_PC_FundingPot_v1.sol +++ b/src/modules/logicModule/interfaces/ILM_PC_FundingPot_v1.sol @@ -12,8 +12,6 @@ interface ILM_PC_FundingPot_v1 is IERC20PaymentClientBase_v2 { // ------------------------------------------------------------------------- // Errors - /// @notice Amount can not be zero. - error Module__LM_PC_FundingPot_InvalidDepositAmount(); // ------------------------------------------------------------------------- // Public - Getters diff --git a/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol b/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol index dea6e24d4..1ae5f47d6 100644 --- a/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol +++ b/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol @@ -54,16 +54,10 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { LM_PC_FundingPot_v1_Exposed fundingPot; address public fundingPotAdmin = makeAddr("FundingPotAdmin"); - // Mocks - ERC20Mock public paymentToken; - // ------------------------------------------------------------------------- // Setup function setUp() public { - // Setup the payment token - paymentToken = new ERC20Mock("Payment Token", "PT"); - // Deploy the SuT address impl = address(new LM_PC_FundingPot_v1_Exposed()); fundingPot = LM_PC_FundingPot_v1_Exposed(Clones.clone(impl)); @@ -113,23 +107,4 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { // ------------------------------------------------------------------------- // Test: Internal Functions - - /* Test internal _ensureValidDepositAmount() - ├── Given amount <= maxDepositAmount - │ └── When validating the amount - │ └── Then it should not revert (not done here) - └── Given amount > maxDepositAmount - └── When validating the amount - └── Then it should revert with InvalidDepositAmount - */ - function testEnsureValidDepositAmount_revertsWhenAmountTooHigh() public { - uint invalidAmount = 101 ether; - - vm.expectRevert( - ILM_PC_FundingPot_v1 - .Module__LM_PC_FundingPot_InvalidDepositAmount - .selector - ); - fundingPot.exposed_ensureValidDepositAmount(invalidAmount); - } } diff --git a/test/unit/modules/logicModule/LM_PC_FundingPot_v1_Exposed.sol b/test/unit/modules/logicModule/LM_PC_FundingPot_v1_Exposed.sol index 0b4318c74..cf0ae2756 100644 --- a/test/unit/modules/logicModule/LM_PC_FundingPot_v1_Exposed.sol +++ b/test/unit/modules/logicModule/LM_PC_FundingPot_v1_Exposed.sol @@ -7,10 +7,6 @@ import {LM_PC_FundingPot_v1} from // Access Mock of the PP_Template_v1 contract for Testing. contract LM_PC_FundingPot_v1_Exposed is LM_PC_FundingPot_v1 { - // Use the `exposed_` prefix for functions to expose internal functions for - // testing. - - function exposed_ensureValidDepositAmount(uint amount_) external pure { - _ensureValidDepositAmount(amount_); - } +// Use the `exposed_` prefix for functions to expose internal functions for +// testing. } From 2deacdc02248d0eb8eddb17349b553b5e16c4b9c Mon Sep 17 00:00:00 2001 From: Zuhaib Mohammed Date: Fri, 14 Mar 2025 06:41:12 +0530 Subject: [PATCH 014/130] chore: inverter standard change --- .../logicModule/LM_PC_FundingPot_v1.sol | 25 ++----------------- .../interfaces/ILM_PC_FundingPot_v1.sol | 24 ++++++------------ .../logicModule/LM_PC_FundingPot_v1.t.sol | 4 +-- 3 files changed, 12 insertions(+), 41 deletions(-) diff --git a/src/modules/logicModule/LM_PC_FundingPot_v1.sol b/src/modules/logicModule/LM_PC_FundingPot_v1.sol index 254ddcab8..74311901b 100644 --- a/src/modules/logicModule/LM_PC_FundingPot_v1.sol +++ b/src/modules/logicModule/LM_PC_FundingPot_v1.sol @@ -49,7 +49,7 @@ contract LM_PC_FundingPot_v1 is // Constants /// @notice The role that allows creating funding rounds. - bytes32 internal constant FUNDING_POT_ADMIN_ROLE = "FUNDING_POT_ADMIN"; + bytes32 public constant FUNDING_POT_ADMIN_ROLE = "FUNDING_POT_ADMIN"; /// @notice The payment processor flag for the start timestamp. uint8 internal constant FLAG_START = 1; @@ -61,11 +61,8 @@ contract LM_PC_FundingPot_v1 is uint8 internal constant FLAG_END = 3; // ------------------------------------------------------------------------- - // State - - /// @notice Payment token. - IERC20 internal _paymentToken; + // State /// @notice Storage gap for future upgrades. uint[50] private __gap; @@ -88,14 +85,6 @@ contract LM_PC_FundingPot_v1 is ) external override(Module_v1) initializer { __Module_init(orchestrator_, metadata_); - // Decode module specific init data through use of configData bytes. - // This value is an example value used to showcase the setters/getters - // and internal functions/state formatting style. - (address paymentToken) = abi.decode(configData_, (address)); - - // Set init state. - _paymentToken = IERC20(paymentToken); - // Set the flags for the PaymentOrders (this module uses 3 flags). bytes32 flags; flags |= bytes32(1 << FLAG_START); @@ -108,16 +97,6 @@ contract LM_PC_FundingPot_v1 is // ------------------------------------------------------------------------- // Public - Getters - /// @inheritdoc ILM_PC_FundingPot_v1 - function getFundingPotAdminRole() external pure returns (bytes32 role_) { - return FUNDING_POT_ADMIN_ROLE; - } - - /// @inheritdoc ILM_PC_FundingPot_v1 - function getPaymentToken() external view returns (address token_) { - return address(_paymentToken); - } - // ------------------------------------------------------------------------- // Public - Mutating diff --git a/src/modules/logicModule/interfaces/ILM_PC_FundingPot_v1.sol b/src/modules/logicModule/interfaces/ILM_PC_FundingPot_v1.sol index 2522d45d0..7d8e3ee49 100644 --- a/src/modules/logicModule/interfaces/ILM_PC_FundingPot_v1.sol +++ b/src/modules/logicModule/interfaces/ILM_PC_FundingPot_v1.sol @@ -6,23 +6,15 @@ import {IERC20PaymentClientBase_v2} from "@lm/interfaces/IERC20PaymentClientBase_v2.sol"; interface ILM_PC_FundingPot_v1 is IERC20PaymentClientBase_v2 { - // ------------------------------------------------------------------------- - // Events +// ------------------------------------------------------------------------- +// Events - // ------------------------------------------------------------------------- - // Errors +// ------------------------------------------------------------------------- +// Errors - // ------------------------------------------------------------------------- - // Public - Getters +// ------------------------------------------------------------------------- +// Public - Getters - /// @notice Returns the payment token. - /// @return token_ The address of the payment token. - function getPaymentToken() external view returns (address token_); - - /// @notice Returns the funding pot admin role. - /// @return role_ The address of the funding pot admin role. - function getFundingPotAdminRole() external pure returns (bytes32 role_); - - // ------------------------------------------------------------------------- - // Public - Mutating +// ------------------------------------------------------------------------- +// Public - Mutating } diff --git a/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol b/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol index 1ae5f47d6..0024b1ae6 100644 --- a/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol +++ b/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol @@ -70,7 +70,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { // Give test contract the DEPOSIT_ADMIN_ROLE. fundingPot.grantModuleRole( - fundingPot.getFundingPotAdminRole(), fundingPotAdmin + fundingPot.FUNDING_POT_ADMIN_ROLE(), fundingPotAdmin ); } @@ -97,7 +97,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { function test_fundingPotAdminRoleGranted() public { bytes32 roleId = _orchestrator.authorizer().generateRoleId( - address(fundingPot), fundingPot.getFundingPotAdminRole() + address(fundingPot), fundingPot.FUNDING_POT_ADMIN_ROLE() ); assertTrue(_orchestrator.authorizer().hasRole(roleId, fundingPotAdmin)); } From 03694d125219c0f5fd33960296a6a00357a4db84 Mon Sep 17 00:00:00 2001 From: JeffreyJoel Date: Wed, 12 Mar 2025 11:14:53 +0100 Subject: [PATCH 015/130] Feat: Add round creation and edit functions --- .../logicModule/LM_PC_FundingPot_v1.sol | 112 +++++++++++ .../interfaces/ILM_PC_FundingPot_v1.sol | 137 ++++++++++++- .../logicModule/LM_PC_FundingPot_v1.t.sol | 190 ++++++++++++++++++ 3 files changed, 431 insertions(+), 8 deletions(-) diff --git a/src/modules/logicModule/LM_PC_FundingPot_v1.sol b/src/modules/logicModule/LM_PC_FundingPot_v1.sol index 74311901b..fdad4ff69 100644 --- a/src/modules/logicModule/LM_PC_FundingPot_v1.sol +++ b/src/modules/logicModule/LM_PC_FundingPot_v1.sol @@ -63,6 +63,16 @@ contract LM_PC_FundingPot_v1 is // ------------------------------------------------------------------------- // State + + /// @notice Payment token. + IERC20 internal _paymentToken; + + /// @notice Stores all funding rounds by their unique ID. + mapping(uint64 => Round) public rounds; + + /// @notice The next available round ID. + uint64 private nextRoundId; + /// @notice Storage gap for future upgrades. uint[50] private __gap; @@ -100,6 +110,108 @@ contract LM_PC_FundingPot_v1 is // ------------------------------------------------------------------------- // Public - Mutating + /// @inheritdoc ILM_PC_FundingPot_v1 + function createRound( + uint _roundStart, + uint _roundEnd, + uint _roundCap, + address _hookContract, + bytes memory _hookFunction, + bool _closureMechanism, + bool _globalAccumulativeCaps + ) external returns (uint64) { + if (_roundStart <= block.timestamp) { + revert Module__LM_PC_FundingPot__RoundStartMustBeInFuture(); + } + + if (_roundEnd <= _roundStart && _roundCap == 0) { + revert Module__LM_PC_FundingPot__RoundMustHaveEndTimeOrCap(); + } + + if (_roundEnd > 0 && _roundEnd <= _roundStart) { + revert Module__LM_PC_FundingPot__RoundEndMustBeAfterStart(); + } + + uint64 roundId = nextRoundId; + rounds[roundId] = Round({ + roundStart: _roundStart, + roundEnd: _roundEnd, + roundCap: _roundCap, + hookContract: _hookContract, + hookFunction: _hookFunction, + closureMechanism: _closureMechanism, + globalAccumulativeCaps: _globalAccumulativeCaps, + isActive: true + }); + + nextRoundId++; + + emit RoundCreated( + roundId, + _roundStart, + _roundEnd, + _roundCap, + _hookContract, + _closureMechanism, + _globalAccumulativeCaps + ); + + return roundId; + } + + /// @inheritdoc ILM_PC_FundingPot_v1 + function editRound( + uint64 _roundId, + uint _roundStart, + uint _roundEnd, + uint _roundCap, + address _hookContract, + bytes memory _hookFunction, + bool _closureMechanism, + bool _globalAccumulativeCaps + ) external returns (bool) { + Round storage round = rounds[_roundId]; + + if (round.roundStart == 0) { + revert Module__LM_PC_FundingPot__RoundDoesNotExist(); + } + + if (block.timestamp >= round.roundStart) { + revert Module__LM_PC_FundingPot__RoundAlreadyStarted(); + } + + if (_roundStart <= block.timestamp) { + revert Module__LM_PC_FundingPot__RoundStartMustBeInFuture(); + } + + if (_roundEnd <= _roundStart && _roundCap == 0) { + revert Module__LM_PC_FundingPot__RoundMustHaveEndTimeOrCap(); + } + + if (_roundEnd > 0 && _roundEnd <= _roundStart) { + revert Module__LM_PC_FundingPot__RoundEndMustBeAfterStart(); + } + + round.roundStart = _roundStart; + round.roundEnd = _roundEnd; + round.roundCap = _roundCap; + round.hookContract = _hookContract; + round.hookFunction = _hookFunction; + round.closureMechanism = _closureMechanism; + round.globalAccumulativeCaps = _globalAccumulativeCaps; + + emit RoundEdited( + _roundId, + _roundStart, + _roundEnd, + _roundCap, + _hookContract, + _closureMechanism, + _globalAccumulativeCaps + ); + + return true; + } // ------------------------------------------------------------------------- // Internal } diff --git a/src/modules/logicModule/interfaces/ILM_PC_FundingPot_v1.sol b/src/modules/logicModule/interfaces/ILM_PC_FundingPot_v1.sol index 7d8e3ee49..5d24a8bfb 100644 --- a/src/modules/logicModule/interfaces/ILM_PC_FundingPot_v1.sol +++ b/src/modules/logicModule/interfaces/ILM_PC_FundingPot_v1.sol @@ -6,15 +6,136 @@ import {IERC20PaymentClientBase_v2} from "@lm/interfaces/IERC20PaymentClientBase_v2.sol"; interface ILM_PC_FundingPot_v1 is IERC20PaymentClientBase_v2 { -// ------------------------------------------------------------------------- -// Events + //-------------------------------------------------------------------------- + // Structs -// ------------------------------------------------------------------------- -// Errors + /// @notice Struct used to store information about a funding round. + /// @param roundStart Timestamp indicating when the round starts. + /// @param roundEnd Timestamp indicating when the round ends. If set to `0`, the round operates only based on `roundCap`. + /// @param roundCap Maximum contribution cap in collateral tokens. If set to `0`, the round operates only based on `roundEnd`. + /// @param hookContract Address of an optional hook contract to be called after round closure. + /// @param hookFunction Encoded function call to be executed on the `hookContract` after round closure. + /// @param closureMechanism Indicates whether the hook closure coincides with the contribution span end. + /// @param globalAccumulativeCaps Indicates whether contribution caps accumulate globally across rounds. + /// @param isActive Indicates whether the round is currently active. + struct Round { + uint roundStart; + uint roundEnd; + uint roundCap; + address hookContract; + bytes hookFunction; + bool closureMechanism; + bool globalAccumulativeCaps; + bool isActive; + } -// ------------------------------------------------------------------------- -// Public - Getters + // ------------------------------------------------------------------------- + // Events -// ------------------------------------------------------------------------- -// Public - Mutating + /// @notice Emitted when a new round is created. + /// @dev This event signals the creation of a new round with specific parameters. + /// @param roundId The unique identifier for the round. + /// @param roundStart The timestamp when the round starts. + /// @param roundEnd The timestamp when the round ends. + /// @param roundCap The maximum allocation or cap for the round. + /// @param hookContract The address of an optional hook contract for custom logic. + /// @param closureMechanism A boolean indicating whether a specific closure mechanism is enabled. + /// @param globalAccumulativeCaps A boolean indicating whether global accumulative caps are enforced. + event RoundCreated( + uint indexed roundId, + uint roundStart, + uint roundEnd, + uint roundCap, + address hookContract, + bool closureMechanism, + bool globalAccumulativeCaps + ); + + /// @notice Emitted when an existing round is edited. + /// @dev This event signals modifications to an existing round's parameters. + /// @param roundId The unique identifier of the round being edited. + /// @param roundStart The updated timestamp for when the round starts. + /// @param roundEnd The updated timestamp for when the round ends. + /// @param roundCap The updated maximum allocation or cap for the round. + /// @param hookContract The address of an optional hook contract for custom logic. + /// @param closureMechanism A boolean indicating whether a specific closure mechanism is enabled. + /// @param globalAccumulativeCaps A boolean indicating whether global accumulative caps are enforced. + event RoundEdited( + uint indexed roundId, + uint roundStart, + uint roundEnd, + uint roundCap, + address hookContract, + bool closureMechanism, + bool globalAccumulativeCaps + ); + + // ------------------------------------------------------------------------- + // Errors + + /// @notice Amount can not be zero. + error Module__LM_PC_FundingPot__InvalidDepositAmount(); + + /// @notice Round does not exist + error Module__LM_PC_FundingPot__RoundDoesNotExist(); + + /// @notice Round start time must be in the future + error Module__LM_PC_FundingPot__RoundStartMustBeInFuture(); + + /// @notice Round must have either an end time or a funding cap + error Module__LM_PC_FundingPot__RoundMustHaveEndTimeOrCap(); + + /// @notice Round end time must be after round start time + error Module__LM_PC_FundingPot__RoundEndMustBeAfterStart(); + + /// @notice Round has already started and cannot be modified + error Module__LM_PC_FundingPot__RoundAlreadyStarted(); + + // ------------------------------------------------------------------------- + // Public - Getters + + // ------------------------------------------------------------------------- + // Public - Mutating + + /// @notice Creates a new funding round + /// @dev Only callable by funding pot admin + /// @param _roundStart Start timestamp for the round + /// @param _roundEnd End timestamp for the round (0 if using roundCap only) + /// @param _roundCap Maximum contribution cap in collateral tokens (0 if using roundEnd only) + /// @param _hookContract Address of contract to call after round closure + /// @param _hookFunction Encoded function call for the hook + /// @param _closureMechanism Whether hook closure coincides with contribution span end + /// @param _globalAccumulativeCaps Whether caps accumulate globally + /// @return The ID of the newly created round + function createRound( + uint _roundStart, + uint _roundEnd, + uint _roundCap, + address _hookContract, + bytes memory _hookFunction, + bool _closureMechanism, + bool _globalAccumulativeCaps + ) external returns (uint64); + + /// @notice Edits an existing funding round + /// @dev Only callable by funding pot admin and only before the round has started + /// @param _roundId ID of the round to edit + /// @param _roundStart New start timestamp + /// @param _roundEnd New end timestamp + /// @param _roundCap New maximum contribution cap + /// @param _hookContract New hook contract address + /// @param _hookFunction New encoded function call + /// @param _closureMechanism New closure mechanism setting + /// @param _globalAccumulativeCaps New global accumulative caps setting + /// @return True if edit was successful + function editRound( + uint64 _roundId, + uint _roundStart, + uint _roundEnd, + uint _roundCap, + address _hookContract, + bytes memory _hookFunction, + bool _closureMechanism, + bool _globalAccumulativeCaps + ) external returns (bool); } diff --git a/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol b/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol index 0024b1ae6..cac3df57e 100644 --- a/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol +++ b/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol @@ -105,6 +105,196 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { // ------------------------------------------------------------------------- // Test External (public + external) + /* Test fuzzed createRound() + ├── Given a round start time is in the future + │ ├── And the round end time is either after the start or the round has a cap + │ │ └── When a new round is created with the given parameters + │ │ └── Then the stored round details should match the provided values + */ + function testFuzz_createRound( + uint roundStart, + uint roundEnd, + uint roundCap, + address hookContract, + bytes memory hookFunction, + bool closureMechanism, + bool globalAccumulativeCaps + ) public { + vm.assume(roundStart > block.timestamp); + vm.assume(roundEnd > roundStart || roundCap > 0); + if (roundEnd > 0) { + vm.assume(roundEnd > roundStart); + } + + uint64 roundId = fundingPot.createRound( + roundStart, + roundEnd, + roundCap, + hookContract, + hookFunction, + closureMechanism, + globalAccumulativeCaps + ); + + (uint storedStart, uint storedEnd, uint storedCap,,,,, bool isActive) = + fundingPot.rounds(roundId); + + assertEq(storedStart, roundStart, "Round start mismatch"); + assertEq(storedEnd, roundEnd, "Round end mismatch"); + assertEq(storedCap, roundCap, "Round cap mismatch"); + assertTrue(isActive, "Round should be active"); + } + + /* Test createRound() with invalid start time + ├── Given a start time that is in the past + │ └── When attempting to create a round with this past start time + │ └── Then it should revert with "Round start must be in the future" + */ + function test_createRound_invalidStart() public { + uint pastTime = block.timestamp - 1; + vm.expectRevert( + ILM_PC_FundingPot_v1 + .Module__LM_PC_FundingPot__RoundStartMustBeInFuture + .selector + ); + fundingPot.createRound( + pastTime, block.timestamp + 10, 1000, address(0), "", false, false + ); + } + + /* Test createRound() with invalid end time + ├── Given a future start time + │ └── When attempting to create a round where end time is before start time and cap is zero + │ └── Then it should revert with "Round must have either end time or cap" + */ + function test_createRound_invalidEnd() public { + uint futureTime = block.timestamp + 100; + vm.expectRevert( + ILM_PC_FundingPot_v1 + .Module__LM_PC_FundingPot__RoundMustHaveEndTimeOrCap + .selector + ); + fundingPot.createRound( + futureTime, futureTime - 1, 0, address(0), "", false, false + ); + } + + /* Test fuzzed editRound() + ├── Given a round start time is in the future + │ ├── And the round end time is greater than zero and after the start time + │ │ └── When editing the round with new parameters + │ │ ├── Then the update should be successful + │ │ └── And the stored values should match the new parameters + */ + function testFuzz_editRound( + uint roundStart, + uint roundEnd, + uint roundCap, + address hookContract, + bytes memory hookFunction, + bool closureMechanism, + bool globalAccumulativeCaps + ) public { + vm.assume(roundStart > block.timestamp); + vm.assume(roundStart < type(uint).max - 100); + vm.assume(roundEnd > 0); + vm.assume(roundEnd > roundStart); + vm.assume(roundEnd <= type(uint).max - 100); + vm.assume(roundCap <= type(uint).max - 100); + + uint64 roundId = fundingPot.createRound( + roundStart, + roundEnd, + roundCap, + hookContract, + hookFunction, + closureMechanism, + globalAccumulativeCaps + ); + + uint newStart = roundStart + 100; + uint newEnd = roundEnd + 100; + uint newCap = roundCap + 100; + + bool success = fundingPot.editRound( + roundId, + newStart, + newEnd, + newCap, + hookContract, + hookFunction, + closureMechanism, + globalAccumulativeCaps + ); + + assertTrue(success, "Round edit failed"); + + (uint storedStart, uint storedEnd, uint storedCap,,,,,) = + fundingPot.rounds(roundId); + assertEq(storedStart, newStart, "Updated round start mismatch"); + assertEq(storedEnd, newEnd, "Updated round end mismatch"); + assertEq(storedCap, newCap, "Updated round cap mismatch"); + } + + /* Test editRound() on nonexistent round + ├── Given a round does not exist + │ └── When attempting to edit the round + │ └── Then it should revert with "Round does not exist" + */ + function test_editRound_nonexistent() public { + vm.expectRevert( + ILM_PC_FundingPot_v1 + .Module__LM_PC_FundingPot__RoundDoesNotExist + .selector + ); + fundingPot.editRound( + 9999, + block.timestamp + 100, + block.timestamp + 200, + 1000, + address(0), + "", + false, + false + ); + } + + /* Test editRound() after round has started + ├── Given a round has been created with a future start time + │ ├── And time has advanced beyond the start time + │ │ └── When attempting to edit the round + │ │ └── Then it should revert with "Round has already started" + */ + function test_editRound_afterStart() public { + uint64 roundId = fundingPot.createRound( + block.timestamp + 10, + block.timestamp + 100, + 1000, + address(0), + "", + false, + false + ); + + vm.warp(block.timestamp + 11); + + vm.expectRevert( + ILM_PC_FundingPot_v1 + .Module__LM_PC_FundingPot__RoundAlreadyStarted + .selector + ); + fundingPot.editRound( + roundId, + block.timestamp + 20, + block.timestamp + 200, + 2000, + address(0), + "", + false, + false + ); + } + // ------------------------------------------------------------------------- // Test: Internal Functions } From cf292e5aa2068b78a9b1407aea8b446b6a87131f Mon Sep 17 00:00:00 2001 From: Zuhaib Mohammed Date: Mon, 17 Mar 2025 20:05:07 +0530 Subject: [PATCH 016/130] feat: create and edit rounds implementation --- .../logicModule/LM_PC_FundingPot_v1.sol | 92 +++-- .../interfaces/ILM_PC_FundingPot_v1.sol | 26 +- .../logicModule/LM_PC_FundingPot_v1.t.sol | 390 ++++++++++-------- 3 files changed, 312 insertions(+), 196 deletions(-) diff --git a/src/modules/logicModule/LM_PC_FundingPot_v1.sol b/src/modules/logicModule/LM_PC_FundingPot_v1.sol index fdad4ff69..813b44b45 100644 --- a/src/modules/logicModule/LM_PC_FundingPot_v1.sol +++ b/src/modules/logicModule/LM_PC_FundingPot_v1.sol @@ -68,7 +68,7 @@ contract LM_PC_FundingPot_v1 is IERC20 internal _paymentToken; /// @notice Stores all funding rounds by their unique ID. - mapping(uint64 => Round) public rounds; + mapping(uint64 => Round) private rounds; /// @notice The next available round ID. uint64 private nextRoundId; @@ -107,6 +107,20 @@ contract LM_PC_FundingPot_v1 is // ------------------------------------------------------------------------- // Public - Getters + /// @inheritdoc ILM_PC_FundingPot_v1 + function getRoundDetails(uint64 _roundId) + external + view + returns (Round memory) + { + return rounds[_roundId]; + } + + /// @inheritdoc ILM_PC_FundingPot_v1 + function getRoundCount() external view returns (uint64) { + return nextRoundId; + } + // ------------------------------------------------------------------------- // Public - Mutating @@ -119,18 +133,14 @@ contract LM_PC_FundingPot_v1 is bytes memory _hookFunction, bool _closureMechanism, bool _globalAccumulativeCaps - ) external returns (uint64) { - if (_roundStart <= block.timestamp) { - revert Module__LM_PC_FundingPot__RoundStartMustBeInFuture(); - } + ) external onlyModuleRole(FUNDING_POT_ADMIN_ROLE) returns (uint64) { + bool _isActive; + // @note: This logic is required if start time is set to block.timestamp + // if (_roundStart == block.timestamp) { + // _isActive = true; + // } - if (_roundEnd <= _roundStart && _roundCap == 0) { - revert Module__LM_PC_FundingPot__RoundMustHaveEndTimeOrCap(); - } - - if (_roundEnd > 0 && _roundEnd <= _roundStart) { - revert Module__LM_PC_FundingPot__RoundEndMustBeAfterStart(); - } + nextRoundId++; uint64 roundId = nextRoundId; rounds[roundId] = Round({ @@ -141,10 +151,10 @@ contract LM_PC_FundingPot_v1 is hookFunction: _hookFunction, closureMechanism: _closureMechanism, globalAccumulativeCaps: _globalAccumulativeCaps, - isActive: true + isActive: _isActive }); - nextRoundId++; + _validateRoundParameters(rounds[roundId]); emit RoundCreated( roundId, @@ -169,29 +179,17 @@ contract LM_PC_FundingPot_v1 is bytes memory _hookFunction, bool _closureMechanism, bool _globalAccumulativeCaps - ) external returns (bool) { + ) external onlyModuleRole(FUNDING_POT_ADMIN_ROLE) returns (bool) { Round storage round = rounds[_roundId]; if (round.roundStart == 0) { - revert Module__LM_PC_FundingPot__RoundDoesNotExist(); + revert Module__LM_PC_FundingPot__RoundNotCreated(); } - if (block.timestamp >= round.roundStart) { + if (block.timestamp > round.roundStart || round.isActive) { revert Module__LM_PC_FundingPot__RoundAlreadyStarted(); } - if (_roundStart <= block.timestamp) { - revert Module__LM_PC_FundingPot__RoundStartMustBeInFuture(); - } - - if (_roundEnd <= _roundStart && _roundCap == 0) { - revert Module__LM_PC_FundingPot__RoundMustHaveEndTimeOrCap(); - } - - if (_roundEnd > 0 && _roundEnd <= _roundStart) { - revert Module__LM_PC_FundingPot__RoundEndMustBeAfterStart(); - } - round.roundStart = _roundStart; round.roundEnd = _roundEnd; round.roundCap = _roundCap; @@ -200,6 +198,8 @@ contract LM_PC_FundingPot_v1 is round.closureMechanism = _closureMechanism; round.globalAccumulativeCaps = _globalAccumulativeCaps; + _validateRoundParameters(round); + emit RoundEdited( _roundId, _roundStart, @@ -214,4 +214,38 @@ contract LM_PC_FundingPot_v1 is } // ------------------------------------------------------------------------- // Internal + + /// @notice Validates the round parameters. + /// @param round The round to validate. + /// @dev Reverts if the round parameters are invalid. + function _validateRoundParameters(Round memory round) internal view { + // Validate round start time is in the future + // @note: The below condition wont allow _roundStart == block.timestamp + // @note: If we allow if then _isActive should be set to true + if (round.roundStart <= block.timestamp) { + revert Module__LM_PC_FundingPot__RoundStartMustBeInFuture(); + } + + // Validate that either end time or cap is set + if (round.roundEnd == 0 && round.roundCap == 0) { + revert Module__LM_PC_FundingPot__RoundMustHaveEndTimeOrCap(); + } + + // If end time is set, validate it's after start time + if (round.roundEnd > 0 && round.roundEnd <= round.roundStart) { + revert Module__LM_PC_FundingPot__RoundEndMustBeAfterStart(); + } + + // Validate hook contract and function consistency + if (round.hookContract != address(0) && round.hookFunction.length == 0) + { + revert + Module__LM_PC_FundingPot__HookFunctionRequiredWithHookContract(); + } + + if (round.hookContract == address(0) && round.hookFunction.length > 0) { + revert + Module__LM_PC_FundingPot__HookContractRequiredWithHookFunction(); + } + } } diff --git a/src/modules/logicModule/interfaces/ILM_PC_FundingPot_v1.sol b/src/modules/logicModule/interfaces/ILM_PC_FundingPot_v1.sol index 5d24a8bfb..bd8ee5b48 100644 --- a/src/modules/logicModule/interfaces/ILM_PC_FundingPot_v1.sol +++ b/src/modules/logicModule/interfaces/ILM_PC_FundingPot_v1.sol @@ -26,7 +26,7 @@ interface ILM_PC_FundingPot_v1 is IERC20PaymentClientBase_v2 { bytes hookFunction; bool closureMechanism; bool globalAccumulativeCaps; - bool isActive; + bool isActive; //@note: do we need this? } // ------------------------------------------------------------------------- @@ -91,9 +91,33 @@ interface ILM_PC_FundingPot_v1 is IERC20PaymentClientBase_v2 { /// @notice Round has already started and cannot be modified error Module__LM_PC_FundingPot__RoundAlreadyStarted(); + /// @notice Hook function is required when a hook contract is provided + error Module__LM_PC_FundingPot__HookFunctionRequiredWithContract(); + + /// @notice Thrown when a hook contract is specified without a hook function. + error Module__LM_PC_FundingPot__HookFunctionRequiredWithHookContract(); + + /// @notice Thrown when a hook function is specified without a hook contract. + error Module__LM_PC_FundingPot__HookContractRequiredWithHookFunction(); + + /// @notice Round does not exist + error Module__LM_PC_FundingPot__RoundNotCreated(); + // ------------------------------------------------------------------------- // Public - Getters + /// @notice Retrieves the details of a specific funding round. + /// @param _roundId The unique identifier of the round to retrieve. + /// @return A struct containing the round's details. + function getRoundDetails(uint64 _roundId) + external + view + returns (Round memory); + + /// @notice Retrieves the total number of funding rounds. + /// @return The total number of funding rounds. + function getRoundCount() external view returns (uint64); + // ------------------------------------------------------------------------- // Public - Mutating diff --git a/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol b/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol index cac3df57e..bcec5ba28 100644 --- a/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol +++ b/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol @@ -52,7 +52,6 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { // SuT LM_PC_FundingPot_v1_Exposed fundingPot; - address public fundingPotAdmin = makeAddr("FundingPotAdmin"); // ------------------------------------------------------------------------- // Setup @@ -70,8 +69,12 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { // Give test contract the DEPOSIT_ADMIN_ROLE. fundingPot.grantModuleRole( - fundingPot.FUNDING_POT_ADMIN_ROLE(), fundingPotAdmin + fundingPot.FUNDING_POT_ADMIN_ROLE(), address(this) ); + _authorizer.setIsAuthorized(address(this), true); + + // Set the block timestamp + vm.warp(block.timestamp + _orchestrator.MODULE_UPDATE_TIMELOCK()); } // ------------------------------------------------------------------------- @@ -95,206 +98,261 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { fundingPot.init(_orchestrator, _METADATA, abi.encode("")); } - function test_fundingPotAdminRoleGranted() public { - bytes32 roleId = _orchestrator.authorizer().generateRoleId( - address(fundingPot), fundingPot.FUNDING_POT_ADMIN_ROLE() - ); - assertTrue(_orchestrator.authorizer().hasRole(roleId, fundingPotAdmin)); - } - // ------------------------------------------------------------------------- // Test External (public + external) - /* Test fuzzed createRound() - ├── Given a round start time is in the future - │ ├── And the round end time is either after the start or the round has a cap - │ │ └── When a new round is created with the given parameters - │ │ └── Then the stored round details should match the provided values + /* Test createRound() + ├── Given user does not have FUNDING_POT_ADMIN_ROLE + │ └── When user attempts to create a round + │ └── Then it should revert + ├── Given round start time is in the past + │ └── When user attempts to create a round + │ └── Then it should revert + ├── Given round end time is 0 and round cap is 0 + │ └── When user attempts to create a round + │ └── Then it should revert + ├── Given round end time is set and round end time is in the past + │ └── When user attempts to create a round + │ └── Then it should revert + ├── Given hook contract is set but hook function is not set + │ └── When user attempts to create a round + │ └── Then it should revert + ├── Given hook function is set but hook contract is not set + │ └── When user attempts to create a round + │ └── Then it should revert */ - function testFuzz_createRound( - uint roundStart, - uint roundEnd, - uint roundCap, - address hookContract, - bytes memory hookFunction, - bool closureMechanism, - bool globalAccumulativeCaps + + function testFuzzCreateRound_revertsGivenUserIsNotFundingPotAdmin( + address user_ ) public { - vm.assume(roundStart > block.timestamp); - vm.assume(roundEnd > roundStart || roundCap > 0); - if (roundEnd > 0) { - vm.assume(roundEnd > roundStart); - } - - uint64 roundId = fundingPot.createRound( - roundStart, - roundEnd, - roundCap, - hookContract, - hookFunction, - closureMechanism, - globalAccumulativeCaps + vm.assume(user_ != address(0) && user_ != address(this)); + vm.startPrank(user_); + bytes32 roleId = _authorizer.generateRoleId( + address(fundingPot), fundingPot.FUNDING_POT_ADMIN_ROLE() ); - - (uint storedStart, uint storedEnd, uint storedCap,,,,, bool isActive) = - fundingPot.rounds(roundId); - - assertEq(storedStart, roundStart, "Round start mismatch"); - assertEq(storedEnd, roundEnd, "Round end mismatch"); - assertEq(storedCap, roundCap, "Round cap mismatch"); - assertTrue(isActive, "Round should be active"); + vm.expectRevert( + abi.encodeWithSelector( + IModule_v1.Module__CallerNotAuthorized.selector, roleId, user_ + ) + ); + ILM_PC_FundingPot_v1.Round memory round = _createDefaultFundingRound(); + _callCreateRound(round); + vm.stopPrank(); } - /* Test createRound() with invalid start time - ├── Given a start time that is in the past - │ └── When attempting to create a round with this past start time - │ └── Then it should revert with "Round start must be in the future" - */ - function test_createRound_invalidStart() public { - uint pastTime = block.timestamp - 1; + function testFuzzCreateRound_revertsGivenRoundStartIsInThePast( + uint roundStart_ + ) public { + vm.assume(roundStart_ < block.timestamp); + ILM_PC_FundingPot_v1.Round memory round = _createDefaultFundingRound(); + round.roundStart = roundStart_; vm.expectRevert( - ILM_PC_FundingPot_v1 - .Module__LM_PC_FundingPot__RoundStartMustBeInFuture - .selector - ); - fundingPot.createRound( - pastTime, block.timestamp + 10, 1000, address(0), "", false, false + abi.encodeWithSelector( + ILM_PC_FundingPot_v1 + .Module__LM_PC_FundingPot__RoundStartMustBeInFuture + .selector + ) ); + _callCreateRound(round); } - /* Test createRound() with invalid end time - ├── Given a future start time - │ └── When attempting to create a round where end time is before start time and cap is zero - │ └── Then it should revert with "Round must have either end time or cap" - */ - function test_createRound_invalidEnd() public { - uint futureTime = block.timestamp + 100; + function testFuzzCreateRound_revertsGivenRoundEndTimeAndCapAreBothZero() + public + { + ILM_PC_FundingPot_v1.Round memory round = _createDefaultFundingRound(); + round.roundEnd = 0; + round.roundCap = 0; vm.expectRevert( - ILM_PC_FundingPot_v1 - .Module__LM_PC_FundingPot__RoundMustHaveEndTimeOrCap - .selector - ); - fundingPot.createRound( - futureTime, futureTime - 1, 0, address(0), "", false, false + abi.encodeWithSelector( + ILM_PC_FundingPot_v1 + .Module__LM_PC_FundingPot__RoundMustHaveEndTimeOrCap + .selector + ) ); + _callCreateRound(round); } - /* Test fuzzed editRound() - ├── Given a round start time is in the future - │ ├── And the round end time is greater than zero and after the start time - │ │ └── When editing the round with new parameters - │ │ ├── Then the update should be successful - │ │ └── And the stored values should match the new parameters - */ - function testFuzz_editRound( - uint roundStart, - uint roundEnd, - uint roundCap, - address hookContract, - bytes memory hookFunction, - bool closureMechanism, - bool globalAccumulativeCaps + function testFuzzCreateRound_revertsGivenRoundEndTimeIsBeforeRoundStart( + uint roundEnd_ ) public { - vm.assume(roundStart > block.timestamp); - vm.assume(roundStart < type(uint).max - 100); - vm.assume(roundEnd > 0); - vm.assume(roundEnd > roundStart); - vm.assume(roundEnd <= type(uint).max - 100); - vm.assume(roundCap <= type(uint).max - 100); - - uint64 roundId = fundingPot.createRound( - roundStart, - roundEnd, - roundCap, - hookContract, - hookFunction, - closureMechanism, - globalAccumulativeCaps + ILM_PC_FundingPot_v1.Round memory round = _createDefaultFundingRound(); + vm.assume(roundEnd_ != 0 && roundEnd_ < round.roundStart); + round.roundEnd = roundEnd_; + vm.expectRevert( + abi.encodeWithSelector( + ILM_PC_FundingPot_v1 + .Module__LM_PC_FundingPot__RoundEndMustBeAfterStart + .selector + ) ); + _callCreateRound(round); + } - uint newStart = roundStart + 100; - uint newEnd = roundEnd + 100; - uint newCap = roundCap + 100; + function testFuzzCreateRound_revertsGivenHookContractIsSetButHookFunctionIsEmpty( + ) public { + ILM_PC_FundingPot_v1.Round memory round = _createDefaultFundingRound(); + round.hookContract = address(1); + round.hookFunction = bytes(""); + vm.expectRevert( + abi.encodeWithSelector( + ILM_PC_FundingPot_v1 + .Module__LM_PC_FundingPot__HookFunctionRequiredWithHookContract + .selector + ) + ); + _callCreateRound(round); + } - bool success = fundingPot.editRound( - roundId, - newStart, - newEnd, - newCap, - hookContract, - hookFunction, - closureMechanism, - globalAccumulativeCaps + function testFuzzCreateRound_revertsGivenHookFunctionIsSetButHookContractIsEmpty( + ) public { + ILM_PC_FundingPot_v1.Round memory round = _createDefaultFundingRound(); + round.hookContract = address(0); + round.hookFunction = bytes("test"); + vm.expectRevert( + abi.encodeWithSelector( + ILM_PC_FundingPot_v1 + .Module__LM_PC_FundingPot__HookContractRequiredWithHookFunction + .selector + ) ); + _callCreateRound(round); + } - assertTrue(success, "Round edit failed"); + /* Test createRound() + ├── Given all the valid parameters are provided + │ └── When user attempts to create a round + │ └── Then it should not be active and should return the round id + */ - (uint storedStart, uint storedEnd, uint storedCap,,,,,) = - fundingPot.rounds(roundId); - assertEq(storedStart, newStart, "Updated round start mismatch"); - assertEq(storedEnd, newEnd, "Updated round end mismatch"); - assertEq(storedCap, newCap, "Updated round cap mismatch"); + function testFuzzCreateRound() public { + ILM_PC_FundingPot_v1.Round memory round = _createDefaultFundingRound(); + _callCreateRound(round); + + uint64 lastRoundId = fundingPot.getRoundCount(); + ILM_PC_FundingPot_v1.Round memory lastRound = + fundingPot.getRoundDetails(lastRoundId); + + assertEq(lastRound.isActive, false); + assertEq(lastRound.roundStart, round.roundStart); + assertEq(lastRound.roundEnd, round.roundEnd); + assertEq(lastRound.roundCap, round.roundCap); + assertEq(lastRound.hookContract, round.hookContract); + assertEq(lastRound.hookFunction, round.hookFunction); + assertEq(lastRound.closureMechanism, round.closureMechanism); + assertEq(lastRound.globalAccumulativeCaps, round.globalAccumulativeCaps); } - /* Test editRound() on nonexistent round - ├── Given a round does not exist - │ └── When attempting to edit the round - │ └── Then it should revert with "Round does not exist" + /* Test editRound() + ├── Given user does not have FUNDING_POT_ADMIN_ROLE + │ └── When user attempts to create a round + │ └── Then it should revert + ├── Given round does not exist + │ └── When user attempts to edit the round + │ └── Then it should revert + ├── Given round is active + │ └── When user attempts to edit the round + │ └── Then it should revert + ├── Given round start time is in the past + │ └── When user attempts to create a round + │ └── Then it should revert + ├── Given round end time is 0 and round cap is 0 + │ └── When user attempts to create a round + │ └── Then it should revert + ├── Given round end time is set and round end time is in the past + │ └── When user attempts to create a round + │ └── Then it should revert + ├── Given hook contract is set but hook function is not set + │ └── When user attempts to create a round + │ └── Then it should revert + ├── Given hook function is set but hook contract is not set + │ └── When user attempts to create a round + │ └── Then it should revert */ - function test_editRound_nonexistent() public { - vm.expectRevert( - ILM_PC_FundingPot_v1 - .Module__LM_PC_FundingPot__RoundDoesNotExist - .selector + + function testFuzzEditRound_revertsGivenUserIsNotFundingPotAdmin( + address user_ + ) public { + testFuzzCreateRound(); + vm.startPrank(user_); + bytes32 roleId = _authorizer.generateRoleId( + address(fundingPot), fundingPot.FUNDING_POT_ADMIN_ROLE() ); - fundingPot.editRound( - 9999, - block.timestamp + 100, - block.timestamp + 200, - 1000, - address(0), - "", - false, - false + vm.expectRevert( + abi.encodeWithSelector( + IModule_v1.Module__CallerNotAuthorized.selector, roleId, user_ + ) ); + ILM_PC_FundingPot_v1.Round memory round = _createDefaultFundingRound(); + _callEditRound(0, round); + vm.stopPrank(); } - /* Test editRound() after round has started - ├── Given a round has been created with a future start time - │ ├── And time has advanced beyond the start time - │ │ └── When attempting to edit the round - │ │ └── Then it should revert with "Round has already started" - */ - function test_editRound_afterStart() public { - uint64 roundId = fundingPot.createRound( - block.timestamp + 10, - block.timestamp + 100, - 1000, - address(0), - "", - false, - false + function testFuzzEditRound_revertsGivenRoundIsNotCreated() public { + testFuzzCreateRound(); + ILM_PC_FundingPot_v1.Round memory round = _createDefaultFundingRound(); + + uint64 roundId = fundingPot.getRoundCount(); + vm.expectRevert( + abi.encodeWithSelector( + ILM_PC_FundingPot_v1 + .Module__LM_PC_FundingPot__RoundNotCreated + .selector + ) ); + _callEditRound(roundId + 1, round); + } + + // ------------------------------------------------------------------------- + // Test: Internal Functions - vm.warp(block.timestamp + 11); + // Helper Functions + + // @notice Creates a default funding round + // @dev make the parameters fuzzable @TODO Jeffrey + function _createDefaultFundingRound() + internal + returns (ILM_PC_FundingPot_v1.Round memory) + { + ILM_PC_FundingPot_v1.Round memory round = ILM_PC_FundingPot_v1.Round({ + roundStart: block.timestamp + 1 days, + roundEnd: block.timestamp + 2 days, + roundCap: 1000, + hookContract: address(0), + hookFunction: bytes(""), + closureMechanism: false, + globalAccumulativeCaps: false, + isActive: false + }); + return round; + } - vm.expectRevert( - ILM_PC_FundingPot_v1 - .Module__LM_PC_FundingPot__RoundAlreadyStarted - .selector + function _callCreateRound(ILM_PC_FundingPot_v1.Round memory round) + internal + { + fundingPot.createRound( + round.roundStart, + round.roundEnd, + round.roundCap, + round.hookContract, + round.hookFunction, + round.closureMechanism, + round.globalAccumulativeCaps ); + } + + function _callEditRound( + uint64 roundId, + ILM_PC_FundingPot_v1.Round memory round + ) internal { fundingPot.editRound( roundId, - block.timestamp + 20, - block.timestamp + 200, - 2000, - address(0), - "", - false, - false + round.roundStart, + round.roundEnd, + round.roundCap, + round.hookContract, + round.hookFunction, + round.closureMechanism, + round.globalAccumulativeCaps ); } - - // ------------------------------------------------------------------------- - // Test: Internal Functions } From ce9b04a2934f15a1f7de46f19deb84fe21793a90 Mon Sep 17 00:00:00 2001 From: JeffreyJoel Date: Tue, 18 Mar 2025 11:30:21 +0100 Subject: [PATCH 017/130] test: add tests for the edit rounds functionality --- .../logicModule/LM_PC_FundingPot_v1.t.sol | 271 ++++++++++++++++-- 1 file changed, 251 insertions(+), 20 deletions(-) diff --git a/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol b/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol index bcec5ba28..d3af17b45 100644 --- a/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol +++ b/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol @@ -253,19 +253,19 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { │ └── When user attempts to edit the round │ └── Then it should revert ├── Given round start time is in the past - │ └── When user attempts to create a round + │ └── When user attempts to edit a round with the above parameter │ └── Then it should revert ├── Given round end time is 0 and round cap is 0 - │ └── When user attempts to create a round + │ └── When user attempts edit a round with the above parameters │ └── Then it should revert ├── Given round end time is set and round end time is in the past - │ └── When user attempts to create a round + │ └── When user attempts to edit a round with the above parameters │ └── Then it should revert ├── Given hook contract is set but hook function is not set - │ └── When user attempts to create a round + │ └── When user attempts to edit a round with the above parameters │ └── Then it should revert ├── Given hook function is set but hook contract is not set - │ └── When user attempts to create a round + │ └── When user attempts to edit a round with the above parameters │ └── Then it should revert */ @@ -282,14 +282,17 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { IModule_v1.Module__CallerNotAuthorized.selector, roleId, user_ ) ); - ILM_PC_FundingPot_v1.Round memory round = _createDefaultFundingRound(); - _callEditRound(0, round); + ILM_PC_FundingPot_v1.Round memory editedRound = + _createEditedRoundParams(); + + _callEditRound(0, editedRound); vm.stopPrank(); } function testFuzzEditRound_revertsGivenRoundIsNotCreated() public { testFuzzCreateRound(); - ILM_PC_FundingPot_v1.Round memory round = _createDefaultFundingRound(); + ILM_PC_FundingPot_v1.Round memory editedRound = + _createEditedRoundParams(); uint64 roundId = fundingPot.getRoundCount(); vm.expectRevert( @@ -299,7 +302,196 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { .selector ) ); - _callEditRound(roundId + 1, round); + _callEditRound(roundId + 1, editedRound); + } + + function testFuzzEditRound_revertsGivenRoundIsActive(uint roundStart_) + public + { + testFuzzCreateRound(); + uint64 roundId = fundingPot.getRoundCount(); + + ILM_PC_FundingPot_v1.Round memory roundDetails = + fundingPot.getRoundDetails(roundId); + + vm.warp(roundDetails.roundStart + 1); + + ILM_PC_FundingPot_v1.Round memory editedRound = + _createEditedRoundParams(); + vm.expectRevert( + abi.encodeWithSelector( + ILM_PC_FundingPot_v1 + .Module__LM_PC_FundingPot__RoundAlreadyStarted + .selector + ) + ); + _callEditRound(roundId, editedRound); + } + + function testFuzzEditRound_revertsGivenRoundStartIsInThePast( + uint roundStart_ + ) public { + testFuzzCreateRound(); + uint64 roundId = fundingPot.getRoundCount(); + + vm.assume(roundStart_ < block.timestamp); + ILM_PC_FundingPot_v1.Round memory editedRound = + _createEditedRoundParams(); + editedRound.roundStart = roundStart_; + + vm.expectRevert( + abi.encodeWithSelector( + ILM_PC_FundingPot_v1 + .Module__LM_PC_FundingPot__RoundStartMustBeInFuture + .selector + ) + ); + + _callEditRound(roundId, editedRound); + } + + function testFuzzEditRound_revertsGivenRoundEndTimeAndCapAreBothZero() + public + { + testFuzzCreateRound(); + uint64 roundId = fundingPot.getRoundCount(); + + ILM_PC_FundingPot_v1.Round memory editedRound = + _createEditedRoundParams(); + editedRound.roundEnd = 0; + editedRound.roundCap = 0; + + vm.expectRevert( + abi.encodeWithSelector( + ILM_PC_FundingPot_v1 + .Module__LM_PC_FundingPot__RoundMustHaveEndTimeOrCap + .selector + ) + ); + + _callEditRound(roundId, editedRound); + } + + function testFuzzEditRound_revertsGivenRoundEndTimeIsBeforeRoundStart( + uint roundEnd_ + ) public { + testFuzzCreateRound(); + uint64 roundId = fundingPot.getRoundCount(); + + ILM_PC_FundingPot_v1.Round memory editedRound = + _createEditedRoundParams(); + vm.assume(roundEnd_ != 0 && roundEnd_ < editedRound.roundStart); + editedRound.roundEnd = roundEnd_; + + vm.expectRevert( + abi.encodeWithSelector( + ILM_PC_FundingPot_v1 + .Module__LM_PC_FundingPot__RoundEndMustBeAfterStart + .selector + ) + ); + + _callEditRound(roundId, editedRound); + } + + function testFuzzEditRound_revertsGivenHookContractIsSetButHookFunctionIsEmpty( + ) public { + testFuzzCreateRound(); + uint64 roundId = fundingPot.getRoundCount(); + + ILM_PC_FundingPot_v1.Round memory editedRound = + _createEditedRoundParams(); + editedRound.hookContract = address(1); + editedRound.hookFunction = bytes(""); + + vm.expectRevert( + abi.encodeWithSelector( + ILM_PC_FundingPot_v1 + .Module__LM_PC_FundingPot__HookFunctionRequiredWithHookContract + .selector + ) + ); + + _callEditRound(roundId, editedRound); + } + + function testFuzzEditRound_revertsGivenHookFunctionIsSetButHookContractIsEmpty( + ) public { + testFuzzCreateRound(); + uint64 roundId = fundingPot.getRoundCount(); + + ILM_PC_FundingPot_v1.Round memory editedRound = + _createEditedRoundParams(); + editedRound.hookContract = address(0); + editedRound.hookFunction = bytes("test"); + + vm.expectRevert( + abi.encodeWithSelector( + ILM_PC_FundingPot_v1 + .Module__LM_PC_FundingPot__HookContractRequiredWithHookFunction + .selector + ) + ); + + _callEditRound(roundId, editedRound); + } + + /* Test editRound() + ├── Given a round has been created and is not active + │ └── When an admin provides valid parameters to edit the round + │ └── Then all the round details should be successfully updated + │ ├── roundStart should be updated to the new value + │ ├── roundEnd should be updated to the new value + │ ├── roundCap should be updated to the new value + │ ├── hookContract should be updated to the new value + │ ├── hookFunction should be updated to the new value + │ ├── closureMechanism should be updated to the new value + │ └── globalAccumulativeCaps should be updated to the new value + */ + + function testFuzzEditRound() public { + testFuzzCreateRound(); + uint64 lastRoundId = fundingPot.getRoundCount(); + + ILM_PC_FundingPot_v1.Round memory editedRound = + _createEditedRoundParams(); + + _callEditRound(lastRoundId, editedRound); + + ILM_PC_FundingPot_v1.Round memory updatedRound = + fundingPot.getRoundDetails(lastRoundId); + + assertEq( + updatedRound.roundStart, + editedRound.roundStart, + "roundStart not updated" + ); + assertEq( + updatedRound.roundEnd, editedRound.roundEnd, "roundEnd not updated" + ); + assertEq( + updatedRound.roundCap, editedRound.roundCap, "roundCap not updated" + ); + assertEq( + updatedRound.hookContract, + editedRound.hookContract, + "hookContract not updated" + ); + assertEq( + keccak256(updatedRound.hookFunction), + keccak256(editedRound.hookFunction), + "hookFunction not updated" + ); + assertEq( + updatedRound.closureMechanism, + editedRound.closureMechanism, + "closureMechanism not updated" + ); + assertEq( + updatedRound.globalAccumulativeCaps, + editedRound.globalAccumulativeCaps, + "globalAccumulativeCaps not updated" + ); } // ------------------------------------------------------------------------- @@ -309,23 +501,45 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { // @notice Creates a default funding round // @dev make the parameters fuzzable @TODO Jeffrey - function _createDefaultFundingRound() - internal - returns (ILM_PC_FundingPot_v1.Round memory) - { + function _generateFundingRoundParams( + uint roundStart_, + uint roundEnd_, + uint roundCap_, + address hookContract_, + bytes memory hookFunction_, + bool closureMechanism_, + bool globalAccumulativeCaps_ + ) internal returns (ILM_PC_FundingPot_v1.Round memory) { ILM_PC_FundingPot_v1.Round memory round = ILM_PC_FundingPot_v1.Round({ - roundStart: block.timestamp + 1 days, - roundEnd: block.timestamp + 2 days, - roundCap: 1000, - hookContract: address(0), - hookFunction: bytes(""), - closureMechanism: false, - globalAccumulativeCaps: false, + roundStart: roundStart_, + roundEnd: roundEnd_, + roundCap: roundCap_, + hookContract: hookContract_, + hookFunction: hookFunction_, + closureMechanism: closureMechanism_, + globalAccumulativeCaps: globalAccumulativeCaps_, isActive: false }); return round; } + // @notice Creates a default funding round + function _createDefaultFundingRound() + internal + returns (ILM_PC_FundingPot_v1.Round memory) + { + return _generateFundingRoundParams( + block.timestamp + 1 days, + block.timestamp + 2 days, + 1000, + address(0), + bytes(""), + false, + false + ); + } + + // @notice calls the create round function function _callCreateRound(ILM_PC_FundingPot_v1.Round memory round) internal { @@ -340,6 +554,23 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { ); } + // @notice Creates a predefined funding round with edited parameters for testing + function _createEditedRoundParams() + internal + returns (ILM_PC_FundingPot_v1.Round memory) + { + return _generateFundingRoundParams( + block.timestamp + 150, + block.timestamp + 250, + 20, + address(0x1), + hex"abcd", + true, + true + ); + } + + // @notice calls the create round function function _callEditRound( uint64 roundId, ILM_PC_FundingPot_v1.Round memory round From a5b4ce7951b272ed1a0b82c702a717ce170f2931 Mon Sep 17 00:00:00 2001 From: Zuhaib Mohammed Date: Tue, 18 Mar 2025 18:05:20 +0530 Subject: [PATCH 018/130] fix: follow inverter standard and remove redundant code --- .../logicModule/LM_PC_FundingPot_v1.sol | 12 +-- .../interfaces/ILM_PC_FundingPot_v1.sol | 2 - .../logicModule/LM_PC_FundingPot_v1.t.sol | 85 ++++++++++--------- 3 files changed, 48 insertions(+), 51 deletions(-) diff --git a/src/modules/logicModule/LM_PC_FundingPot_v1.sol b/src/modules/logicModule/LM_PC_FundingPot_v1.sol index 813b44b45..7cf13c810 100644 --- a/src/modules/logicModule/LM_PC_FundingPot_v1.sol +++ b/src/modules/logicModule/LM_PC_FundingPot_v1.sol @@ -134,12 +134,6 @@ contract LM_PC_FundingPot_v1 is bool _closureMechanism, bool _globalAccumulativeCaps ) external onlyModuleRole(FUNDING_POT_ADMIN_ROLE) returns (uint64) { - bool _isActive; - // @note: This logic is required if start time is set to block.timestamp - // if (_roundStart == block.timestamp) { - // _isActive = true; - // } - nextRoundId++; uint64 roundId = nextRoundId; @@ -150,8 +144,7 @@ contract LM_PC_FundingPot_v1 is hookContract: _hookContract, hookFunction: _hookFunction, closureMechanism: _closureMechanism, - globalAccumulativeCaps: _globalAccumulativeCaps, - isActive: _isActive + globalAccumulativeCaps: _globalAccumulativeCaps }); _validateRoundParameters(rounds[roundId]); @@ -186,7 +179,7 @@ contract LM_PC_FundingPot_v1 is revert Module__LM_PC_FundingPot__RoundNotCreated(); } - if (block.timestamp > round.roundStart || round.isActive) { + if (block.timestamp > round.roundStart) { revert Module__LM_PC_FundingPot__RoundAlreadyStarted(); } @@ -221,7 +214,6 @@ contract LM_PC_FundingPot_v1 is function _validateRoundParameters(Round memory round) internal view { // Validate round start time is in the future // @note: The below condition wont allow _roundStart == block.timestamp - // @note: If we allow if then _isActive should be set to true if (round.roundStart <= block.timestamp) { revert Module__LM_PC_FundingPot__RoundStartMustBeInFuture(); } diff --git a/src/modules/logicModule/interfaces/ILM_PC_FundingPot_v1.sol b/src/modules/logicModule/interfaces/ILM_PC_FundingPot_v1.sol index bd8ee5b48..b2ea1ff16 100644 --- a/src/modules/logicModule/interfaces/ILM_PC_FundingPot_v1.sol +++ b/src/modules/logicModule/interfaces/ILM_PC_FundingPot_v1.sol @@ -17,7 +17,6 @@ interface ILM_PC_FundingPot_v1 is IERC20PaymentClientBase_v2 { /// @param hookFunction Encoded function call to be executed on the `hookContract` after round closure. /// @param closureMechanism Indicates whether the hook closure coincides with the contribution span end. /// @param globalAccumulativeCaps Indicates whether contribution caps accumulate globally across rounds. - /// @param isActive Indicates whether the round is currently active. struct Round { uint roundStart; uint roundEnd; @@ -26,7 +25,6 @@ interface ILM_PC_FundingPot_v1 is IERC20PaymentClientBase_v2 { bytes hookFunction; bool closureMechanism; bool globalAccumulativeCaps; - bool isActive; //@note: do we need this? } // ------------------------------------------------------------------------- diff --git a/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol b/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol index d3af17b45..4d3f7644a 100644 --- a/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol +++ b/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol @@ -135,8 +135,9 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { IModule_v1.Module__CallerNotAuthorized.selector, roleId, user_ ) ); - ILM_PC_FundingPot_v1.Round memory round = _createDefaultFundingRound(); - _callCreateRound(round); + ILM_PC_FundingPot_v1.Round memory round = + _helper_createDefaultFundingRound(); + _helper_callCreateRound(round); vm.stopPrank(); } @@ -144,7 +145,8 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { uint roundStart_ ) public { vm.assume(roundStart_ < block.timestamp); - ILM_PC_FundingPot_v1.Round memory round = _createDefaultFundingRound(); + ILM_PC_FundingPot_v1.Round memory round = + _helper_createDefaultFundingRound(); round.roundStart = roundStart_; vm.expectRevert( abi.encodeWithSelector( @@ -153,13 +155,14 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { .selector ) ); - _callCreateRound(round); + _helper_callCreateRound(round); } function testFuzzCreateRound_revertsGivenRoundEndTimeAndCapAreBothZero() public { - ILM_PC_FundingPot_v1.Round memory round = _createDefaultFundingRound(); + ILM_PC_FundingPot_v1.Round memory round = + _helper_createDefaultFundingRound(); round.roundEnd = 0; round.roundCap = 0; vm.expectRevert( @@ -169,13 +172,14 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { .selector ) ); - _callCreateRound(round); + _helper_callCreateRound(round); } function testFuzzCreateRound_revertsGivenRoundEndTimeIsBeforeRoundStart( uint roundEnd_ ) public { - ILM_PC_FundingPot_v1.Round memory round = _createDefaultFundingRound(); + ILM_PC_FundingPot_v1.Round memory round = + _helper_createDefaultFundingRound(); vm.assume(roundEnd_ != 0 && roundEnd_ < round.roundStart); round.roundEnd = roundEnd_; vm.expectRevert( @@ -185,12 +189,13 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { .selector ) ); - _callCreateRound(round); + _helper_callCreateRound(round); } function testFuzzCreateRound_revertsGivenHookContractIsSetButHookFunctionIsEmpty( ) public { - ILM_PC_FundingPot_v1.Round memory round = _createDefaultFundingRound(); + ILM_PC_FundingPot_v1.Round memory round = + _helper_createDefaultFundingRound(); round.hookContract = address(1); round.hookFunction = bytes(""); vm.expectRevert( @@ -200,12 +205,13 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { .selector ) ); - _callCreateRound(round); + _helper_callCreateRound(round); } function testFuzzCreateRound_revertsGivenHookFunctionIsSetButHookContractIsEmpty( ) public { - ILM_PC_FundingPot_v1.Round memory round = _createDefaultFundingRound(); + ILM_PC_FundingPot_v1.Round memory round = + _helper_createDefaultFundingRound(); round.hookContract = address(0); round.hookFunction = bytes("test"); vm.expectRevert( @@ -215,7 +221,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { .selector ) ); - _callCreateRound(round); + _helper_callCreateRound(round); } /* Test createRound() @@ -225,14 +231,14 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { */ function testFuzzCreateRound() public { - ILM_PC_FundingPot_v1.Round memory round = _createDefaultFundingRound(); - _callCreateRound(round); + ILM_PC_FundingPot_v1.Round memory round = + _helper_createDefaultFundingRound(); + _helper_callCreateRound(round); uint64 lastRoundId = fundingPot.getRoundCount(); ILM_PC_FundingPot_v1.Round memory lastRound = fundingPot.getRoundDetails(lastRoundId); - assertEq(lastRound.isActive, false); assertEq(lastRound.roundStart, round.roundStart); assertEq(lastRound.roundEnd, round.roundEnd); assertEq(lastRound.roundCap, round.roundCap); @@ -283,16 +289,16 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { ) ); ILM_PC_FundingPot_v1.Round memory editedRound = - _createEditedRoundParams(); + _helper_createEditedRoundParams(); - _callEditRound(0, editedRound); + _helper_callEditRound(0, editedRound); vm.stopPrank(); } function testFuzzEditRound_revertsGivenRoundIsNotCreated() public { testFuzzCreateRound(); ILM_PC_FundingPot_v1.Round memory editedRound = - _createEditedRoundParams(); + _helper_createEditedRoundParams(); uint64 roundId = fundingPot.getRoundCount(); vm.expectRevert( @@ -302,7 +308,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { .selector ) ); - _callEditRound(roundId + 1, editedRound); + _helper_callEditRound(roundId + 1, editedRound); } function testFuzzEditRound_revertsGivenRoundIsActive(uint roundStart_) @@ -317,7 +323,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { vm.warp(roundDetails.roundStart + 1); ILM_PC_FundingPot_v1.Round memory editedRound = - _createEditedRoundParams(); + _helper_createEditedRoundParams(); vm.expectRevert( abi.encodeWithSelector( ILM_PC_FundingPot_v1 @@ -325,7 +331,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { .selector ) ); - _callEditRound(roundId, editedRound); + _helper_callEditRound(roundId, editedRound); } function testFuzzEditRound_revertsGivenRoundStartIsInThePast( @@ -336,7 +342,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { vm.assume(roundStart_ < block.timestamp); ILM_PC_FundingPot_v1.Round memory editedRound = - _createEditedRoundParams(); + _helper_createEditedRoundParams(); editedRound.roundStart = roundStart_; vm.expectRevert( @@ -347,7 +353,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { ) ); - _callEditRound(roundId, editedRound); + _helper_callEditRound(roundId, editedRound); } function testFuzzEditRound_revertsGivenRoundEndTimeAndCapAreBothZero() @@ -357,7 +363,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { uint64 roundId = fundingPot.getRoundCount(); ILM_PC_FundingPot_v1.Round memory editedRound = - _createEditedRoundParams(); + _helper_createEditedRoundParams(); editedRound.roundEnd = 0; editedRound.roundCap = 0; @@ -369,7 +375,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { ) ); - _callEditRound(roundId, editedRound); + _helper_callEditRound(roundId, editedRound); } function testFuzzEditRound_revertsGivenRoundEndTimeIsBeforeRoundStart( @@ -379,7 +385,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { uint64 roundId = fundingPot.getRoundCount(); ILM_PC_FundingPot_v1.Round memory editedRound = - _createEditedRoundParams(); + _helper_createEditedRoundParams(); vm.assume(roundEnd_ != 0 && roundEnd_ < editedRound.roundStart); editedRound.roundEnd = roundEnd_; @@ -391,7 +397,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { ) ); - _callEditRound(roundId, editedRound); + _helper_callEditRound(roundId, editedRound); } function testFuzzEditRound_revertsGivenHookContractIsSetButHookFunctionIsEmpty( @@ -400,7 +406,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { uint64 roundId = fundingPot.getRoundCount(); ILM_PC_FundingPot_v1.Round memory editedRound = - _createEditedRoundParams(); + _helper_createEditedRoundParams(); editedRound.hookContract = address(1); editedRound.hookFunction = bytes(""); @@ -412,7 +418,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { ) ); - _callEditRound(roundId, editedRound); + _helper_callEditRound(roundId, editedRound); } function testFuzzEditRound_revertsGivenHookFunctionIsSetButHookContractIsEmpty( @@ -421,7 +427,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { uint64 roundId = fundingPot.getRoundCount(); ILM_PC_FundingPot_v1.Round memory editedRound = - _createEditedRoundParams(); + _helper_createEditedRoundParams(); editedRound.hookContract = address(0); editedRound.hookFunction = bytes("test"); @@ -433,7 +439,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { ) ); - _callEditRound(roundId, editedRound); + _helper_callEditRound(roundId, editedRound); } /* Test editRound() @@ -454,9 +460,9 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { uint64 lastRoundId = fundingPot.getRoundCount(); ILM_PC_FundingPot_v1.Round memory editedRound = - _createEditedRoundParams(); + _helper_createEditedRoundParams(); - _callEditRound(lastRoundId, editedRound); + _helper_callEditRound(lastRoundId, editedRound); ILM_PC_FundingPot_v1.Round memory updatedRound = fundingPot.getRoundDetails(lastRoundId); @@ -517,17 +523,18 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { hookContract: hookContract_, hookFunction: hookFunction_, closureMechanism: closureMechanism_, - globalAccumulativeCaps: globalAccumulativeCaps_, - isActive: false + globalAccumulativeCaps: globalAccumulativeCaps_ }); return round; } // @notice Creates a default funding round - function _createDefaultFundingRound() + function _helper_createDefaultFundingRound() internal returns (ILM_PC_FundingPot_v1.Round memory) { + //@todo need to randomize the input using vm.bound or vm.assume, do the same for the edited round + //@33 do you have any input here? return _generateFundingRoundParams( block.timestamp + 1 days, block.timestamp + 2 days, @@ -540,7 +547,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { } // @notice calls the create round function - function _callCreateRound(ILM_PC_FundingPot_v1.Round memory round) + function _helper_callCreateRound(ILM_PC_FundingPot_v1.Round memory round) internal { fundingPot.createRound( @@ -555,7 +562,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { } // @notice Creates a predefined funding round with edited parameters for testing - function _createEditedRoundParams() + function _helper_createEditedRoundParams() internal returns (ILM_PC_FundingPot_v1.Round memory) { @@ -571,7 +578,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { } // @notice calls the create round function - function _callEditRound( + function _helper_callEditRound( uint64 roundId, ILM_PC_FundingPot_v1.Round memory round ) internal { From 10cc1eacaa03bb7bb3c8cd35a268fa4219547edb Mon Sep 17 00:00:00 2001 From: JeffreyJoel Date: Thu, 20 Mar 2025 12:31:18 +0100 Subject: [PATCH 019/130] fix: follow inverter standard and remove redundant code --- .../logicModule/LM_PC_FundingPot_v1.sol | 26 +++- .../logicModule/LM_PC_FundingPot_v1.t.sol | 127 ++++++++++-------- .../LM_PC_FundingPot_v1_Exposed.sol | 2 +- 3 files changed, 93 insertions(+), 62 deletions(-) diff --git a/src/modules/logicModule/LM_PC_FundingPot_v1.sol b/src/modules/logicModule/LM_PC_FundingPot_v1.sol index 7cf13c810..8c3ec4b4c 100644 --- a/src/modules/logicModule/LM_PC_FundingPot_v1.sol +++ b/src/modules/logicModule/LM_PC_FundingPot_v1.sol @@ -21,6 +21,27 @@ import {SafeERC20} from "@oz/token/ERC20/utils/SafeERC20.sol"; import {ERC165Upgradeable} from "@oz-up/utils/introspection/ERC165Upgradeable.sol"; +/** + * @title Inverter Funding Pot Module + * + * @notice The module allows project supporters to contribute during funding rounds. + * + * @dev Extends {ERC20PaymentClientBase_v2} and implements {ILM_PC_FundingPot_v1}. + * This contract manages funding rounds with configurable parameters including + * start/end times, funding caps, and hook contracts for custom logic. + * Uses timestamps as flags for payment processing via FLAG_START, FLAG_CLIFF, + * and FLAG_END constants. + * + * @custom:security-contact security@inverter.network + * In case of any concerns or findings, please refer to our Security Policy + * at security.inverter.network or email us directly! + * + * @custom:version v1.0.0 + * + * @custom:inverter-standard-version v0.1.0 + * + * @author Inverter Network + */ contract LM_PC_FundingPot_v1 is ILM_PC_FundingPot_v1, ERC20PaymentClientBase_v2 @@ -64,9 +85,6 @@ contract LM_PC_FundingPot_v1 is // State - /// @notice Payment token. - IERC20 internal _paymentToken; - /// @notice Stores all funding rounds by their unique ID. mapping(uint64 => Round) private rounds; @@ -175,7 +193,7 @@ contract LM_PC_FundingPot_v1 is ) external onlyModuleRole(FUNDING_POT_ADMIN_ROLE) returns (bool) { Round storage round = rounds[_roundId]; - if (round.roundStart == 0) { + if (round.roundEnd == 0 && round.roundCap == 0) { revert Module__LM_PC_FundingPot__RoundNotCreated(); } diff --git a/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol b/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol index 4d3f7644a..6de64fd88 100644 --- a/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol +++ b/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.0; +pragma solidity 0.8.23; // Internal import { @@ -8,12 +8,11 @@ import { IOrchestrator_v1 } from "test/modules/ModuleTest.sol"; import {OZErrors} from "test/utils/errors/OZErrors.sol"; -import {ERC20Mock} from "test/utils/mocks/ERC20Mock.sol"; // External import {Clones} from "@oz/proxy/Clones.sol"; -// Tests and Mocks +// Mocks import { IERC20PaymentClientBase_v2, ERC20PaymentClientBaseV2Mock, @@ -67,7 +66,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { // Initiate the Logic Module with the metadata and config data fundingPot.init(_orchestrator, _METADATA, abi.encode("")); - // Give test contract the DEPOSIT_ADMIN_ROLE. + // Give test contract the FUNDING_POT_ROLE. fundingPot.grantModuleRole( fundingPot.FUNDING_POT_ADMIN_ROLE(), address(this) ); @@ -101,30 +100,37 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { // ------------------------------------------------------------------------- // Test External (public + external) - /* Test createRound() + /* Test fuzzed createRound() ├── Given user does not have FUNDING_POT_ADMIN_ROLE │ └── When user attempts to create a round │ └── Then it should revert - ├── Given round start time is in the past - │ └── When user attempts to create a round - │ └── Then it should revert - ├── Given round end time is 0 and round cap is 0 + └── Given user has FUNDING_POT_ADMIN_ROLE + ├── And round start < block.timestamp │ └── When user attempts to create a round │ └── Then it should revert - ├── Given round end time is set and round end time is in the past + ├── And round end time == 0 + │ ├── And round cap == 0 + │ │ └── When user attempts to create a round + │ │ └── Then it should revert + ├── And round end time is set + │ ├── And round end != 0 + │ ├── And round end < round start + │ │ └── When user attempts to create a round + │ │ └── Then it should revert + ├── And hook contract is set but hook function is not set │ └── When user attempts to create a round │ └── Then it should revert - ├── Given hook contract is set but hook function is not set - │ └── When user attempts to create a round - │ └── Then it should revert - ├── Given hook function is set but hook contract is not set + ├── And hook function is set but hook contract is not set │ └── When user attempts to create a round │ └── Then it should revert + └── Given all the valid parameters are provided + └── When user attempts to create a round + └── Then it should not be active and should return the round id */ - function testFuzzCreateRound_revertsGivenUserIsNotFundingPotAdmin( - address user_ - ) public { + function testCreateRound_revertsGivenUserIsNotFundingPotAdmin(address user_) + public + { vm.assume(user_ != address(0) && user_ != address(this)); vm.startPrank(user_); bytes32 roleId = _authorizer.generateRoleId( @@ -137,13 +143,14 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { ); ILM_PC_FundingPot_v1.Round memory round = _helper_createDefaultFundingRound(); + _helper_callCreateRound(round); vm.stopPrank(); } - function testFuzzCreateRound_revertsGivenRoundStartIsInThePast( - uint roundStart_ - ) public { + function testCreateRound_revertsGivenRoundStartIsInThePast(uint roundStart_) + public + { vm.assume(roundStart_ < block.timestamp); ILM_PC_FundingPot_v1.Round memory round = _helper_createDefaultFundingRound(); @@ -158,7 +165,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { _helper_callCreateRound(round); } - function testFuzzCreateRound_revertsGivenRoundEndTimeAndCapAreBothZero() + function testCreateRound_revertsGivenRoundEndTimeAndCapAreBothZero() public { ILM_PC_FundingPot_v1.Round memory round = @@ -175,7 +182,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { _helper_callCreateRound(round); } - function testFuzzCreateRound_revertsGivenRoundEndTimeIsBeforeRoundStart( + function testCreateRound_revertsGivenRoundEndTimeIsBeforeRoundStart( uint roundEnd_ ) public { ILM_PC_FundingPot_v1.Round memory round = @@ -192,7 +199,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { _helper_callCreateRound(round); } - function testFuzzCreateRound_revertsGivenHookContractIsSetButHookFunctionIsEmpty( + function testCreateRound_revertsGivenHookContractIsSetButHookFunctionIsEmpty( ) public { ILM_PC_FundingPot_v1.Round memory round = _helper_createDefaultFundingRound(); @@ -208,7 +215,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { _helper_callCreateRound(round); } - function testFuzzCreateRound_revertsGivenHookFunctionIsSetButHookContractIsEmpty( + function testCreateRound_revertsGivenHookFunctionIsSetButHookContractIsEmpty( ) public { ILM_PC_FundingPot_v1.Round memory round = _helper_createDefaultFundingRound(); @@ -230,7 +237,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { │ └── Then it should not be active and should return the round id */ - function testFuzzCreateRound() public { + function testCreateRound() public { ILM_PC_FundingPot_v1.Round memory round = _helper_createDefaultFundingRound(); _helper_callCreateRound(round); @@ -248,9 +255,9 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { assertEq(lastRound.globalAccumulativeCaps, round.globalAccumulativeCaps); } - /* Test editRound() + /* Test fuzzed editRound() ├── Given user does not have FUNDING_POT_ADMIN_ROLE - │ └── When user attempts to create a round + │ └── When user attempts to edit a round │ └── Then it should revert ├── Given round does not exist │ └── When user attempts to edit the round @@ -261,24 +268,28 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { ├── Given round start time is in the past │ └── When user attempts to edit a round with the above parameter │ └── Then it should revert - ├── Given round end time is 0 and round cap is 0 - │ └── When user attempts edit a round with the above parameters - │ └── Then it should revert - ├── Given round end time is set and round end time is in the past + ├── Given round end time == 0 + │ ├── And round cap == 0 │ └── When user attempts to edit a round with the above parameters │ └── Then it should revert - ├── Given hook contract is set but hook function is not set - │ └── When user attempts to edit a round with the above parameters + ├── Given round end time is set + │ ├── And round end is before round start + │ └── When user attempts to edit the round │ └── Then it should revert - ├── Given hook function is set but hook contract is not set - │ └── When user attempts to edit a round with the above parameters + ├── Given hook contract is set + │ ├── And hook function is empty + │ └── When user attempts to edit the round │ └── Then it should revert + └── Given hook function is set + ├── And hook contract is empty + └── When user attempts to edit the round + └── Then it should revert */ function testFuzzEditRound_revertsGivenUserIsNotFundingPotAdmin( address user_ ) public { - testFuzzCreateRound(); + testCreateRound(); vm.startPrank(user_); bytes32 roleId = _authorizer.generateRoleId( address(fundingPot), fundingPot.FUNDING_POT_ADMIN_ROLE() @@ -296,11 +307,13 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { } function testFuzzEditRound_revertsGivenRoundIsNotCreated() public { - testFuzzCreateRound(); + testCreateRound(); + + uint64 roundId = fundingPot.getRoundCount(); + ILM_PC_FundingPot_v1.Round memory editedRound = _helper_createEditedRoundParams(); - uint64 roundId = fundingPot.getRoundCount(); vm.expectRevert( abi.encodeWithSelector( ILM_PC_FundingPot_v1 @@ -314,7 +327,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { function testFuzzEditRound_revertsGivenRoundIsActive(uint roundStart_) public { - testFuzzCreateRound(); + testCreateRound(); uint64 roundId = fundingPot.getRoundCount(); ILM_PC_FundingPot_v1.Round memory roundDetails = @@ -337,7 +350,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { function testFuzzEditRound_revertsGivenRoundStartIsInThePast( uint roundStart_ ) public { - testFuzzCreateRound(); + testCreateRound(); uint64 roundId = fundingPot.getRoundCount(); vm.assume(roundStart_ < block.timestamp); @@ -359,7 +372,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { function testFuzzEditRound_revertsGivenRoundEndTimeAndCapAreBothZero() public { - testFuzzCreateRound(); + testCreateRound(); uint64 roundId = fundingPot.getRoundCount(); ILM_PC_FundingPot_v1.Round memory editedRound = @@ -381,7 +394,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { function testFuzzEditRound_revertsGivenRoundEndTimeIsBeforeRoundStart( uint roundEnd_ ) public { - testFuzzCreateRound(); + testCreateRound(); uint64 roundId = fundingPot.getRoundCount(); ILM_PC_FundingPot_v1.Round memory editedRound = @@ -402,7 +415,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { function testFuzzEditRound_revertsGivenHookContractIsSetButHookFunctionIsEmpty( ) public { - testFuzzCreateRound(); + testCreateRound(); uint64 roundId = fundingPot.getRoundCount(); ILM_PC_FundingPot_v1.Round memory editedRound = @@ -423,7 +436,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { function testFuzzEditRound_revertsGivenHookFunctionIsSetButHookContractIsEmpty( ) public { - testFuzzCreateRound(); + testCreateRound(); uint64 roundId = fundingPot.getRoundCount(); ILM_PC_FundingPot_v1.Round memory editedRound = @@ -443,20 +456,21 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { } /* Test editRound() - ├── Given a round has been created and is not active - │ └── When an admin provides valid parameters to edit the round - │ └── Then all the round details should be successfully updated - │ ├── roundStart should be updated to the new value - │ ├── roundEnd should be updated to the new value - │ ├── roundCap should be updated to the new value - │ ├── hookContract should be updated to the new value - │ ├── hookFunction should be updated to the new value - │ ├── closureMechanism should be updated to the new value - │ └── globalAccumulativeCaps should be updated to the new value + └── Given a round has been created + ├── And the round is not active + └── When an admin provides valid parameters to edit the round + └── Then all the round details should be successfully updated + ├── roundStart should be updated to the new value + ├── roundEnd should be updated to the new value + ├── roundCap should be updated to the new value + ├── hookContract should be updated to the new value + ├── hookFunction should be updated to the new value + ├── closureMechanism should be updated to the new value + └── globalAccumulativeCaps should be updated to the new value */ - function testFuzzEditRound() public { - testFuzzCreateRound(); + function testEditRound() public { + testCreateRound(); uint64 lastRoundId = fundingPot.getRoundCount(); ILM_PC_FundingPot_v1.Round memory editedRound = @@ -506,7 +520,6 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { // Helper Functions // @notice Creates a default funding round - // @dev make the parameters fuzzable @TODO Jeffrey function _generateFundingRoundParams( uint roundStart_, uint roundEnd_, diff --git a/test/unit/modules/logicModule/LM_PC_FundingPot_v1_Exposed.sol b/test/unit/modules/logicModule/LM_PC_FundingPot_v1_Exposed.sol index cf0ae2756..a2557f4dc 100644 --- a/test/unit/modules/logicModule/LM_PC_FundingPot_v1_Exposed.sol +++ b/test/unit/modules/logicModule/LM_PC_FundingPot_v1_Exposed.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: LGPL-3.0-only -pragma solidity ^0.8.0; +pragma solidity 0.8.23; // Internal import {LM_PC_FundingPot_v1} from From b058ca0b3bbb4531c90b2621fa0e4bbe001b9ab1 Mon Sep 17 00:00:00 2001 From: JeffreyJoel Date: Thu, 20 Mar 2025 14:03:59 +0100 Subject: [PATCH 020/130] fix: follow inverter standard and remove redundant code --- .../logicModule/LM_PC_FundingPot_v1.t.sol | 37 ++++++------------- 1 file changed, 11 insertions(+), 26 deletions(-) diff --git a/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol b/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol index 6de64fd88..f797af77f 100644 --- a/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol +++ b/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol @@ -290,6 +290,9 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { address user_ ) public { testCreateRound(); + + uint64 roundId = fundingPot.getRoundCount(); + vm.startPrank(user_); bytes32 roleId = _authorizer.generateRoleId( address(fundingPot), fundingPot.FUNDING_POT_ADMIN_ROLE() @@ -302,7 +305,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { ILM_PC_FundingPot_v1.Round memory editedRound = _helper_createEditedRoundParams(); - _helper_callEditRound(0, editedRound); + _helper_callEditRound(roundId, editedRound); vm.stopPrank(); } @@ -481,36 +484,18 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { ILM_PC_FundingPot_v1.Round memory updatedRound = fundingPot.getRoundDetails(lastRoundId); - assertEq( - updatedRound.roundStart, - editedRound.roundStart, - "roundStart not updated" - ); - assertEq( - updatedRound.roundEnd, editedRound.roundEnd, "roundEnd not updated" - ); - assertEq( - updatedRound.roundCap, editedRound.roundCap, "roundCap not updated" - ); - assertEq( - updatedRound.hookContract, - editedRound.hookContract, - "hookContract not updated" - ); + assertEq(updatedRound.roundStart, editedRound.roundStart); + assertEq(updatedRound.roundEnd, editedRound.roundEnd); + assertEq(updatedRound.roundCap, editedRound.roundCap); + assertEq(updatedRound.hookContract, editedRound.hookContract); assertEq( keccak256(updatedRound.hookFunction), - keccak256(editedRound.hookFunction), - "hookFunction not updated" - ); - assertEq( - updatedRound.closureMechanism, - editedRound.closureMechanism, - "closureMechanism not updated" + keccak256(editedRound.hookFunction) ); + assertEq(updatedRound.closureMechanism, editedRound.closureMechanism); assertEq( updatedRound.globalAccumulativeCaps, - editedRound.globalAccumulativeCaps, - "globalAccumulativeCaps not updated" + editedRound.globalAccumulativeCaps ); } From 23097bfa2e9543199a26e9541ae2d06152cbb6b5 Mon Sep 17 00:00:00 2001 From: Zuhaib Mohammed Date: Mon, 24 Mar 2025 17:48:02 +0530 Subject: [PATCH 021/130] feat: add access criteria implementation --- .../logicModule/LM_PC_FundingPot_v1.sol | 101 ++- .../interfaces/ILM_PC_FundingPot_v1.sol | 77 ++- .../logicModule/LM_PC_FundingPot_v1.t.sol | 628 +++++++++++++----- 3 files changed, 617 insertions(+), 189 deletions(-) diff --git a/src/modules/logicModule/LM_PC_FundingPot_v1.sol b/src/modules/logicModule/LM_PC_FundingPot_v1.sol index 8c3ec4b4c..275910e28 100644 --- a/src/modules/logicModule/LM_PC_FundingPot_v1.sol +++ b/src/modules/logicModule/LM_PC_FundingPot_v1.sol @@ -126,12 +126,47 @@ contract LM_PC_FundingPot_v1 is // Public - Getters /// @inheritdoc ILM_PC_FundingPot_v1 - function getRoundDetails(uint64 _roundId) + function getRoundGenericParameters(uint64 _roundId) external view - returns (Round memory) + returns ( + uint roundStart, + uint roundEnd, + uint roundCap, + address hookContract, + bytes memory hookFunction, + bool closureMechanism, + bool globalAccumulativeCaps + ) { - return rounds[_roundId]; + Round storage round = rounds[_roundId]; + return ( + round.roundStart, + round.roundEnd, + round.roundCap, + round.hookContract, + round.hookFunction, + round.closureMechanism, + round.globalAccumulativeCaps + ); + } + + function getRoundAccessCriteria(uint64 _roundId, uint64 _id) + external + view + returns ( + address nftContract, + bytes32 merkleRoot, + address[] memory allowedAddresses + ) + { + Round storage round = rounds[_roundId]; + AccessCriteria storage accessCriteria = round.accessCriterias[_id]; + return ( + accessCriteria.nftContract, + accessCriteria.merkleRoot, + accessCriteria.allowedAddresses + ); } /// @inheritdoc ILM_PC_FundingPot_v1 @@ -155,17 +190,17 @@ contract LM_PC_FundingPot_v1 is nextRoundId++; uint64 roundId = nextRoundId; - rounds[roundId] = Round({ - roundStart: _roundStart, - roundEnd: _roundEnd, - roundCap: _roundCap, - hookContract: _hookContract, - hookFunction: _hookFunction, - closureMechanism: _closureMechanism, - globalAccumulativeCaps: _globalAccumulativeCaps - }); - - _validateRoundParameters(rounds[roundId]); + + Round storage round = rounds[roundId]; + round.roundStart = _roundStart; + round.roundEnd = _roundEnd; + round.roundCap = _roundCap; + round.hookContract = _hookContract; + round.hookFunction = _hookFunction; + round.closureMechanism = _closureMechanism; + round.globalAccumulativeCaps = _globalAccumulativeCaps; + + _validateRoundParameters(round); emit RoundCreated( roundId, @@ -223,13 +258,49 @@ contract LM_PC_FundingPot_v1 is return true; } + + function setAccessCriteriaForRound( + uint64 _roundId, + uint8 _accessId, + AccessCriteria memory _accessCriteria + ) external onlyModuleRole(FUNDING_POT_ADMIN_ROLE) { + Round storage round = rounds[_roundId]; + + if (round.roundEnd == 0 && round.roundCap == 0) { + revert Module__LM_PC_FundingPot__RoundNotCreated(); + } + + if (block.timestamp > round.roundStart) { + revert Module__LM_PC_FundingPot__RoundAlreadyStarted(); + } + + if ( + ( + _accessCriteria.accessCriteriaId == AccessCriteriaId.NFT + && _accessCriteria.nftContract == address(0) + ) + || ( + _accessCriteria.accessCriteriaId == AccessCriteriaId.MERKLE + && _accessCriteria.merkleRoot == bytes32("") + ) + || ( + _accessCriteria.accessCriteriaId == AccessCriteriaId.LIST + && _accessCriteria.allowedAddresses.length == 0 + ) + ) { + revert Module__LM_PC_FundingPot__IncorrectAccessCriteria(); + } + + round.accessCriterias[_accessId] = _accessCriteria; + emit AccessCriteriaSet(_roundId, _accessId, _accessCriteria); + } // ------------------------------------------------------------------------- // Internal /// @notice Validates the round parameters. /// @param round The round to validate. /// @dev Reverts if the round parameters are invalid. - function _validateRoundParameters(Round memory round) internal view { + function _validateRoundParameters(Round storage round) internal view { // Validate round start time is in the future // @note: The below condition wont allow _roundStart == block.timestamp if (round.roundStart <= block.timestamp) { diff --git a/src/modules/logicModule/interfaces/ILM_PC_FundingPot_v1.sol b/src/modules/logicModule/interfaces/ILM_PC_FundingPot_v1.sol index b2ea1ff16..1fec08169 100644 --- a/src/modules/logicModule/interfaces/ILM_PC_FundingPot_v1.sol +++ b/src/modules/logicModule/interfaces/ILM_PC_FundingPot_v1.sol @@ -17,6 +17,7 @@ interface ILM_PC_FundingPot_v1 is IERC20PaymentClientBase_v2 { /// @param hookFunction Encoded function call to be executed on the `hookContract` after round closure. /// @param closureMechanism Indicates whether the hook closure coincides with the contribution span end. /// @param globalAccumulativeCaps Indicates whether contribution caps accumulate globally across rounds. + /// @param accessCriterias Mapping of access criteria IDs to their respective access criteria. struct Round { uint roundStart; uint roundEnd; @@ -25,6 +26,30 @@ interface ILM_PC_FundingPot_v1 is IERC20PaymentClientBase_v2 { bytes hookFunction; bool closureMechanism; bool globalAccumulativeCaps; + mapping(uint64 id => AccessCriteria) accessCriterias; + } + + /// @notice Struct used to store information about a funding round's access criteria. + /// @param nftContract Address of the NFT contract. + /// @param merkleRoot Merkle root for the access criteria. + /// @param allowedAddresses Mapping of addresses to their access status. + struct AccessCriteria { + AccessCriteriaId accessCriteriaId; + address nftContract; // NFT contract address (0x0 if unused) + bytes32 merkleRoot; // Merkle root (0x0 if unused) + address[] allowedAddresses; // Explicit allowlist + } + + // ------------------------------------------------------------------------- + // Enums + + /// @notice Enum used to identify the type of access criteria. + enum AccessCriteriaId { + OPEN, // 0 + NFT, // 1 + MERKLE, // 2 + LIST // 3 + } // ------------------------------------------------------------------------- @@ -68,6 +93,11 @@ interface ILM_PC_FundingPot_v1 is IERC20PaymentClientBase_v2 { bool globalAccumulativeCaps ); + /// @notice + event AccessCriteriaSet( + uint64 indexed roundId, uint8 accessId, AccessCriteria accessCriteria + ); + // ------------------------------------------------------------------------- // Errors @@ -101,16 +131,48 @@ interface ILM_PC_FundingPot_v1 is IERC20PaymentClientBase_v2 { /// @notice Round does not exist error Module__LM_PC_FundingPot__RoundNotCreated(); + /// @notice + error Module__LM_PC_FundingPot__IncorrectAccessCriteria(); + // ------------------------------------------------------------------------- // Public - Getters - /// @notice Retrieves the details of a specific funding round. + /// @notice Retrieves the generic parameters of a specific funding round. /// @param _roundId The unique identifier of the round to retrieve. - /// @return A struct containing the round's details. - function getRoundDetails(uint64 _roundId) + /// @return roundStart The timestamp when the round starts + /// @return roundEnd The timestamp when the round ends + /// @return roundCap The maximum contribution cap for the round + /// @return hookContract The address of the hook contract + /// @return hookFunction The encoded function call for the hook + /// @return closureMechanism Whether hook closure coincides with contribution span end + /// @return globalAccumulativeCaps Whether caps accumulate globally across rounds + function getRoundGenericParameters(uint64 _roundId) external view - returns (Round memory); + returns ( + uint roundStart, + uint roundEnd, + uint roundCap, + address hookContract, + bytes memory hookFunction, + bool closureMechanism, + bool globalAccumulativeCaps + ); + + /// @notice Retrieves the access criteria for a specific funding round. + /// @param _roundId The unique identifier of the round to retrieve. + /// @param _id The identifier of the access criteria to retrieve. + /// @return nftContract The address of the NFT contract used for access control + /// @return merkleRoot The merkle root used for access verification + /// @return allowedAddresses The list of explicitly allowed addresses + function getRoundAccessCriteria(uint64 _roundId, uint64 _id) + external + view + returns ( + address nftContract, + bytes32 merkleRoot, + address[] memory allowedAddresses + ); /// @notice Retrieves the total number of funding rounds. /// @return The total number of funding rounds. @@ -160,4 +222,11 @@ interface ILM_PC_FundingPot_v1 is IERC20PaymentClientBase_v2 { bool _closureMechanism, bool _globalAccumulativeCaps ) external returns (bool); + + /// @notice Set Access Control Check + function setAccessCriteriaForRound( + uint64 _roundId, + uint8 _accessId, + AccessCriteria memory _accessCriteria + ) external; } diff --git a/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol b/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol index f797af77f..84350bfe3 100644 --- a/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol +++ b/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol @@ -141,10 +141,24 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { IModule_v1.Module__CallerNotAuthorized.selector, roleId, user_ ) ); - ILM_PC_FundingPot_v1.Round memory round = - _helper_createDefaultFundingRound(); - - _helper_callCreateRound(round); + ( + uint roundStart, + uint roundEnd, + uint roundCap, + address hookContract, + bytes memory hookFunction, + bool closureMechanism, + bool globalAccumulativeCaps + ) = _helper_createDefaultFundingRound(); + _helper_callCreateRound( + roundStart, + roundEnd, + roundCap, + hookContract, + hookFunction, + closureMechanism, + globalAccumulativeCaps + ); vm.stopPrank(); } @@ -152,9 +166,16 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { public { vm.assume(roundStart_ < block.timestamp); - ILM_PC_FundingPot_v1.Round memory round = - _helper_createDefaultFundingRound(); - round.roundStart = roundStart_; + ( + uint roundStart, + uint roundEnd, + uint roundCap, + address hookContract, + bytes memory hookFunction, + bool closureMechanism, + bool globalAccumulativeCaps + ) = _helper_createDefaultFundingRound(); + roundStart = roundStart_; vm.expectRevert( abi.encodeWithSelector( ILM_PC_FundingPot_v1 @@ -162,16 +183,31 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { .selector ) ); - _helper_callCreateRound(round); + _helper_callCreateRound( + roundStart, + roundEnd, + roundCap, + hookContract, + hookFunction, + closureMechanism, + globalAccumulativeCaps + ); } function testCreateRound_revertsGivenRoundEndTimeAndCapAreBothZero() public { - ILM_PC_FundingPot_v1.Round memory round = - _helper_createDefaultFundingRound(); - round.roundEnd = 0; - round.roundCap = 0; + ( + uint roundStart, + uint roundEnd, + uint roundCap, + address hookContract, + bytes memory hookFunction, + bool closureMechanism, + bool globalAccumulativeCaps + ) = _helper_createDefaultFundingRound(); + roundEnd = 0; + roundCap = 0; vm.expectRevert( abi.encodeWithSelector( ILM_PC_FundingPot_v1 @@ -179,16 +215,31 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { .selector ) ); - _helper_callCreateRound(round); + _helper_callCreateRound( + roundStart, + roundEnd, + roundCap, + hookContract, + hookFunction, + closureMechanism, + globalAccumulativeCaps + ); } function testCreateRound_revertsGivenRoundEndTimeIsBeforeRoundStart( uint roundEnd_ ) public { - ILM_PC_FundingPot_v1.Round memory round = - _helper_createDefaultFundingRound(); - vm.assume(roundEnd_ != 0 && roundEnd_ < round.roundStart); - round.roundEnd = roundEnd_; + ( + uint roundStart, + uint roundEnd, + uint roundCap, + address hookContract, + bytes memory hookFunction, + bool closureMechanism, + bool globalAccumulativeCaps + ) = _helper_createDefaultFundingRound(); + vm.assume(roundEnd_ != 0 && roundEnd_ < roundStart); + roundEnd = roundEnd_; vm.expectRevert( abi.encodeWithSelector( ILM_PC_FundingPot_v1 @@ -196,15 +247,30 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { .selector ) ); - _helper_callCreateRound(round); + _helper_callCreateRound( + roundStart, + roundEnd, + roundCap, + hookContract, + hookFunction, + closureMechanism, + globalAccumulativeCaps + ); } function testCreateRound_revertsGivenHookContractIsSetButHookFunctionIsEmpty( ) public { - ILM_PC_FundingPot_v1.Round memory round = - _helper_createDefaultFundingRound(); - round.hookContract = address(1); - round.hookFunction = bytes(""); + ( + uint roundStart, + uint roundEnd, + uint roundCap, + address hookContract, + bytes memory hookFunction, + bool closureMechanism, + bool globalAccumulativeCaps + ) = _helper_createDefaultFundingRound(); + hookContract = address(1); + hookFunction = bytes(""); vm.expectRevert( abi.encodeWithSelector( ILM_PC_FundingPot_v1 @@ -212,15 +278,30 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { .selector ) ); - _helper_callCreateRound(round); + _helper_callCreateRound( + roundStart, + roundEnd, + roundCap, + hookContract, + hookFunction, + closureMechanism, + globalAccumulativeCaps + ); } function testCreateRound_revertsGivenHookFunctionIsSetButHookContractIsEmpty( ) public { - ILM_PC_FundingPot_v1.Round memory round = - _helper_createDefaultFundingRound(); - round.hookContract = address(0); - round.hookFunction = bytes("test"); + ( + uint roundStart, + uint roundEnd, + uint roundCap, + address hookContract, + bytes memory hookFunction, + bool closureMechanism, + bool globalAccumulativeCaps + ) = _helper_createDefaultFundingRound(); + hookContract = address(0); + hookFunction = bytes("test"); vm.expectRevert( abi.encodeWithSelector( ILM_PC_FundingPot_v1 @@ -228,31 +309,61 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { .selector ) ); - _helper_callCreateRound(round); + _helper_callCreateRound( + roundStart, + roundEnd, + roundCap, + hookContract, + hookFunction, + closureMechanism, + globalAccumulativeCaps + ); } /* Test createRound() - ├── Given all the valid parameters are provided - │ └── When user attempts to create a round - │ └── Then it should not be active and should return the round id - */ + ├── Given all the valid parameters are provided + │ └── When user attempts to create a round + │ └── Then it should not be active and should return the round id + */ function testCreateRound() public { - ILM_PC_FundingPot_v1.Round memory round = - _helper_createDefaultFundingRound(); - _helper_callCreateRound(round); + ( + uint roundStart, + uint roundEnd, + uint roundCap, + address hookContract, + bytes memory hookFunction, + bool closureMechanism, + bool globalAccumulativeCaps + ) = _helper_createDefaultFundingRound(); + _helper_callCreateRound( + roundStart, + roundEnd, + roundCap, + hookContract, + hookFunction, + closureMechanism, + globalAccumulativeCaps + ); uint64 lastRoundId = fundingPot.getRoundCount(); - ILM_PC_FundingPot_v1.Round memory lastRound = - fundingPot.getRoundDetails(lastRoundId); - - assertEq(lastRound.roundStart, round.roundStart); - assertEq(lastRound.roundEnd, round.roundEnd); - assertEq(lastRound.roundCap, round.roundCap); - assertEq(lastRound.hookContract, round.hookContract); - assertEq(lastRound.hookFunction, round.hookFunction); - assertEq(lastRound.closureMechanism, round.closureMechanism); - assertEq(lastRound.globalAccumulativeCaps, round.globalAccumulativeCaps); + ( + uint roundStart_, + uint roundEnd_, + uint roundCap_, + address hookContract_, + bytes memory hookFunction_, + bool closureMechanism_, + bool globalAccumulativeCaps_ + ) = fundingPot.getRoundGenericParameters(lastRoundId); + + assertEq(roundStart, roundStart_); + assertEq(roundEnd, roundEnd_); + assertEq(roundCap, roundCap_); + assertEq(hookContract, hookContract_); + assertEq(hookFunction, hookFunction_); + assertEq(closureMechanism, closureMechanism_); + assertEq(globalAccumulativeCaps, globalAccumulativeCaps_); } /* Test fuzzed editRound() @@ -302,10 +413,26 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { IModule_v1.Module__CallerNotAuthorized.selector, roleId, user_ ) ); - ILM_PC_FundingPot_v1.Round memory editedRound = - _helper_createEditedRoundParams(); - - _helper_callEditRound(roundId, editedRound); + ( + uint roundStart_, + uint roundEnd_, + uint roundCap_, + address hookContract_, + bytes memory hookFunction_, + bool closureMechanism_, + bool globalAccumulativeCaps_ + ) = _helper_createEditedRoundParams(); + + _helper_callEditRound( + 0, + roundStart_, + roundEnd_, + roundCap_, + hookContract_, + hookFunction_, + closureMechanism_, + globalAccumulativeCaps_ + ); vm.stopPrank(); } @@ -314,8 +441,15 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { uint64 roundId = fundingPot.getRoundCount(); - ILM_PC_FundingPot_v1.Round memory editedRound = - _helper_createEditedRoundParams(); + ( + uint roundStart_, + uint roundEnd_, + uint roundCap_, + address hookContract_, + bytes memory hookFunction_, + bool closureMechanism_, + bool globalAccumulativeCaps_ + ) = _helper_createEditedRoundParams(); vm.expectRevert( abi.encodeWithSelector( @@ -324,7 +458,16 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { .selector ) ); - _helper_callEditRound(roundId + 1, editedRound); + _helper_callEditRound( + roundId + 1, + roundStart_, + roundEnd_, + roundCap_, + hookContract_, + hookFunction_, + closureMechanism_, + globalAccumulativeCaps_ + ); } function testFuzzEditRound_revertsGivenRoundIsActive(uint roundStart_) @@ -333,13 +476,26 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { testCreateRound(); uint64 roundId = fundingPot.getRoundCount(); - ILM_PC_FundingPot_v1.Round memory roundDetails = - fundingPot.getRoundDetails(roundId); - - vm.warp(roundDetails.roundStart + 1); - - ILM_PC_FundingPot_v1.Round memory editedRound = - _helper_createEditedRoundParams(); + ( + uint roundStart, + uint roundEnd, + uint roundCap, + address hookContract, + bytes memory hookFunction, + bool closureMechanism, + bool globalAccumulativeCaps + ) = fundingPot.getRoundGenericParameters(roundId); + vm.warp(roundStart + 1); + + ( + uint roundStart_, + uint roundEnd_, + uint roundCap_, + address hookContract_, + bytes memory hookFunction_, + bool closureMechanism_, + bool globalAccumulativeCaps_ + ) = _helper_createEditedRoundParams(); vm.expectRevert( abi.encodeWithSelector( ILM_PC_FundingPot_v1 @@ -347,19 +503,35 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { .selector ) ); - _helper_callEditRound(roundId, editedRound); + _helper_callEditRound( + roundId, + roundStart_, + roundEnd_, + roundCap_, + hookContract_, + hookFunction_, + closureMechanism_, + globalAccumulativeCaps_ + ); } function testFuzzEditRound_revertsGivenRoundStartIsInThePast( - uint roundStart_ + uint roundStartP_ ) public { testCreateRound(); uint64 roundId = fundingPot.getRoundCount(); - vm.assume(roundStart_ < block.timestamp); - ILM_PC_FundingPot_v1.Round memory editedRound = - _helper_createEditedRoundParams(); - editedRound.roundStart = roundStart_; + vm.assume(roundStartP_ < block.timestamp); + ( + uint roundStart_, + uint roundEnd_, + uint roundCap_, + address hookContract_, + bytes memory hookFunction_, + bool closureMechanism_, + bool globalAccumulativeCaps_ + ) = _helper_createEditedRoundParams(); + roundStart_ = roundStartP_; vm.expectRevert( abi.encodeWithSelector( @@ -369,7 +541,16 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { ) ); - _helper_callEditRound(roundId, editedRound); + _helper_callEditRound( + roundId, + roundStart_, + roundEnd_, + roundCap_, + hookContract_, + hookFunction_, + closureMechanism_, + globalAccumulativeCaps_ + ); } function testFuzzEditRound_revertsGivenRoundEndTimeAndCapAreBothZero() @@ -378,10 +559,17 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { testCreateRound(); uint64 roundId = fundingPot.getRoundCount(); - ILM_PC_FundingPot_v1.Round memory editedRound = - _helper_createEditedRoundParams(); - editedRound.roundEnd = 0; - editedRound.roundCap = 0; + ( + uint roundStart_, + uint roundEnd_, + uint roundCap_, + address hookContract_, + bytes memory hookFunction_, + bool closureMechanism_, + bool globalAccumulativeCaps_ + ) = _helper_createEditedRoundParams(); + roundEnd_ = 0; + roundCap_ = 0; vm.expectRevert( abi.encodeWithSelector( @@ -391,7 +579,16 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { ) ); - _helper_callEditRound(roundId, editedRound); + _helper_callEditRound( + roundId, + roundStart_, + roundEnd_, + roundCap_, + hookContract_, + hookFunction_, + closureMechanism_, + globalAccumulativeCaps_ + ); } function testFuzzEditRound_revertsGivenRoundEndTimeIsBeforeRoundStart( @@ -400,10 +597,16 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { testCreateRound(); uint64 roundId = fundingPot.getRoundCount(); - ILM_PC_FundingPot_v1.Round memory editedRound = - _helper_createEditedRoundParams(); - vm.assume(roundEnd_ != 0 && roundEnd_ < editedRound.roundStart); - editedRound.roundEnd = roundEnd_; + ( + uint roundStart_, + uint roundEnd_, + uint roundCap_, + address hookContract_, + bytes memory hookFunction_, + bool closureMechanism_, + bool globalAccumulativeCaps_ + ) = _helper_createEditedRoundParams(); + roundEnd_ = bound(roundEnd_, 0, roundStart_ - 1); vm.expectRevert( abi.encodeWithSelector( @@ -413,7 +616,16 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { ) ); - _helper_callEditRound(roundId, editedRound); + _helper_callEditRound( + roundId, + roundStart_, + roundEnd_, + roundCap_, + hookContract_, + hookFunction_, + closureMechanism_, + globalAccumulativeCaps_ + ); } function testFuzzEditRound_revertsGivenHookContractIsSetButHookFunctionIsEmpty( @@ -421,10 +633,17 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { testCreateRound(); uint64 roundId = fundingPot.getRoundCount(); - ILM_PC_FundingPot_v1.Round memory editedRound = - _helper_createEditedRoundParams(); - editedRound.hookContract = address(1); - editedRound.hookFunction = bytes(""); + ( + uint roundStart_, + uint roundEnd_, + uint roundCap_, + address hookContract_, + bytes memory hookFunction_, + bool closureMechanism_, + bool globalAccumulativeCaps_ + ) = _helper_createEditedRoundParams(); + hookContract_ = address(1); + hookFunction_ = bytes(""); vm.expectRevert( abi.encodeWithSelector( @@ -434,7 +653,16 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { ) ); - _helper_callEditRound(roundId, editedRound); + _helper_callEditRound( + roundId, + roundStart_, + roundEnd_, + roundCap_, + hookContract_, + hookFunction_, + closureMechanism_, + globalAccumulativeCaps_ + ); } function testFuzzEditRound_revertsGivenHookFunctionIsSetButHookContractIsEmpty( @@ -442,10 +670,17 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { testCreateRound(); uint64 roundId = fundingPot.getRoundCount(); - ILM_PC_FundingPot_v1.Round memory editedRound = - _helper_createEditedRoundParams(); - editedRound.hookContract = address(0); - editedRound.hookFunction = bytes("test"); + ( + uint roundStart_, + uint roundEnd_, + uint roundCap_, + address hookContract_, + bytes memory hookFunction_, + bool closureMechanism_, + bool globalAccumulativeCaps_ + ) = _helper_createEditedRoundParams(); + hookContract_ = address(0); + hookFunction_ = bytes("test"); vm.expectRevert( abi.encodeWithSelector( @@ -455,7 +690,16 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { ) ); - _helper_callEditRound(roundId, editedRound); + _helper_callEditRound( + roundId, + roundStart_, + roundEnd_, + roundCap_, + hookContract_, + hookFunction_, + closureMechanism_, + globalAccumulativeCaps_ + ); } /* Test editRound() @@ -476,119 +720,163 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { testCreateRound(); uint64 lastRoundId = fundingPot.getRoundCount(); - ILM_PC_FundingPot_v1.Round memory editedRound = - _helper_createEditedRoundParams(); - - _helper_callEditRound(lastRoundId, editedRound); - - ILM_PC_FundingPot_v1.Round memory updatedRound = - fundingPot.getRoundDetails(lastRoundId); - - assertEq(updatedRound.roundStart, editedRound.roundStart); - assertEq(updatedRound.roundEnd, editedRound.roundEnd); - assertEq(updatedRound.roundCap, editedRound.roundCap); - assertEq(updatedRound.hookContract, editedRound.hookContract); - assertEq( - keccak256(updatedRound.hookFunction), - keccak256(editedRound.hookFunction) + ( + uint roundStart_, + uint roundEnd_, + uint roundCap_, + address hookContract_, + bytes memory hookFunction_, + bool closureMechanism_, + bool globalAccumulativeCaps_ + ) = _helper_createEditedRoundParams(); + + _helper_callEditRound( + lastRoundId, + roundStart_, + roundEnd_, + roundCap_, + hookContract_, + hookFunction_, + closureMechanism_, + globalAccumulativeCaps_ ); - assertEq(updatedRound.closureMechanism, editedRound.closureMechanism); - assertEq( - updatedRound.globalAccumulativeCaps, - editedRound.globalAccumulativeCaps - ); - } - - // ------------------------------------------------------------------------- - // Test: Internal Functions - - // Helper Functions - // @notice Creates a default funding round - function _generateFundingRoundParams( - uint roundStart_, - uint roundEnd_, - uint roundCap_, - address hookContract_, - bytes memory hookFunction_, - bool closureMechanism_, - bool globalAccumulativeCaps_ - ) internal returns (ILM_PC_FundingPot_v1.Round memory) { - ILM_PC_FundingPot_v1.Round memory round = ILM_PC_FundingPot_v1.Round({ - roundStart: roundStart_, - roundEnd: roundEnd_, - roundCap: roundCap_, - hookContract: hookContract_, - hookFunction: hookFunction_, - closureMechanism: closureMechanism_, - globalAccumulativeCaps: globalAccumulativeCaps_ - }); - return round; + ( + uint roundStart, + uint roundEnd, + uint roundCap, + address hookContract, + bytes memory hookFunction, + bool closureMechanism, + bool globalAccumulativeCaps + ) = fundingPot.getRoundGenericParameters(lastRoundId); + + assertEq(roundStart, roundStart_); + assertEq(roundEnd, roundEnd_); + assertEq(roundCap, roundCap_); + assertEq(hookContract, hookContract_); + assertEq(hookFunction, hookFunction_); + assertEq(closureMechanism, closureMechanism_); + assertEq(globalAccumulativeCaps, globalAccumulativeCaps_); } + // // ------------------------------------------------------------------------- + // // Test: Internal Functions + + // // Helper Functions + + // // @notice Creates a default funding round + // // @dev make the parameters fuzzable @TODO Jeffrey + // function _generateFundingRoundParams( + // uint roundStart_, + // uint roundEnd_, + // uint roundCap_, + // address hookContract_, + // bytes memory hookFunction_, + // bool closureMechanism_, + // bool globalAccumulativeCaps_ + // ) internal returns (ILM_PC_FundingPot_v1.Round memory) { + // ILM_PC_FundingPot_v1.Round memory round = ILM_PC_FundingPot_v1.Round({ + // roundStart: roundStart_, + // roundEnd: roundEnd_, + // roundCap: roundCap_, + // hookContract: hookContract_, + // hookFunction: hookFunction_, + // closureMechanism: closureMechanism_, + // globalAccumulativeCaps: globalAccumulativeCaps_ + // }); + // return round; + // } + // @notice Creates a default funding round function _helper_createDefaultFundingRound() internal - returns (ILM_PC_FundingPot_v1.Round memory) + returns (uint, uint, uint, address, bytes memory, bool, bool) { - //@todo need to randomize the input using vm.bound or vm.assume, do the same for the edited round - //@33 do you have any input here? - return _generateFundingRoundParams( - block.timestamp + 1 days, - block.timestamp + 2 days, - 1000, - address(0), - bytes(""), - false, - false + uint roundStart = block.timestamp + 1 days; + uint roundEnd = block.timestamp + 2 days; + uint roundCap = 1000; + address hookContract = address(0); + bytes memory hookFunction = bytes(""); + bool closureMechanism = false; + bool globalAccumulativeCaps = false; + + return ( + roundStart, + roundEnd, + roundCap, + hookContract, + hookFunction, + closureMechanism, + globalAccumulativeCaps ); } // @notice calls the create round function - function _helper_callCreateRound(ILM_PC_FundingPot_v1.Round memory round) - internal - { + function _helper_callCreateRound( + uint roundStart, + uint roundEnd, + uint roundCap, + address hookContract, + bytes memory hookFunction, + bool closureMechanism, + bool globalAccumulativeCaps + ) internal { fundingPot.createRound( - round.roundStart, - round.roundEnd, - round.roundCap, - round.hookContract, - round.hookFunction, - round.closureMechanism, - round.globalAccumulativeCaps + roundStart, + roundEnd, + roundCap, + hookContract, + hookFunction, + closureMechanism, + globalAccumulativeCaps ); } // @notice Creates a predefined funding round with edited parameters for testing function _helper_createEditedRoundParams() internal - returns (ILM_PC_FundingPot_v1.Round memory) + returns (uint, uint, uint, address, bytes memory, bool, bool) { - return _generateFundingRoundParams( - block.timestamp + 150, - block.timestamp + 250, - 20, - address(0x1), - hex"abcd", - true, - true + uint roundStart_ = block.timestamp + 3 days; + uint roundEnd_ = block.timestamp + 4 days; + uint roundCap_ = 2000; + address hookContract_ = address(0x1); + bytes memory hookFunction_ = bytes("test"); + bool closureMechanism_ = true; + bool globalAccumulativeCaps_ = true; + + return ( + roundStart_, + roundEnd_, + roundCap_, + hookContract_, + hookFunction_, + closureMechanism_, + globalAccumulativeCaps_ ); } // @notice calls the create round function function _helper_callEditRound( uint64 roundId, - ILM_PC_FundingPot_v1.Round memory round + uint roundStart, + uint roundEnd, + uint roundCap, + address hookContract, + bytes memory hookFunction, + bool closureMechanism, + bool globalAccumulativeCaps ) internal { fundingPot.editRound( roundId, - round.roundStart, - round.roundEnd, - round.roundCap, - round.hookContract, - round.hookFunction, - round.closureMechanism, - round.globalAccumulativeCaps + roundStart, + roundEnd, + roundCap, + hookContract, + hookFunction, + closureMechanism, + globalAccumulativeCaps ); } } From 59626508b15a40986235a919b569def51444353b Mon Sep 17 00:00:00 2001 From: Zuhaib Mohammed Date: Tue, 25 Mar 2025 04:05:00 +0530 Subject: [PATCH 022/130] test: code format & access control criteria unit tests --- .../logicModule/LM_PC_FundingPot_v1.sol | 8 +- .../interfaces/ILM_PC_FundingPot_v1.sol | 9 +- .../logicModule/LM_PC_FundingPot_v1.t.sol | 315 +++++++++++++++--- 3 files changed, 275 insertions(+), 57 deletions(-) diff --git a/src/modules/logicModule/LM_PC_FundingPot_v1.sol b/src/modules/logicModule/LM_PC_FundingPot_v1.sol index 275910e28..96490bb32 100644 --- a/src/modules/logicModule/LM_PC_FundingPot_v1.sol +++ b/src/modules/logicModule/LM_PC_FundingPot_v1.sol @@ -126,6 +126,8 @@ contract LM_PC_FundingPot_v1 is // Public - Getters /// @inheritdoc ILM_PC_FundingPot_v1 + /// @dev Returns the generic parameters of a round. + function getRoundGenericParameters(uint64 _roundId) external view @@ -162,6 +164,7 @@ contract LM_PC_FundingPot_v1 is { Round storage round = rounds[_roundId]; AccessCriteria storage accessCriteria = round.accessCriterias[_id]; + //@todo : Waht if the round is open? add a bool? check what can be done !!! return ( accessCriteria.nftContract, accessCriteria.merkleRoot, @@ -259,6 +262,7 @@ contract LM_PC_FundingPot_v1 is return true; } + /// @inheritdoc ILM_PC_FundingPot_v1 function setAccessCriteriaForRound( uint64 _roundId, uint8 _accessId, @@ -288,7 +292,7 @@ contract LM_PC_FundingPot_v1 is && _accessCriteria.allowedAddresses.length == 0 ) ) { - revert Module__LM_PC_FundingPot__IncorrectAccessCriteria(); + revert Module__LM_PC_FundingPot__MissingRequiredAccessCriteriaData(); } round.accessCriterias[_accessId] = _accessCriteria; @@ -308,7 +312,7 @@ contract LM_PC_FundingPot_v1 is } // Validate that either end time or cap is set - if (round.roundEnd == 0 && round.roundCap == 0) { + if (round.roundEnd == 0 || round.roundCap == 0) { revert Module__LM_PC_FundingPot__RoundMustHaveEndTimeOrCap(); } diff --git a/src/modules/logicModule/interfaces/ILM_PC_FundingPot_v1.sol b/src/modules/logicModule/interfaces/ILM_PC_FundingPot_v1.sol index 1fec08169..9dbbad8b5 100644 --- a/src/modules/logicModule/interfaces/ILM_PC_FundingPot_v1.sol +++ b/src/modules/logicModule/interfaces/ILM_PC_FundingPot_v1.sol @@ -93,7 +93,10 @@ interface ILM_PC_FundingPot_v1 is IERC20PaymentClientBase_v2 { bool globalAccumulativeCaps ); - /// @notice + /// @notice Emitted when access criteria is set for a round. + /// @param roundId The unique identifier of the round. + /// @param accessId The identifier of the access criteria. + /// @param accessCriteria The access criteria. event AccessCriteriaSet( uint64 indexed roundId, uint8 accessId, AccessCriteria accessCriteria ); @@ -131,8 +134,8 @@ interface ILM_PC_FundingPot_v1 is IERC20PaymentClientBase_v2 { /// @notice Round does not exist error Module__LM_PC_FundingPot__RoundNotCreated(); - /// @notice - error Module__LM_PC_FundingPot__IncorrectAccessCriteria(); + /// @notice Incorrect access criteria + error Module__LM_PC_FundingPot__MissingRequiredAccessCriteriaData(); // ------------------------------------------------------------------------- // Public - Getters diff --git a/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol b/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol index 84350bfe3..19869a2f5 100644 --- a/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol +++ b/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol @@ -54,7 +54,6 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { // ------------------------------------------------------------------------- // Setup - function setUp() public { // Deploy the SuT address impl = address(new LM_PC_FundingPot_v1_Exposed()); @@ -66,10 +65,6 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { // Initiate the Logic Module with the metadata and config data fundingPot.init(_orchestrator, _METADATA, abi.encode("")); - // Give test contract the FUNDING_POT_ROLE. - fundingPot.grantModuleRole( - fundingPot.FUNDING_POT_ADMIN_ROLE(), address(this) - ); _authorizer.setIsAuthorized(address(this), true); // Set the block timestamp @@ -100,7 +95,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { // ------------------------------------------------------------------------- // Test External (public + external) - /* Test fuzzed createRound() + /* Test createRound() ├── Given user does not have FUNDING_POT_ADMIN_ROLE │ └── When user attempts to create a round │ └── Then it should revert @@ -366,7 +361,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { assertEq(globalAccumulativeCaps, globalAccumulativeCaps_); } - /* Test fuzzed editRound() + /* Test editRound() ├── Given user does not have FUNDING_POT_ADMIN_ROLE │ └── When user attempts to edit a round │ └── Then it should revert @@ -397,9 +392,9 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { └── Then it should revert */ - function testFuzzEditRound_revertsGivenUserIsNotFundingPotAdmin( - address user_ - ) public { + function testEditRound_revertsGivenUserIsNotFundingPotAdmin(address user_) + public + { testCreateRound(); uint64 roundId = fundingPot.getRoundCount(); @@ -436,7 +431,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { vm.stopPrank(); } - function testFuzzEditRound_revertsGivenRoundIsNotCreated() public { + function testEditRound_revertsGivenRoundIsNotCreated() public { testCreateRound(); uint64 roundId = fundingPot.getRoundCount(); @@ -470,9 +465,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { ); } - function testFuzzEditRound_revertsGivenRoundIsActive(uint roundStart_) - public - { + function testEditRound_revertsGivenRoundIsActive(uint roundStart_) public { testCreateRound(); uint64 roundId = fundingPot.getRoundCount(); @@ -515,9 +508,9 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { ); } - function testFuzzEditRound_revertsGivenRoundStartIsInThePast( - uint roundStartP_ - ) public { + function testEditRound_revertsGivenRoundStartIsInThePast(uint roundStartP_) + public + { testCreateRound(); uint64 roundId = fundingPot.getRoundCount(); @@ -553,9 +546,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { ); } - function testFuzzEditRound_revertsGivenRoundEndTimeAndCapAreBothZero() - public - { + function testEditRound_revertsGivenRoundEndTimeAndCapAreBothZero() public { testCreateRound(); uint64 roundId = fundingPot.getRoundCount(); @@ -591,7 +582,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { ); } - function testFuzzEditRound_revertsGivenRoundEndTimeIsBeforeRoundStart( + function testEditRound_revertsGivenRoundEndTimeIsBeforeRoundStart( uint roundEnd_ ) public { testCreateRound(); @@ -628,8 +619,9 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { ); } - function testFuzzEditRound_revertsGivenHookContractIsSetButHookFunctionIsEmpty( - ) public { + function testEditRound_revertsGivenHookContractIsSetButHookFunctionIsEmpty() + public + { testCreateRound(); uint64 roundId = fundingPot.getRoundCount(); @@ -665,8 +657,9 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { ); } - function testFuzzEditRound_revertsGivenHookFunctionIsSetButHookContractIsEmpty( - ) public { + function testEditRound_revertsGivenHookFunctionIsSetButHookContractIsEmpty() + public + { testCreateRound(); uint64 roundId = fundingPot.getRoundCount(); @@ -760,33 +753,193 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { assertEq(globalAccumulativeCaps, globalAccumulativeCaps_); } - // // ------------------------------------------------------------------------- - // // Test: Internal Functions - - // // Helper Functions - - // // @notice Creates a default funding round - // // @dev make the parameters fuzzable @TODO Jeffrey - // function _generateFundingRoundParams( - // uint roundStart_, - // uint roundEnd_, - // uint roundCap_, - // address hookContract_, - // bytes memory hookFunction_, - // bool closureMechanism_, - // bool globalAccumulativeCaps_ - // ) internal returns (ILM_PC_FundingPot_v1.Round memory) { - // ILM_PC_FundingPot_v1.Round memory round = ILM_PC_FundingPot_v1.Round({ - // roundStart: roundStart_, - // roundEnd: roundEnd_, - // roundCap: roundCap_, - // hookContract: hookContract_, - // hookFunction: hookFunction_, - // closureMechanism: closureMechanism_, - // globalAccumulativeCaps: globalAccumulativeCaps_ - // }); - // return round; - // } + /* Test setAccessCriteria() + ├── Given user does not have FUNDING_POT_ADMIN_ROLE + │ └── When user attempts to set access criteria + │ └── Then it should revert + ├── Given round does not exist + │ └── When user attempts to set access criteria + │ └── Then it should revert + ├── Given round is active + │ └── When user attempts to set access criteria + │ └── Then it should revert + ├── Given AccessCriteriaId is NFT and nftContract is 0x0 + │ └── When user attempts to set access criteria + │ └── Then it should revert + ├── Given AccessCriteriaId is MERKLE and merkleRoot is 0x0 + │ └── When user attempts to set access criteria + │ └── Then it should revert + ├── Given AccessCriteriaId is LIST and allowedAddresses is empty + │ └── When user attempts to set access criteria + │ └── Then it should revert + └── Given all the valid parameters are provided + └── When user attempts to set access criteria + └── Then it should not revert + */ + + function testFuzzSetAccessCriteria_revertsGivenUserDoesNotHaveFundingPotAdminRole( + uint8 accessCriteriaEnum_, + address user_ + ) public { + vm.assume(accessCriteriaEnum_ >= 0 && accessCriteriaEnum_ <= 3); + vm.assume(user_ != address(0) && user_ != address(this)); + + testCreateRound(); + uint64 roundId = fundingPot.getRoundCount(); + uint8 accessId = 1; + + ILM_PC_FundingPot_v1.AccessCriteria memory accessCriteria = + _helper_createAccessCriteria(accessCriteriaEnum_); + + vm.startPrank(user_); + bytes32 roleId = _authorizer.generateRoleId( + address(fundingPot), fundingPot.FUNDING_POT_ADMIN_ROLE() + ); + vm.expectRevert( + abi.encodeWithSelector( + IModule_v1.Module__CallerNotAuthorized.selector, roleId, user_ + ) + ); + fundingPot.setAccessCriteriaForRound(roundId, accessId, accessCriteria); + } + + function testFuzzSetAccessCriteria_revertsGivenRoundDoesNotExist( + uint8 accessCriteriaEnum + ) public { + vm.assume(accessCriteriaEnum >= 0 && accessCriteriaEnum <= 3); + + uint64 roundId = fundingPot.getRoundCount(); + uint8 accessId = 1; + + ILM_PC_FundingPot_v1.AccessCriteria memory accessCriteria = + _helper_createAccessCriteria(accessCriteriaEnum); + + vm.expectRevert( + abi.encodeWithSelector( + ILM_PC_FundingPot_v1 + .Module__LM_PC_FundingPot__RoundNotCreated + .selector + ) + ); + fundingPot.setAccessCriteriaForRound(roundId, accessId, accessCriteria); + } + + function testFuzzSetAccessCriteria_revertsGivenRoundIsActive( + uint8 accessCriteriaEnum + ) public { + vm.assume(accessCriteriaEnum >= 0 && accessCriteriaEnum <= 3); + testCreateRound(); + uint64 roundId = fundingPot.getRoundCount(); + uint8 accessId = 1; + + ILM_PC_FundingPot_v1.AccessCriteria memory accessCriteria = + _helper_createAccessCriteria(accessCriteriaEnum); + + (uint roundStart,,,,,,) = fundingPot.getRoundGenericParameters(roundId); + vm.warp(roundStart + 1); + vm.expectRevert( + abi.encodeWithSelector( + ILM_PC_FundingPot_v1 + .Module__LM_PC_FundingPot__RoundAlreadyStarted + .selector + ) + ); + fundingPot.setAccessCriteriaForRound(roundId, accessId, accessCriteria); + } + + function testSetAccessCriteria_revertsGivenAccessCriteriaIdIsNFTAndNftContractIsZero( + ) public { + uint8 accessCriteriaEnum = + uint8(ILM_PC_FundingPot_v1.AccessCriteriaId.NFT); + testCreateRound(); + uint64 roundId = fundingPot.getRoundCount(); + uint8 accessId = 1; + + ILM_PC_FundingPot_v1.AccessCriteria memory accessCriteria = + _helper_createAccessCriteria(accessCriteriaEnum); + accessCriteria.nftContract = address(0); + + vm.expectRevert( + abi.encodeWithSelector( + ILM_PC_FundingPot_v1 + .Module__LM_PC_FundingPot__MissingRequiredAccessCriteriaData + .selector + ) + ); + fundingPot.setAccessCriteriaForRound(roundId, accessId, accessCriteria); + } + + function testSetAccessCriteria_revertsGivenAccessCriteriaIdIsMerkleAndMerkleRootIsZero( + ) public { + uint8 accessCriteriaEnum = + uint8(ILM_PC_FundingPot_v1.AccessCriteriaId.MERKLE); + testCreateRound(); + uint64 roundId = fundingPot.getRoundCount(); + uint8 accessId = 1; + + ILM_PC_FundingPot_v1.AccessCriteria memory accessCriteria = + _helper_createAccessCriteria(accessCriteriaEnum); + accessCriteria.merkleRoot = bytes32(uint(0x0)); + + vm.expectRevert( + abi.encodeWithSelector( + ILM_PC_FundingPot_v1 + .Module__LM_PC_FundingPot__MissingRequiredAccessCriteriaData + .selector + ) + ); + fundingPot.setAccessCriteriaForRound(roundId, accessId, accessCriteria); + } + + function testSetAccessCriteria_revertsGivenAccessCriteriaIdIsListAndAllowedAddressesIsEmpty( + ) public { + uint8 accessCriteriaEnum = + uint8(ILM_PC_FundingPot_v1.AccessCriteriaId.LIST); + testCreateRound(); + uint64 roundId = fundingPot.getRoundCount(); + uint8 accessId = 1; + + ILM_PC_FundingPot_v1.AccessCriteria memory accessCriteria = + _helper_createAccessCriteria(accessCriteriaEnum); + accessCriteria.allowedAddresses = new address[](0); + + vm.expectRevert( + abi.encodeWithSelector( + ILM_PC_FundingPot_v1 + .Module__LM_PC_FundingPot__MissingRequiredAccessCriteriaData + .selector + ) + ); + fundingPot.setAccessCriteriaForRound(roundId, accessId, accessCriteria); + } + + function testFuzzSetAccessCriteria(uint8 accessCriteriaEnum) public { + vm.assume(accessCriteriaEnum >= 0 && accessCriteriaEnum <= 3); + testCreateRound(); + uint64 roundId = fundingPot.getRoundCount(); + uint8 accessId = 1; + + ILM_PC_FundingPot_v1.AccessCriteria memory accessCriteria = + _helper_createAccessCriteria(accessCriteriaEnum); + + fundingPot.setAccessCriteriaForRound(roundId, accessId, accessCriteria); + + ( + address nftContract, + bytes32 merkleRoot, + address[] memory allowedAddresses + ) = fundingPot.getRoundAccessCriteria(roundId, accessId); + + assertEq(nftContract, accessCriteria.nftContract); + assertEq(merkleRoot, accessCriteria.merkleRoot); + assertEq(allowedAddresses, accessCriteria.allowedAddresses); + } + + // ------------------------------------------------------------------------- + // Test: Internal Functions + + // ------------------------------------------------------------------------- + // Helper Functions // @notice Creates a default funding round function _helper_createDefaultFundingRound() @@ -879,4 +1032,62 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { globalAccumulativeCaps ); } + + function _helper_createAccessCriteria(uint8 accessCriteriaEnum) + internal + returns (ILM_PC_FundingPot_v1.AccessCriteria memory) + { + { + if ( + accessCriteriaEnum + == uint8(ILM_PC_FundingPot_v1.AccessCriteriaId.OPEN) + ) { + return ILM_PC_FundingPot_v1.AccessCriteria( + ILM_PC_FundingPot_v1.AccessCriteriaId.OPEN, + address(0x0), + bytes32(uint(0x0)), + new address[](0) + ); + } else if ( + accessCriteriaEnum + == uint8(ILM_PC_FundingPot_v1.AccessCriteriaId.NFT) + ) { + address nftContract = address(0x1); + + return ILM_PC_FundingPot_v1.AccessCriteria( + ILM_PC_FundingPot_v1.AccessCriteriaId.NFT, + nftContract, + bytes32(uint(0x0)), + new address[](0) + ); + } else if ( + accessCriteriaEnum + == uint8(ILM_PC_FundingPot_v1.AccessCriteriaId.MERKLE) + ) { + bytes32 merkleRoot = bytes32(uint(0x1)); + + return ILM_PC_FundingPot_v1.AccessCriteria( + ILM_PC_FundingPot_v1.AccessCriteriaId.MERKLE, + address(0x0), + merkleRoot, + new address[](0) + ); + } else if ( + accessCriteriaEnum + == uint8(ILM_PC_FundingPot_v1.AccessCriteriaId.LIST) + ) { + address[] memory allowedAddresses = new address[](3); + allowedAddresses[0] = address(0x1); + allowedAddresses[1] = address(0x2); + allowedAddresses[2] = address(0x3); + + return ILM_PC_FundingPot_v1.AccessCriteria( + ILM_PC_FundingPot_v1.AccessCriteriaId.LIST, + address(0x0), + bytes32(uint(0x0)), + allowedAddresses + ); + } + } + } } From 72a3f1801f29e0baffbef024ee106219d8634963 Mon Sep 17 00:00:00 2001 From: Zuhaib Mohammed Date: Tue, 25 Mar 2025 12:31:36 +0530 Subject: [PATCH 023/130] fix: inverter standard changes --- .../logicModule/LM_PC_FundingPot_v1.sol | 36 +++++++++---------- .../interfaces/ILM_PC_FundingPot_v1.sol | 7 ++-- 2 files changed, 22 insertions(+), 21 deletions(-) diff --git a/src/modules/logicModule/LM_PC_FundingPot_v1.sol b/src/modules/logicModule/LM_PC_FundingPot_v1.sol index 96490bb32..923e68b55 100644 --- a/src/modules/logicModule/LM_PC_FundingPot_v1.sol +++ b/src/modules/logicModule/LM_PC_FundingPot_v1.sol @@ -126,8 +126,6 @@ contract LM_PC_FundingPot_v1 is // Public - Getters /// @inheritdoc ILM_PC_FundingPot_v1 - /// @dev Returns the generic parameters of a round. - function getRoundGenericParameters(uint64 _roundId) external view @@ -153,6 +151,7 @@ contract LM_PC_FundingPot_v1 is ); } + /// @inheritdoc ILM_PC_FundingPot_v1 function getRoundAccessCriteria(uint64 _roundId, uint64 _id) external view @@ -228,16 +227,10 @@ contract LM_PC_FundingPot_v1 is bytes memory _hookFunction, bool _closureMechanism, bool _globalAccumulativeCaps - ) external onlyModuleRole(FUNDING_POT_ADMIN_ROLE) returns (bool) { + ) external onlyModuleRole(FUNDING_POT_ADMIN_ROLE) { Round storage round = rounds[_roundId]; - if (round.roundEnd == 0 && round.roundCap == 0) { - revert Module__LM_PC_FundingPot__RoundNotCreated(); - } - - if (block.timestamp > round.roundStart) { - revert Module__LM_PC_FundingPot__RoundAlreadyStarted(); - } + _validateEditRoundParameters(round); round.roundStart = _roundStart; round.roundEnd = _roundEnd; @@ -258,8 +251,6 @@ contract LM_PC_FundingPot_v1 is _closureMechanism, _globalAccumulativeCaps ); - - return true; } /// @inheritdoc ILM_PC_FundingPot_v1 @@ -270,13 +261,7 @@ contract LM_PC_FundingPot_v1 is ) external onlyModuleRole(FUNDING_POT_ADMIN_ROLE) { Round storage round = rounds[_roundId]; - if (round.roundEnd == 0 && round.roundCap == 0) { - revert Module__LM_PC_FundingPot__RoundNotCreated(); - } - - if (block.timestamp > round.roundStart) { - revert Module__LM_PC_FundingPot__RoundAlreadyStarted(); - } + _validateEditRoundParameters(round); if ( ( @@ -333,4 +318,17 @@ contract LM_PC_FundingPot_v1 is Module__LM_PC_FundingPot__HookContractRequiredWithHookFunction(); } } + + /// @notice Validates the round parameters. + /// @param round The round to validate. + /// @dev Reverts if the round parameters are invalid. + function _validateEditRoundParameters(Round storage round) internal view { + if (round.roundEnd == 0 && round.roundCap == 0) { + revert Module__LM_PC_FundingPot__RoundNotCreated(); + } + + if (block.timestamp > round.roundStart) { + revert Module__LM_PC_FundingPot__RoundAlreadyStarted(); + } + } } diff --git a/src/modules/logicModule/interfaces/ILM_PC_FundingPot_v1.sol b/src/modules/logicModule/interfaces/ILM_PC_FundingPot_v1.sol index 9dbbad8b5..4b05c982a 100644 --- a/src/modules/logicModule/interfaces/ILM_PC_FundingPot_v1.sol +++ b/src/modules/logicModule/interfaces/ILM_PC_FundingPot_v1.sol @@ -214,7 +214,6 @@ interface ILM_PC_FundingPot_v1 is IERC20PaymentClientBase_v2 { /// @param _hookFunction New encoded function call /// @param _closureMechanism New closure mechanism setting /// @param _globalAccumulativeCaps New global accumulative caps setting - /// @return True if edit was successful function editRound( uint64 _roundId, uint _roundStart, @@ -224,9 +223,13 @@ interface ILM_PC_FundingPot_v1 is IERC20PaymentClientBase_v2 { bytes memory _hookFunction, bool _closureMechanism, bool _globalAccumulativeCaps - ) external returns (bool); + ) external; /// @notice Set Access Control Check + /// @dev Only callable by funding pot admin and only before the round has started + /// @param _roundId ID of the round + /// @param _accessId ID of the access criteria + /// @param _accessCriteria Access criteria to set function setAccessCriteriaForRound( uint64 _roundId, uint8 _accessId, From e683568241193a62d1e277390f15c9ecf372c147 Mon Sep 17 00:00:00 2001 From: Zuhaib Mohammed Date: Tue, 25 Mar 2025 15:39:27 +0530 Subject: [PATCH 024/130] fix: add bool to getRoundAccessCriteria() --- .../logicModule/LM_PC_FundingPot_v1.sol | 25 +++++++++++++------ .../interfaces/ILM_PC_FundingPot_v1.sol | 4 ++- .../logicModule/LM_PC_FundingPot_v1.t.sol | 2 ++ 3 files changed, 23 insertions(+), 8 deletions(-) diff --git a/src/modules/logicModule/LM_PC_FundingPot_v1.sol b/src/modules/logicModule/LM_PC_FundingPot_v1.sol index 923e68b55..6286603a7 100644 --- a/src/modules/logicModule/LM_PC_FundingPot_v1.sol +++ b/src/modules/logicModule/LM_PC_FundingPot_v1.sol @@ -152,10 +152,11 @@ contract LM_PC_FundingPot_v1 is } /// @inheritdoc ILM_PC_FundingPot_v1 - function getRoundAccessCriteria(uint64 _roundId, uint64 _id) + function getRoundAccessCriteria(uint64 _roundId, uint8 _id) external view returns ( + bool isOpen, address nftContract, bytes32 merkleRoot, address[] memory allowedAddresses @@ -163,12 +164,22 @@ contract LM_PC_FundingPot_v1 is { Round storage round = rounds[_roundId]; AccessCriteria storage accessCriteria = round.accessCriterias[_id]; - //@todo : Waht if the round is open? add a bool? check what can be done !!! - return ( - accessCriteria.nftContract, - accessCriteria.merkleRoot, - accessCriteria.allowedAddresses - ); + + if (accessCriteria.accessCriteriaId == AccessCriteriaId.OPEN) { + return ( + true, + accessCriteria.nftContract, + accessCriteria.merkleRoot, + accessCriteria.allowedAddresses + ); + } else { + return ( + false, + accessCriteria.nftContract, + accessCriteria.merkleRoot, + accessCriteria.allowedAddresses + ); + } } /// @inheritdoc ILM_PC_FundingPot_v1 diff --git a/src/modules/logicModule/interfaces/ILM_PC_FundingPot_v1.sol b/src/modules/logicModule/interfaces/ILM_PC_FundingPot_v1.sol index 4b05c982a..93eea217b 100644 --- a/src/modules/logicModule/interfaces/ILM_PC_FundingPot_v1.sol +++ b/src/modules/logicModule/interfaces/ILM_PC_FundingPot_v1.sol @@ -165,13 +165,15 @@ interface ILM_PC_FundingPot_v1 is IERC20PaymentClientBase_v2 { /// @notice Retrieves the access criteria for a specific funding round. /// @param _roundId The unique identifier of the round to retrieve. /// @param _id The identifier of the access criteria to retrieve. + /// @return isOpen Whether the access criteria is open /// @return nftContract The address of the NFT contract used for access control /// @return merkleRoot The merkle root used for access verification /// @return allowedAddresses The list of explicitly allowed addresses - function getRoundAccessCriteria(uint64 _roundId, uint64 _id) + function getRoundAccessCriteria(uint64 _roundId, uint8 _id) external view returns ( + bool isOpen, address nftContract, bytes32 merkleRoot, address[] memory allowedAddresses diff --git a/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol b/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol index 19869a2f5..4142fb9e4 100644 --- a/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol +++ b/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol @@ -925,11 +925,13 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { fundingPot.setAccessCriteriaForRound(roundId, accessId, accessCriteria); ( + bool isOpen, address nftContract, bytes32 merkleRoot, address[] memory allowedAddresses ) = fundingPot.getRoundAccessCriteria(roundId, accessId); + assertEq(isOpen, accessCriteriaEnum == 0); assertEq(nftContract, accessCriteria.nftContract); assertEq(merkleRoot, accessCriteria.merkleRoot); assertEq(allowedAddresses, accessCriteria.allowedAddresses); From 48cedee4c133dce6165af01e00e8e06fedd1f332 Mon Sep 17 00:00:00 2001 From: JeffreyJoel Date: Tue, 25 Mar 2025 19:51:30 +0100 Subject: [PATCH 025/130] fix: inverter standard changes --- .../logicModule/LM_PC_FundingPot_v1.sol | 146 +++++------ .../interfaces/ILM_PC_FundingPot_v1.sol | 244 +++++++++--------- 2 files changed, 196 insertions(+), 194 deletions(-) diff --git a/src/modules/logicModule/LM_PC_FundingPot_v1.sol b/src/modules/logicModule/LM_PC_FundingPot_v1.sol index 6286603a7..280fbab49 100644 --- a/src/modules/logicModule/LM_PC_FundingPot_v1.sol +++ b/src/modules/logicModule/LM_PC_FundingPot_v1.sol @@ -126,7 +126,7 @@ contract LM_PC_FundingPot_v1 is // Public - Getters /// @inheritdoc ILM_PC_FundingPot_v1 - function getRoundGenericParameters(uint64 _roundId) + function getRoundGenericParameters(uint64 roundId_) external view returns ( @@ -139,7 +139,7 @@ contract LM_PC_FundingPot_v1 is bool globalAccumulativeCaps ) { - Round storage round = rounds[_roundId]; + Round storage round = rounds[roundId_]; return ( round.roundStart, round.roundEnd, @@ -152,7 +152,7 @@ contract LM_PC_FundingPot_v1 is } /// @inheritdoc ILM_PC_FundingPot_v1 - function getRoundAccessCriteria(uint64 _roundId, uint8 _id) + function getRoundAccessCriteria(uint64 roundId_, uint8 id_) external view returns ( @@ -162,8 +162,8 @@ contract LM_PC_FundingPot_v1 is address[] memory allowedAddresses ) { - Round storage round = rounds[_roundId]; - AccessCriteria storage accessCriteria = round.accessCriterias[_id]; + Round storage round = rounds[roundId_]; + AccessCriteria storage accessCriteria = round.accessCriterias[id_]; if (accessCriteria.accessCriteriaId == AccessCriteriaId.OPEN) { return ( @@ -192,37 +192,37 @@ contract LM_PC_FundingPot_v1 is /// @inheritdoc ILM_PC_FundingPot_v1 function createRound( - uint _roundStart, - uint _roundEnd, - uint _roundCap, - address _hookContract, - bytes memory _hookFunction, - bool _closureMechanism, - bool _globalAccumulativeCaps + uint roundStart_, + uint roundEnd_, + uint roundCap_, + address hookContract_, + bytes memory hookFunction_, + bool closureMechanism_, + bool globalAccumulativeCaps_ ) external onlyModuleRole(FUNDING_POT_ADMIN_ROLE) returns (uint64) { nextRoundId++; uint64 roundId = nextRoundId; Round storage round = rounds[roundId]; - round.roundStart = _roundStart; - round.roundEnd = _roundEnd; - round.roundCap = _roundCap; - round.hookContract = _hookContract; - round.hookFunction = _hookFunction; - round.closureMechanism = _closureMechanism; - round.globalAccumulativeCaps = _globalAccumulativeCaps; + round.roundStart = roundStart_; + round.roundEnd = roundEnd_; + round.roundCap = roundCap_; + round.hookContract = hookContract_; + round.hookFunction = hookFunction_; + round.closureMechanism = closureMechanism_; + round.globalAccumulativeCaps = globalAccumulativeCaps_; _validateRoundParameters(round); emit RoundCreated( roundId, - _roundStart, - _roundEnd, - _roundCap, - _hookContract, - _closureMechanism, - _globalAccumulativeCaps + roundStart_, + roundEnd_, + roundCap_, + hookContract_, + closureMechanism_, + globalAccumulativeCaps_ ); return roundId; @@ -230,115 +230,117 @@ contract LM_PC_FundingPot_v1 is /// @inheritdoc ILM_PC_FundingPot_v1 function editRound( - uint64 _roundId, - uint _roundStart, - uint _roundEnd, - uint _roundCap, - address _hookContract, - bytes memory _hookFunction, - bool _closureMechanism, - bool _globalAccumulativeCaps + uint64 roundId_, + uint roundStart_, + uint roundEnd_, + uint roundCap_, + address hookContract_, + bytes memory hookFunction_, + bool closureMechanism_, + bool globalAccumulativeCaps_ ) external onlyModuleRole(FUNDING_POT_ADMIN_ROLE) { - Round storage round = rounds[_roundId]; + Round storage round = rounds[roundId_]; _validateEditRoundParameters(round); - round.roundStart = _roundStart; - round.roundEnd = _roundEnd; - round.roundCap = _roundCap; - round.hookContract = _hookContract; - round.hookFunction = _hookFunction; - round.closureMechanism = _closureMechanism; - round.globalAccumulativeCaps = _globalAccumulativeCaps; + round.roundStart = roundStart_; + round.roundEnd = roundEnd_; + round.roundCap = roundCap_; + round.hookContract = hookContract_; + round.hookFunction = hookFunction_; + round.closureMechanism = closureMechanism_; + round.globalAccumulativeCaps = globalAccumulativeCaps_; _validateRoundParameters(round); emit RoundEdited( - _roundId, - _roundStart, - _roundEnd, - _roundCap, - _hookContract, - _closureMechanism, - _globalAccumulativeCaps + roundId_, + roundStart_, + roundEnd_, + roundCap_, + hookContract_, + closureMechanism_, + globalAccumulativeCaps_ ); } /// @inheritdoc ILM_PC_FundingPot_v1 function setAccessCriteriaForRound( - uint64 _roundId, - uint8 _accessId, - AccessCriteria memory _accessCriteria + uint64 roundId_, + uint8 accessId_, + AccessCriteria memory accessCriteria_ ) external onlyModuleRole(FUNDING_POT_ADMIN_ROLE) { - Round storage round = rounds[_roundId]; + Round storage round = rounds[roundId_]; _validateEditRoundParameters(round); if ( ( - _accessCriteria.accessCriteriaId == AccessCriteriaId.NFT - && _accessCriteria.nftContract == address(0) + accessCriteria_.accessCriteriaId == AccessCriteriaId.NFT + && accessCriteria_.nftContract == address(0) ) || ( - _accessCriteria.accessCriteriaId == AccessCriteriaId.MERKLE - && _accessCriteria.merkleRoot == bytes32("") + accessCriteria_.accessCriteriaId == AccessCriteriaId.MERKLE + && accessCriteria_.merkleRoot == bytes32("") ) || ( - _accessCriteria.accessCriteriaId == AccessCriteriaId.LIST - && _accessCriteria.allowedAddresses.length == 0 + accessCriteria_.accessCriteriaId == AccessCriteriaId.LIST + && accessCriteria_.allowedAddresses.length == 0 ) ) { revert Module__LM_PC_FundingPot__MissingRequiredAccessCriteriaData(); } - round.accessCriterias[_accessId] = _accessCriteria; - emit AccessCriteriaSet(_roundId, _accessId, _accessCriteria); + round.accessCriterias[accessId_] = accessCriteria_; + emit AccessCriteriaSet(roundId_, accessId_, accessCriteria_); } // ------------------------------------------------------------------------- // Internal /// @notice Validates the round parameters. - /// @param round The round to validate. + /// @param round_ The round to validate. /// @dev Reverts if the round parameters are invalid. - function _validateRoundParameters(Round storage round) internal view { + function _validateRoundParameters(Round storage round_) internal view { // Validate round start time is in the future // @note: The below condition wont allow _roundStart == block.timestamp - if (round.roundStart <= block.timestamp) { + if (round_.roundStart <= block.timestamp) { revert Module__LM_PC_FundingPot__RoundStartMustBeInFuture(); } // Validate that either end time or cap is set - if (round.roundEnd == 0 || round.roundCap == 0) { + if (round_.roundEnd == 0 || round_.roundCap == 0) { revert Module__LM_PC_FundingPot__RoundMustHaveEndTimeOrCap(); } // If end time is set, validate it's after start time - if (round.roundEnd > 0 && round.roundEnd <= round.roundStart) { + if (round_.roundEnd > 0 && round_.roundEnd <= round_.roundStart) { revert Module__LM_PC_FundingPot__RoundEndMustBeAfterStart(); } // Validate hook contract and function consistency - if (round.hookContract != address(0) && round.hookFunction.length == 0) - { + if ( + round_.hookContract != address(0) && round_.hookFunction.length == 0 + ) { revert Module__LM_PC_FundingPot__HookFunctionRequiredWithHookContract(); } - if (round.hookContract == address(0) && round.hookFunction.length > 0) { + if (round_.hookContract == address(0) && round_.hookFunction.length > 0) + { revert Module__LM_PC_FundingPot__HookContractRequiredWithHookFunction(); } } /// @notice Validates the round parameters. - /// @param round The round to validate. + /// @param round_ The round to validate. /// @dev Reverts if the round parameters are invalid. - function _validateEditRoundParameters(Round storage round) internal view { - if (round.roundEnd == 0 && round.roundCap == 0) { + function _validateEditRoundParameters(Round storage round_) internal view { + if (round_.roundEnd == 0 && round_.roundCap == 0) { revert Module__LM_PC_FundingPot__RoundNotCreated(); } - if (block.timestamp > round.roundStart) { + if (block.timestamp > round_.roundStart) { revert Module__LM_PC_FundingPot__RoundAlreadyStarted(); } } diff --git a/src/modules/logicModule/interfaces/ILM_PC_FundingPot_v1.sol b/src/modules/logicModule/interfaces/ILM_PC_FundingPot_v1.sol index 93eea217b..beb33022d 100644 --- a/src/modules/logicModule/interfaces/ILM_PC_FundingPot_v1.sol +++ b/src/modules/logicModule/interfaces/ILM_PC_FundingPot_v1.sol @@ -10,34 +10,34 @@ interface ILM_PC_FundingPot_v1 is IERC20PaymentClientBase_v2 { // Structs /// @notice Struct used to store information about a funding round. - /// @param roundStart Timestamp indicating when the round starts. - /// @param roundEnd Timestamp indicating when the round ends. If set to `0`, the round operates only based on `roundCap`. - /// @param roundCap Maximum contribution cap in collateral tokens. If set to `0`, the round operates only based on `roundEnd`. - /// @param hookContract Address of an optional hook contract to be called after round closure. - /// @param hookFunction Encoded function call to be executed on the `hookContract` after round closure. - /// @param closureMechanism Indicates whether the hook closure coincides with the contribution span end. - /// @param globalAccumulativeCaps Indicates whether contribution caps accumulate globally across rounds. - /// @param accessCriterias Mapping of access criteria IDs to their respective access criteria. + /// @param roundStart_ Timestamp indicating when the round starts. + /// @param roundEnd_ Timestamp indicating when the round ends. If set to `0`, the round operates only based on `roundCap`. + /// @param roundCap_ Maximum contribution cap in collateral tokens. If set to `0`, the round operates only based on `roundEnd`. + /// @param hookContract_ Address of an optional hook contract to be called after round closure. + /// @param hookFunction_ Encoded function call to be executed on the `hookContract` after round closure. + /// @param closureMechanism_ Indicates whether the hook closure coincides with the contribution span end. + /// @param globalAccumulativeCaps_ Indicates whether contribution caps accumulate globally across rounds. + /// @param accessCriterias_ Mapping of access criteria IDs to their respective access criteria. struct Round { - uint roundStart; - uint roundEnd; - uint roundCap; - address hookContract; - bytes hookFunction; - bool closureMechanism; - bool globalAccumulativeCaps; - mapping(uint64 id => AccessCriteria) accessCriterias; + uint roundStart_; + uint roundEnd_; + uint roundCap_; + address hookContract_; + bytes hookFunction_; + bool closureMechanism_; + bool globalAccumulativeCaps_; + mapping(uint64 id => AccessCriteria) accessCriterias_; } /// @notice Struct used to store information about a funding round's access criteria. - /// @param nftContract Address of the NFT contract. - /// @param merkleRoot Merkle root for the access criteria. - /// @param allowedAddresses Mapping of addresses to their access status. + /// @param nftContract_ Address of the NFT contract. + /// @param merkleRoot_ Merkle root for the access criteria. + /// @param allowedAddresses_ Mapping of addresses to their access status. struct AccessCriteria { AccessCriteriaId accessCriteriaId; - address nftContract; // NFT contract address (0x0 if unused) - bytes32 merkleRoot; // Merkle root (0x0 if unused) - address[] allowedAddresses; // Explicit allowlist + address nftContract_; // NFT contract address (0x0 if unused) + bytes32 merkleRoot_; // Merkle root (0x0 if unused) + address[] allowedAddresses_; // Explicit allowlist } // ------------------------------------------------------------------------- @@ -56,49 +56,49 @@ interface ILM_PC_FundingPot_v1 is IERC20PaymentClientBase_v2 { // Events /// @notice Emitted when a new round is created. - /// @dev This event signals the creation of a new round with specific parameters. - /// @param roundId The unique identifier for the round. - /// @param roundStart The timestamp when the round starts. - /// @param roundEnd The timestamp when the round ends. - /// @param roundCap The maximum allocation or cap for the round. - /// @param hookContract The address of an optional hook contract for custom logic. - /// @param closureMechanism A boolean indicating whether a specific closure mechanism is enabled. - /// @param globalAccumulativeCaps A boolean indicating whether global accumulative caps are enforced. + /// @dev This event signals the creation of a new round with specific parameters. + /// @param roundId_ The unique identifier for the round. + /// @param roundStart_ The timestamp when the round starts. + /// @param roundEnd_ The timestamp when the round ends. + /// @param roundCap_ The maximum allocation or cap for the round. + /// @param hookContract_ The address of an optional hook contract for custom logic. + /// @param closureMechanism_ A boolean indicating whether a specific closure mechanism is enabled. + /// @param globalAccumulativeCaps_ A boolean indicating whether global accumulative caps are enforced. event RoundCreated( - uint indexed roundId, - uint roundStart, - uint roundEnd, - uint roundCap, - address hookContract, - bool closureMechanism, - bool globalAccumulativeCaps + uint indexed roundId_, + uint roundStart_, + uint roundEnd_, + uint roundCap_, + address hookContract_, + bool closureMechanism_, + bool globalAccumulativeCaps_ ); /// @notice Emitted when an existing round is edited. - /// @dev This event signals modifications to an existing round's parameters. - /// @param roundId The unique identifier of the round being edited. - /// @param roundStart The updated timestamp for when the round starts. - /// @param roundEnd The updated timestamp for when the round ends. - /// @param roundCap The updated maximum allocation or cap for the round. - /// @param hookContract The address of an optional hook contract for custom logic. - /// @param closureMechanism A boolean indicating whether a specific closure mechanism is enabled. - /// @param globalAccumulativeCaps A boolean indicating whether global accumulative caps are enforced. + /// @dev This event signals modifications to an existing round's parameters. + /// @param roundId_ The unique identifier of the round being edited. + /// @param roundStart_ The updated timestamp for when the round starts. + /// @param roundEnd_ The updated timestamp for when the round ends. + /// @param roundCap_ The updated maximum allocation or cap for the round. + /// @param hookContract_ The address of an optional hook contract for custom logic. + /// @param closureMechanism_ A boolean indicating whether a specific closure mechanism is enabled. + /// @param globalAccumulativeCaps_ A boolean indicating whether global accumulative caps are enforced. event RoundEdited( - uint indexed roundId, - uint roundStart, - uint roundEnd, - uint roundCap, - address hookContract, - bool closureMechanism, - bool globalAccumulativeCaps + uint indexed roundId_, + uint roundStart_, + uint roundEnd_, + uint roundCap_, + address hookContract_, + bool closureMechanism_, + bool globalAccumulativeCaps_ ); /// @notice Emitted when access criteria is set for a round. - /// @param roundId The unique identifier of the round. - /// @param accessId The identifier of the access criteria. - /// @param accessCriteria The access criteria. + /// @param roundId_ The unique identifier of the round. + /// @param accessId_ The identifier of the access criteria. + /// @param accessCriteria_ The access criteria. event AccessCriteriaSet( - uint64 indexed roundId, uint8 accessId, AccessCriteria accessCriteria + uint64 indexed roundId_, uint8 accessId_, AccessCriteria accessCriteria_ ); // ------------------------------------------------------------------------- @@ -141,42 +141,42 @@ interface ILM_PC_FundingPot_v1 is IERC20PaymentClientBase_v2 { // Public - Getters /// @notice Retrieves the generic parameters of a specific funding round. - /// @param _roundId The unique identifier of the round to retrieve. - /// @return roundStart The timestamp when the round starts - /// @return roundEnd The timestamp when the round ends - /// @return roundCap The maximum contribution cap for the round - /// @return hookContract The address of the hook contract - /// @return hookFunction The encoded function call for the hook - /// @return closureMechanism Whether hook closure coincides with contribution span end - /// @return globalAccumulativeCaps Whether caps accumulate globally across rounds - function getRoundGenericParameters(uint64 _roundId) + /// @param roundId_ The unique identifier of the round to retrieve. + /// @return roundStart_ The timestamp when the round starts + /// @return roundEnd_ The timestamp when the round ends + /// @return roundCap_ The maximum contribution cap for the round + /// @return hookContract_ The address of the hook contract + /// @return hookFunction_ The encoded function call for the hook + /// @return closureMechanism_ Whether hook closure coincides with contribution span end + /// @return globalAccumulativeCaps_ Whether caps accumulate globally across rounds + function getRoundGenericParameters(uint64 roundId_) external view returns ( - uint roundStart, - uint roundEnd, - uint roundCap, - address hookContract, - bytes memory hookFunction, - bool closureMechanism, - bool globalAccumulativeCaps + uint roundStart_, + uint roundEnd_, + uint roundCap_, + address hookContract_, + bytes memory hookFunction_, + bool closureMechanism_, + bool globalAccumulativeCaps_ ); /// @notice Retrieves the access criteria for a specific funding round. - /// @param _roundId The unique identifier of the round to retrieve. - /// @param _id The identifier of the access criteria to retrieve. - /// @return isOpen Whether the access criteria is open - /// @return nftContract The address of the NFT contract used for access control - /// @return merkleRoot The merkle root used for access verification - /// @return allowedAddresses The list of explicitly allowed addresses - function getRoundAccessCriteria(uint64 _roundId, uint8 _id) + /// @param roundId_ The unique identifier of the round to retrieve. + /// @param id_ The identifier of the access criteria to retrieve. + /// @return isOpen_ Whether the access criteria is open + /// @return nftContract_ The address of the NFT contract used for access control + /// @return merkleRoot_ The merkle root used for access verification + /// @return allowedAddresses_ The list of explicitly allowed addresses + function getRoundAccessCriteria(uint64 roundId_, uint8 id_) external view returns ( - bool isOpen, - address nftContract, - bytes32 merkleRoot, - address[] memory allowedAddresses + bool isOpen_, + address nftContract_, + bytes32 merkleRoot_, + address[] memory allowedAddresses_ ); /// @notice Retrieves the total number of funding rounds. @@ -187,54 +187,54 @@ interface ILM_PC_FundingPot_v1 is IERC20PaymentClientBase_v2 { // Public - Mutating /// @notice Creates a new funding round - /// @dev Only callable by funding pot admin - /// @param _roundStart Start timestamp for the round - /// @param _roundEnd End timestamp for the round (0 if using roundCap only) - /// @param _roundCap Maximum contribution cap in collateral tokens (0 if using roundEnd only) - /// @param _hookContract Address of contract to call after round closure - /// @param _hookFunction Encoded function call for the hook - /// @param _closureMechanism Whether hook closure coincides with contribution span end - /// @param _globalAccumulativeCaps Whether caps accumulate globally + /// @dev Only callable by funding pot admin + /// @param roundStart_ Start timestamp for the round + /// @param roundEnd_ End timestamp for the round (0 if using roundCap only) + /// @param roundCap_ Maximum contribution cap in collateral tokens (0 if using roundEnd only) + /// @param hookContract_ Address of contract to call after round closure + /// @param hookFunction_ Encoded function call for the hook + /// @param closureMechanism_ Whether hook closure coincides with contribution span end + /// @param globalAccumulativeCaps_ Whether caps accumulate globally /// @return The ID of the newly created round function createRound( - uint _roundStart, - uint _roundEnd, - uint _roundCap, - address _hookContract, - bytes memory _hookFunction, - bool _closureMechanism, - bool _globalAccumulativeCaps + uint roundStart_, + uint roundEnd_, + uint roundCap_, + address hookContract_, + bytes memory hookFunction_, + bool closureMechanism_, + bool globalAccumulativeCaps_ ) external returns (uint64); /// @notice Edits an existing funding round - /// @dev Only callable by funding pot admin and only before the round has started - /// @param _roundId ID of the round to edit - /// @param _roundStart New start timestamp - /// @param _roundEnd New end timestamp - /// @param _roundCap New maximum contribution cap - /// @param _hookContract New hook contract address - /// @param _hookFunction New encoded function call - /// @param _closureMechanism New closure mechanism setting - /// @param _globalAccumulativeCaps New global accumulative caps setting + /// @dev Only callable by funding pot admin and only before the round has started + /// @param roundId_ ID of the round to edit + /// @param roundStart_ New start timestamp + /// @param roundEnd_ New end timestamp + /// @param roundCap_ New maximum contribution cap + /// @param hookContract_ New hook contract address + /// @param hookFunction_ New encoded function call + /// @param closureMechanism_ New closure mechanism setting + /// @param globalAccumulativeCaps_ New global accumulative caps setting function editRound( - uint64 _roundId, - uint _roundStart, - uint _roundEnd, - uint _roundCap, - address _hookContract, - bytes memory _hookFunction, - bool _closureMechanism, - bool _globalAccumulativeCaps + uint64 roundId_, + uint roundStart_, + uint roundEnd_, + uint roundCap_, + address hookContract_, + bytes memory hookFunction_, + bool closureMechanism_, + bool globalAccumulativeCaps_ ) external; /// @notice Set Access Control Check - /// @dev Only callable by funding pot admin and only before the round has started - /// @param _roundId ID of the round - /// @param _accessId ID of the access criteria - /// @param _accessCriteria Access criteria to set + /// @dev Only callable by funding pot admin and only before the round has started + /// @param roundId_ ID of the round + /// @param accessId_ ID of the access criteria + /// @param accessCriteria_ Access criteria to set function setAccessCriteriaForRound( - uint64 _roundId, - uint8 _accessId, - AccessCriteria memory _accessCriteria + uint64 roundId_, + uint8 accessId_, + AccessCriteria memory accessCriteria_ ) external; } From 3179ce5babbd109fc54e14e028f7614acd49a62c Mon Sep 17 00:00:00 2001 From: JeffreyJoel Date: Tue, 25 Mar 2025 19:57:28 +0100 Subject: [PATCH 026/130] fix: inverter standard changes --- .../interfaces/ILM_PC_FundingPot_v1.sol | 44 +++++++++---------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/src/modules/logicModule/interfaces/ILM_PC_FundingPot_v1.sol b/src/modules/logicModule/interfaces/ILM_PC_FundingPot_v1.sol index beb33022d..4a92e0796 100644 --- a/src/modules/logicModule/interfaces/ILM_PC_FundingPot_v1.sol +++ b/src/modules/logicModule/interfaces/ILM_PC_FundingPot_v1.sol @@ -10,34 +10,34 @@ interface ILM_PC_FundingPot_v1 is IERC20PaymentClientBase_v2 { // Structs /// @notice Struct used to store information about a funding round. - /// @param roundStart_ Timestamp indicating when the round starts. - /// @param roundEnd_ Timestamp indicating when the round ends. If set to `0`, the round operates only based on `roundCap`. - /// @param roundCap_ Maximum contribution cap in collateral tokens. If set to `0`, the round operates only based on `roundEnd`. - /// @param hookContract_ Address of an optional hook contract to be called after round closure. - /// @param hookFunction_ Encoded function call to be executed on the `hookContract` after round closure. - /// @param closureMechanism_ Indicates whether the hook closure coincides with the contribution span end. - /// @param globalAccumulativeCaps_ Indicates whether contribution caps accumulate globally across rounds. - /// @param accessCriterias_ Mapping of access criteria IDs to their respective access criteria. + /// @param roundStart Timestamp indicating when the round starts. + /// @param roundEnd Timestamp indicating when the round ends. If set to `0`, the round operates only based on `roundCap`. + /// @param roundCap Maximum contribution cap in collateral tokens. If set to `0`, the round operates only based on `roundEnd`. + /// @param hookContract Address of an optional hook contract to be called after round closure. + /// @param hookFunction Encoded function call to be executed on the `hookContract` after round closure. + /// @param closureMechanism Indicates whether the hook closure coincides with the contribution span end. + /// @param globalAccumulativeCaps Indicates whether contribution caps accumulate globally across rounds. + /// @param accessCriterias Mapping of access criteria IDs to their respective access criteria. struct Round { - uint roundStart_; - uint roundEnd_; - uint roundCap_; - address hookContract_; - bytes hookFunction_; - bool closureMechanism_; - bool globalAccumulativeCaps_; - mapping(uint64 id => AccessCriteria) accessCriterias_; + uint roundStart; + uint roundEnd; + uint roundCap; + address hookContract; + bytes hookFunction; + bool closureMechanism; + bool globalAccumulativeCaps; + mapping(uint64 id => AccessCriteria) accessCriterias; } /// @notice Struct used to store information about a funding round's access criteria. - /// @param nftContract_ Address of the NFT contract. - /// @param merkleRoot_ Merkle root for the access criteria. - /// @param allowedAddresses_ Mapping of addresses to their access status. + /// @param nftContract Address of the NFT contract. + /// @param merkleRoot Merkle root for the access criteria. + /// @param allowedAddresses Mapping of addresses to their access status. struct AccessCriteria { AccessCriteriaId accessCriteriaId; - address nftContract_; // NFT contract address (0x0 if unused) - bytes32 merkleRoot_; // Merkle root (0x0 if unused) - address[] allowedAddresses_; // Explicit allowlist + address nftContract; // NFT contract address (0x0 if unused) + bytes32 merkleRoot; // Merkle root (0x0 if unused) + address[] allowedAddresses; // Explicit allowlist } // ------------------------------------------------------------------------- From 9c8e42a168f0101b8ccea1f7063664c6a8400f46 Mon Sep 17 00:00:00 2001 From: Zuhaib Mohammed Date: Sat, 29 Mar 2025 06:26:38 +0530 Subject: [PATCH 027/130] fix: PR Review Fix#1 --- .../logicModule/LM_PC_FundingPot_v1.sol | 25 ++-- .../interfaces/ILM_PC_FundingPot_v1.sol | 100 ++++++++------- .../logicModule/LM_PC_FundingPot_v1.t.sol | 115 +++++++++--------- 3 files changed, 119 insertions(+), 121 deletions(-) diff --git a/src/modules/logicModule/LM_PC_FundingPot_v1.sol b/src/modules/logicModule/LM_PC_FundingPot_v1.sol index 280fbab49..0a0ad34a5 100644 --- a/src/modules/logicModule/LM_PC_FundingPot_v1.sol +++ b/src/modules/logicModule/LM_PC_FundingPot_v1.sol @@ -82,7 +82,6 @@ contract LM_PC_FundingPot_v1 is uint8 internal constant FLAG_END = 3; // ------------------------------------------------------------------------- - // State /// @notice Stores all funding rounds by their unique ID. @@ -135,7 +134,7 @@ contract LM_PC_FundingPot_v1 is uint roundCap, address hookContract, bytes memory hookFunction, - bool closureMechanism, + bool autoClosure, bool globalAccumulativeCaps ) { @@ -146,7 +145,7 @@ contract LM_PC_FundingPot_v1 is round.roundCap, round.hookContract, round.hookFunction, - round.closureMechanism, + round.autoClosure, round.globalAccumulativeCaps ); } @@ -165,7 +164,7 @@ contract LM_PC_FundingPot_v1 is Round storage round = rounds[roundId_]; AccessCriteria storage accessCriteria = round.accessCriterias[id_]; - if (accessCriteria.accessCriteriaId == AccessCriteriaId.OPEN) { + if (accessCriteria.accessCriteriaType == AccessCriteriaType.OPEN) { return ( true, accessCriteria.nftContract, @@ -197,7 +196,7 @@ contract LM_PC_FundingPot_v1 is uint roundCap_, address hookContract_, bytes memory hookFunction_, - bool closureMechanism_, + bool autoClosure_, bool globalAccumulativeCaps_ ) external onlyModuleRole(FUNDING_POT_ADMIN_ROLE) returns (uint64) { nextRoundId++; @@ -210,7 +209,7 @@ contract LM_PC_FundingPot_v1 is round.roundCap = roundCap_; round.hookContract = hookContract_; round.hookFunction = hookFunction_; - round.closureMechanism = closureMechanism_; + round.autoClosure = autoClosure_; round.globalAccumulativeCaps = globalAccumulativeCaps_; _validateRoundParameters(round); @@ -221,7 +220,7 @@ contract LM_PC_FundingPot_v1 is roundEnd_, roundCap_, hookContract_, - closureMechanism_, + autoClosure_, globalAccumulativeCaps_ ); @@ -236,7 +235,7 @@ contract LM_PC_FundingPot_v1 is uint roundCap_, address hookContract_, bytes memory hookFunction_, - bool closureMechanism_, + bool autoClosure_, bool globalAccumulativeCaps_ ) external onlyModuleRole(FUNDING_POT_ADMIN_ROLE) { Round storage round = rounds[roundId_]; @@ -248,7 +247,7 @@ contract LM_PC_FundingPot_v1 is round.roundCap = roundCap_; round.hookContract = hookContract_; round.hookFunction = hookFunction_; - round.closureMechanism = closureMechanism_; + round.autoClosure = autoClosure_; round.globalAccumulativeCaps = globalAccumulativeCaps_; _validateRoundParameters(round); @@ -259,7 +258,7 @@ contract LM_PC_FundingPot_v1 is roundEnd_, roundCap_, hookContract_, - closureMechanism_, + autoClosure_, globalAccumulativeCaps_ ); } @@ -276,15 +275,15 @@ contract LM_PC_FundingPot_v1 is if ( ( - accessCriteria_.accessCriteriaId == AccessCriteriaId.NFT + accessCriteria_.accessCriteriaType == AccessCriteriaType.NFT && accessCriteria_.nftContract == address(0) ) || ( - accessCriteria_.accessCriteriaId == AccessCriteriaId.MERKLE + accessCriteria_.accessCriteriaType == AccessCriteriaType.MERKLE && accessCriteria_.merkleRoot == bytes32("") ) || ( - accessCriteria_.accessCriteriaId == AccessCriteriaId.LIST + accessCriteria_.accessCriteriaType == AccessCriteriaType.LIST && accessCriteria_.allowedAddresses.length == 0 ) ) { diff --git a/src/modules/logicModule/interfaces/ILM_PC_FundingPot_v1.sol b/src/modules/logicModule/interfaces/ILM_PC_FundingPot_v1.sol index 4a92e0796..92c14e24f 100644 --- a/src/modules/logicModule/interfaces/ILM_PC_FundingPot_v1.sol +++ b/src/modules/logicModule/interfaces/ILM_PC_FundingPot_v1.sol @@ -6,7 +6,7 @@ import {IERC20PaymentClientBase_v2} from "@lm/interfaces/IERC20PaymentClientBase_v2.sol"; interface ILM_PC_FundingPot_v1 is IERC20PaymentClientBase_v2 { - //-------------------------------------------------------------------------- + // -------------------------------------------------------------------------- // Structs /// @notice Struct used to store information about a funding round. @@ -15,7 +15,7 @@ interface ILM_PC_FundingPot_v1 is IERC20PaymentClientBase_v2 { /// @param roundCap Maximum contribution cap in collateral tokens. If set to `0`, the round operates only based on `roundEnd`. /// @param hookContract Address of an optional hook contract to be called after round closure. /// @param hookFunction Encoded function call to be executed on the `hookContract` after round closure. - /// @param closureMechanism Indicates whether the hook closure coincides with the contribution span end. + /// @param autoClosure Indicates whether the hook closure coincides with the contribution span end. /// @param globalAccumulativeCaps Indicates whether contribution caps accumulate globally across rounds. /// @param accessCriterias Mapping of access criteria IDs to their respective access criteria. struct Round { @@ -24,17 +24,18 @@ interface ILM_PC_FundingPot_v1 is IERC20PaymentClientBase_v2 { uint roundCap; address hookContract; bytes hookFunction; - bool closureMechanism; + bool autoClosure; bool globalAccumulativeCaps; mapping(uint64 id => AccessCriteria) accessCriterias; } /// @notice Struct used to store information about a funding round's access criteria. + /// @param accessCriteriaType Type of access criteria. /// @param nftContract Address of the NFT contract. /// @param merkleRoot Merkle root for the access criteria. /// @param allowedAddresses Mapping of addresses to their access status. struct AccessCriteria { - AccessCriteriaId accessCriteriaId; + AccessCriteriaType accessCriteriaType; address nftContract; // NFT contract address (0x0 if unused) bytes32 merkleRoot; // Merkle root (0x0 if unused) address[] allowedAddresses; // Explicit allowlist @@ -44,7 +45,7 @@ interface ILM_PC_FundingPot_v1 is IERC20PaymentClientBase_v2 { // Enums /// @notice Enum used to identify the type of access criteria. - enum AccessCriteriaId { + enum AccessCriteriaType { OPEN, // 0 NFT, // 1 MERKLE, // 2 @@ -107,22 +108,19 @@ interface ILM_PC_FundingPot_v1 is IERC20PaymentClientBase_v2 { /// @notice Amount can not be zero. error Module__LM_PC_FundingPot__InvalidDepositAmount(); - /// @notice Round does not exist - error Module__LM_PC_FundingPot__RoundDoesNotExist(); - - /// @notice Round start time must be in the future + /// @notice Round start time must be in the future. error Module__LM_PC_FundingPot__RoundStartMustBeInFuture(); - /// @notice Round must have either an end time or a funding cap + /// @notice Round must have either an end time or a funding cap. error Module__LM_PC_FundingPot__RoundMustHaveEndTimeOrCap(); - /// @notice Round end time must be after round start time + /// @notice Round end time must be after round start time. error Module__LM_PC_FundingPot__RoundEndMustBeAfterStart(); - /// @notice Round has already started and cannot be modified + /// @notice Round has already started and cannot be modified. error Module__LM_PC_FundingPot__RoundAlreadyStarted(); - /// @notice Hook function is required when a hook contract is provided + /// @notice Hook function is required when a hook contract is provided. error Module__LM_PC_FundingPot__HookFunctionRequiredWithContract(); /// @notice Thrown when a hook contract is specified without a hook function. @@ -131,10 +129,10 @@ interface ILM_PC_FundingPot_v1 is IERC20PaymentClientBase_v2 { /// @notice Thrown when a hook function is specified without a hook contract. error Module__LM_PC_FundingPot__HookContractRequiredWithHookFunction(); - /// @notice Round does not exist + /// @notice Round does not exist. error Module__LM_PC_FundingPot__RoundNotCreated(); - /// @notice Incorrect access criteria + /// @notice Incorrect access criteria. error Module__LM_PC_FundingPot__MissingRequiredAccessCriteriaData(); // ------------------------------------------------------------------------- @@ -142,13 +140,13 @@ interface ILM_PC_FundingPot_v1 is IERC20PaymentClientBase_v2 { /// @notice Retrieves the generic parameters of a specific funding round. /// @param roundId_ The unique identifier of the round to retrieve. - /// @return roundStart_ The timestamp when the round starts - /// @return roundEnd_ The timestamp when the round ends - /// @return roundCap_ The maximum contribution cap for the round - /// @return hookContract_ The address of the hook contract - /// @return hookFunction_ The encoded function call for the hook - /// @return closureMechanism_ Whether hook closure coincides with contribution span end - /// @return globalAccumulativeCaps_ Whether caps accumulate globally across rounds + /// @return roundStart_ The timestamp when the round starts. + /// @return roundEnd_ The timestamp when the round ends. + /// @return roundCap_ The maximum contribution cap for the round. + /// @return hookContract_ The address of the hook contract. + /// @return hookFunction_ The encoded function call for the hook. + /// @return closureMechanism_ Whether hook closure coincides with contribution span end. + /// @return globalAccumulativeCaps_ Whether caps accumulate globally across rounds. function getRoundGenericParameters(uint64 roundId_) external view @@ -165,10 +163,10 @@ interface ILM_PC_FundingPot_v1 is IERC20PaymentClientBase_v2 { /// @notice Retrieves the access criteria for a specific funding round. /// @param roundId_ The unique identifier of the round to retrieve. /// @param id_ The identifier of the access criteria to retrieve. - /// @return isOpen_ Whether the access criteria is open - /// @return nftContract_ The address of the NFT contract used for access control - /// @return merkleRoot_ The merkle root used for access verification - /// @return allowedAddresses_ The list of explicitly allowed addresses + /// @return isOpen_ Whether the access criteria is open. + /// @return nftContract_ The address of the NFT contract used for access control. + /// @return merkleRoot_ The merkle root used for access verification. + /// @return allowedAddresses_ The list of explicitly allowed addresses. function getRoundAccessCriteria(uint64 roundId_, uint8 id_) external view @@ -186,16 +184,16 @@ interface ILM_PC_FundingPot_v1 is IERC20PaymentClientBase_v2 { // ------------------------------------------------------------------------- // Public - Mutating - /// @notice Creates a new funding round - /// @dev Only callable by funding pot admin - /// @param roundStart_ Start timestamp for the round - /// @param roundEnd_ End timestamp for the round (0 if using roundCap only) - /// @param roundCap_ Maximum contribution cap in collateral tokens (0 if using roundEnd only) - /// @param hookContract_ Address of contract to call after round closure - /// @param hookFunction_ Encoded function call for the hook - /// @param closureMechanism_ Whether hook closure coincides with contribution span end - /// @param globalAccumulativeCaps_ Whether caps accumulate globally - /// @return The ID of the newly created round + /// @notice Creates a new funding round. + /// @dev Only callable by funding pot admin. + /// @param roundStart_ Start timestamp for the round. + /// @param roundEnd_ End timestamp for the round (0 if using roundCap only). + /// @param roundCap_ Maximum contribution cap in collateral tokens (0 if using roundEnd only). + /// @param hookContract_ Address of contract to call after round closure. + /// @param hookFunction_ Encoded function call for the hook. + /// @param closureMechanism_ Whether hook closure coincides with contribution span end. + /// @param globalAccumulativeCaps_ Whether caps accumulate globally. + /// @return The ID of the newly created round. function createRound( uint roundStart_, uint roundEnd_, @@ -206,16 +204,16 @@ interface ILM_PC_FundingPot_v1 is IERC20PaymentClientBase_v2 { bool globalAccumulativeCaps_ ) external returns (uint64); - /// @notice Edits an existing funding round - /// @dev Only callable by funding pot admin and only before the round has started - /// @param roundId_ ID of the round to edit - /// @param roundStart_ New start timestamp - /// @param roundEnd_ New end timestamp - /// @param roundCap_ New maximum contribution cap - /// @param hookContract_ New hook contract address - /// @param hookFunction_ New encoded function call - /// @param closureMechanism_ New closure mechanism setting - /// @param globalAccumulativeCaps_ New global accumulative caps setting + /// @notice Edits an existing funding round. + /// @dev Only callable by funding pot admin and only before the round has started. + /// @param roundId_ ID of the round to edit. + /// @param roundStart_ New start timestamp. + /// @param roundEnd_ New end timestamp. + /// @param roundCap_ New maximum contribution cap. + /// @param hookContract_ New hook contract address. + /// @param hookFunction_ New encoded function call. + /// @param closureMechanism_ New closure mechanism setting. + /// @param globalAccumulativeCaps_ New global accumulative caps setting. function editRound( uint64 roundId_, uint roundStart_, @@ -227,11 +225,11 @@ interface ILM_PC_FundingPot_v1 is IERC20PaymentClientBase_v2 { bool globalAccumulativeCaps_ ) external; - /// @notice Set Access Control Check - /// @dev Only callable by funding pot admin and only before the round has started - /// @param roundId_ ID of the round - /// @param accessId_ ID of the access criteria - /// @param accessCriteria_ Access criteria to set + /// @notice Set Access Control Check. + /// @dev Only callable by funding pot admin and only before the round has started. + /// @param roundId_ ID of the round. + /// @param accessId_ ID of the access criteria. + /// @param accessCriteria_ Access criteria to set. function setAccessCriteriaForRound( uint64 roundId_, uint8 accessId_, diff --git a/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol b/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol index 4142fb9e4..6310698df 100644 --- a/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol +++ b/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol @@ -142,7 +142,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { uint roundCap, address hookContract, bytes memory hookFunction, - bool closureMechanism, + bool autoClosure, bool globalAccumulativeCaps ) = _helper_createDefaultFundingRound(); _helper_callCreateRound( @@ -151,7 +151,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { roundCap, hookContract, hookFunction, - closureMechanism, + autoClosure, globalAccumulativeCaps ); vm.stopPrank(); @@ -167,7 +167,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { uint roundCap, address hookContract, bytes memory hookFunction, - bool closureMechanism, + bool autoClosure, bool globalAccumulativeCaps ) = _helper_createDefaultFundingRound(); roundStart = roundStart_; @@ -184,7 +184,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { roundCap, hookContract, hookFunction, - closureMechanism, + autoClosure, globalAccumulativeCaps ); } @@ -198,7 +198,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { uint roundCap, address hookContract, bytes memory hookFunction, - bool closureMechanism, + bool autoClosure, bool globalAccumulativeCaps ) = _helper_createDefaultFundingRound(); roundEnd = 0; @@ -216,7 +216,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { roundCap, hookContract, hookFunction, - closureMechanism, + autoClosure, globalAccumulativeCaps ); } @@ -230,7 +230,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { uint roundCap, address hookContract, bytes memory hookFunction, - bool closureMechanism, + bool autoClosure, bool globalAccumulativeCaps ) = _helper_createDefaultFundingRound(); vm.assume(roundEnd_ != 0 && roundEnd_ < roundStart); @@ -248,7 +248,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { roundCap, hookContract, hookFunction, - closureMechanism, + autoClosure, globalAccumulativeCaps ); } @@ -261,7 +261,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { uint roundCap, address hookContract, bytes memory hookFunction, - bool closureMechanism, + bool autoClosure, bool globalAccumulativeCaps ) = _helper_createDefaultFundingRound(); hookContract = address(1); @@ -279,7 +279,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { roundCap, hookContract, hookFunction, - closureMechanism, + autoClosure, globalAccumulativeCaps ); } @@ -292,7 +292,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { uint roundCap, address hookContract, bytes memory hookFunction, - bool closureMechanism, + bool autoClosure, bool globalAccumulativeCaps ) = _helper_createDefaultFundingRound(); hookContract = address(0); @@ -310,7 +310,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { roundCap, hookContract, hookFunction, - closureMechanism, + autoClosure, globalAccumulativeCaps ); } @@ -328,7 +328,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { uint roundCap, address hookContract, bytes memory hookFunction, - bool closureMechanism, + bool autoClosure, bool globalAccumulativeCaps ) = _helper_createDefaultFundingRound(); _helper_callCreateRound( @@ -337,7 +337,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { roundCap, hookContract, hookFunction, - closureMechanism, + autoClosure, globalAccumulativeCaps ); @@ -348,7 +348,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { uint roundCap_, address hookContract_, bytes memory hookFunction_, - bool closureMechanism_, + bool autoClosure_, bool globalAccumulativeCaps_ ) = fundingPot.getRoundGenericParameters(lastRoundId); @@ -357,7 +357,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { assertEq(roundCap, roundCap_); assertEq(hookContract, hookContract_); assertEq(hookFunction, hookFunction_); - assertEq(closureMechanism, closureMechanism_); + assertEq(autoClosure, autoClosure_); assertEq(globalAccumulativeCaps, globalAccumulativeCaps_); } @@ -395,6 +395,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { function testEditRound_revertsGivenUserIsNotFundingPotAdmin(address user_) public { + vm.assume(user_ != address(0) && user_ != address(this)); testCreateRound(); uint64 roundId = fundingPot.getRoundCount(); @@ -414,7 +415,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { uint roundCap_, address hookContract_, bytes memory hookFunction_, - bool closureMechanism_, + bool autoClosure_, bool globalAccumulativeCaps_ ) = _helper_createEditedRoundParams(); @@ -425,7 +426,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { roundCap_, hookContract_, hookFunction_, - closureMechanism_, + autoClosure_, globalAccumulativeCaps_ ); vm.stopPrank(); @@ -442,7 +443,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { uint roundCap_, address hookContract_, bytes memory hookFunction_, - bool closureMechanism_, + bool autoClosure_, bool globalAccumulativeCaps_ ) = _helper_createEditedRoundParams(); @@ -460,7 +461,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { roundCap_, hookContract_, hookFunction_, - closureMechanism_, + autoClosure_, globalAccumulativeCaps_ ); } @@ -475,7 +476,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { uint roundCap, address hookContract, bytes memory hookFunction, - bool closureMechanism, + bool autoClosure, bool globalAccumulativeCaps ) = fundingPot.getRoundGenericParameters(roundId); vm.warp(roundStart + 1); @@ -486,7 +487,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { uint roundCap_, address hookContract_, bytes memory hookFunction_, - bool closureMechanism_, + bool autoClosure_, bool globalAccumulativeCaps_ ) = _helper_createEditedRoundParams(); vm.expectRevert( @@ -503,7 +504,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { roundCap_, hookContract_, hookFunction_, - closureMechanism_, + autoClosure_, globalAccumulativeCaps_ ); } @@ -521,7 +522,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { uint roundCap_, address hookContract_, bytes memory hookFunction_, - bool closureMechanism_, + bool autoClosure_, bool globalAccumulativeCaps_ ) = _helper_createEditedRoundParams(); roundStart_ = roundStartP_; @@ -541,7 +542,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { roundCap_, hookContract_, hookFunction_, - closureMechanism_, + autoClosure_, globalAccumulativeCaps_ ); } @@ -556,7 +557,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { uint roundCap_, address hookContract_, bytes memory hookFunction_, - bool closureMechanism_, + bool autoClosure_, bool globalAccumulativeCaps_ ) = _helper_createEditedRoundParams(); roundEnd_ = 0; @@ -577,7 +578,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { roundCap_, hookContract_, hookFunction_, - closureMechanism_, + autoClosure_, globalAccumulativeCaps_ ); } @@ -594,7 +595,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { uint roundCap_, address hookContract_, bytes memory hookFunction_, - bool closureMechanism_, + bool autoClosure_, bool globalAccumulativeCaps_ ) = _helper_createEditedRoundParams(); roundEnd_ = bound(roundEnd_, 0, roundStart_ - 1); @@ -614,7 +615,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { roundCap_, hookContract_, hookFunction_, - closureMechanism_, + autoClosure_, globalAccumulativeCaps_ ); } @@ -631,7 +632,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { uint roundCap_, address hookContract_, bytes memory hookFunction_, - bool closureMechanism_, + bool autoClosure_, bool globalAccumulativeCaps_ ) = _helper_createEditedRoundParams(); hookContract_ = address(1); @@ -652,7 +653,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { roundCap_, hookContract_, hookFunction_, - closureMechanism_, + autoClosure_, globalAccumulativeCaps_ ); } @@ -669,7 +670,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { uint roundCap_, address hookContract_, bytes memory hookFunction_, - bool closureMechanism_, + bool autoClosure_, bool globalAccumulativeCaps_ ) = _helper_createEditedRoundParams(); hookContract_ = address(0); @@ -690,7 +691,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { roundCap_, hookContract_, hookFunction_, - closureMechanism_, + autoClosure_, globalAccumulativeCaps_ ); } @@ -705,7 +706,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { ├── roundCap should be updated to the new value ├── hookContract should be updated to the new value ├── hookFunction should be updated to the new value - ├── closureMechanism should be updated to the new value + ├── autoClosure should be updated to the new value └── globalAccumulativeCaps should be updated to the new value */ @@ -719,7 +720,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { uint roundCap_, address hookContract_, bytes memory hookFunction_, - bool closureMechanism_, + bool autoClosure_, bool globalAccumulativeCaps_ ) = _helper_createEditedRoundParams(); @@ -730,7 +731,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { roundCap_, hookContract_, hookFunction_, - closureMechanism_, + autoClosure_, globalAccumulativeCaps_ ); @@ -740,7 +741,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { uint roundCap, address hookContract, bytes memory hookFunction, - bool closureMechanism, + bool autoClosure, bool globalAccumulativeCaps ) = fundingPot.getRoundGenericParameters(lastRoundId); @@ -749,7 +750,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { assertEq(roundCap, roundCap_); assertEq(hookContract, hookContract_); assertEq(hookFunction, hookFunction_); - assertEq(closureMechanism, closureMechanism_); + assertEq(autoClosure, autoClosure_); assertEq(globalAccumulativeCaps, globalAccumulativeCaps_); } @@ -850,7 +851,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { function testSetAccessCriteria_revertsGivenAccessCriteriaIdIsNFTAndNftContractIsZero( ) public { uint8 accessCriteriaEnum = - uint8(ILM_PC_FundingPot_v1.AccessCriteriaId.NFT); + uint8(ILM_PC_FundingPot_v1.AccessCriteriaType.NFT); testCreateRound(); uint64 roundId = fundingPot.getRoundCount(); uint8 accessId = 1; @@ -872,7 +873,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { function testSetAccessCriteria_revertsGivenAccessCriteriaIdIsMerkleAndMerkleRootIsZero( ) public { uint8 accessCriteriaEnum = - uint8(ILM_PC_FundingPot_v1.AccessCriteriaId.MERKLE); + uint8(ILM_PC_FundingPot_v1.AccessCriteriaType.MERKLE); testCreateRound(); uint64 roundId = fundingPot.getRoundCount(); uint8 accessId = 1; @@ -894,7 +895,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { function testSetAccessCriteria_revertsGivenAccessCriteriaIdIsListAndAllowedAddressesIsEmpty( ) public { uint8 accessCriteriaEnum = - uint8(ILM_PC_FundingPot_v1.AccessCriteriaId.LIST); + uint8(ILM_PC_FundingPot_v1.AccessCriteriaType.LIST); testCreateRound(); uint64 roundId = fundingPot.getRoundCount(); uint8 accessId = 1; @@ -953,7 +954,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { uint roundCap = 1000; address hookContract = address(0); bytes memory hookFunction = bytes(""); - bool closureMechanism = false; + bool autoClosure = false; bool globalAccumulativeCaps = false; return ( @@ -962,7 +963,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { roundCap, hookContract, hookFunction, - closureMechanism, + autoClosure, globalAccumulativeCaps ); } @@ -974,7 +975,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { uint roundCap, address hookContract, bytes memory hookFunction, - bool closureMechanism, + bool autoClosure, bool globalAccumulativeCaps ) internal { fundingPot.createRound( @@ -983,7 +984,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { roundCap, hookContract, hookFunction, - closureMechanism, + autoClosure, globalAccumulativeCaps ); } @@ -998,7 +999,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { uint roundCap_ = 2000; address hookContract_ = address(0x1); bytes memory hookFunction_ = bytes("test"); - bool closureMechanism_ = true; + bool autoClosure_ = true; bool globalAccumulativeCaps_ = true; return ( @@ -1007,7 +1008,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { roundCap_, hookContract_, hookFunction_, - closureMechanism_, + autoClosure_, globalAccumulativeCaps_ ); } @@ -1020,7 +1021,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { uint roundCap, address hookContract, bytes memory hookFunction, - bool closureMechanism, + bool autoClosure, bool globalAccumulativeCaps ) internal { fundingPot.editRound( @@ -1030,7 +1031,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { roundCap, hookContract, hookFunction, - closureMechanism, + autoClosure, globalAccumulativeCaps ); } @@ -1042,41 +1043,41 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { { if ( accessCriteriaEnum - == uint8(ILM_PC_FundingPot_v1.AccessCriteriaId.OPEN) + == uint8(ILM_PC_FundingPot_v1.AccessCriteriaType.OPEN) ) { return ILM_PC_FundingPot_v1.AccessCriteria( - ILM_PC_FundingPot_v1.AccessCriteriaId.OPEN, + ILM_PC_FundingPot_v1.AccessCriteriaType.OPEN, address(0x0), bytes32(uint(0x0)), new address[](0) ); } else if ( accessCriteriaEnum - == uint8(ILM_PC_FundingPot_v1.AccessCriteriaId.NFT) + == uint8(ILM_PC_FundingPot_v1.AccessCriteriaType.NFT) ) { address nftContract = address(0x1); return ILM_PC_FundingPot_v1.AccessCriteria( - ILM_PC_FundingPot_v1.AccessCriteriaId.NFT, + ILM_PC_FundingPot_v1.AccessCriteriaType.NFT, nftContract, bytes32(uint(0x0)), new address[](0) ); } else if ( accessCriteriaEnum - == uint8(ILM_PC_FundingPot_v1.AccessCriteriaId.MERKLE) + == uint8(ILM_PC_FundingPot_v1.AccessCriteriaType.MERKLE) ) { bytes32 merkleRoot = bytes32(uint(0x1)); return ILM_PC_FundingPot_v1.AccessCriteria( - ILM_PC_FundingPot_v1.AccessCriteriaId.MERKLE, + ILM_PC_FundingPot_v1.AccessCriteriaType.MERKLE, address(0x0), merkleRoot, new address[](0) ); } else if ( accessCriteriaEnum - == uint8(ILM_PC_FundingPot_v1.AccessCriteriaId.LIST) + == uint8(ILM_PC_FundingPot_v1.AccessCriteriaType.LIST) ) { address[] memory allowedAddresses = new address[](3); allowedAddresses[0] = address(0x1); @@ -1084,7 +1085,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { allowedAddresses[2] = address(0x3); return ILM_PC_FundingPot_v1.AccessCriteria( - ILM_PC_FundingPot_v1.AccessCriteriaId.LIST, + ILM_PC_FundingPot_v1.AccessCriteriaType.LIST, address(0x0), bytes32(uint(0x0)), allowedAddresses From f0a6ec69c1fb1dc6aa4621e0008bd75cbb706922 Mon Sep 17 00:00:00 2001 From: Zuhaib Mohammed Date: Sat, 29 Mar 2025 12:36:59 +0530 Subject: [PATCH 028/130] fix: PR Review Fix#2 --- .../logicModule/LM_PC_FundingPot_v1.sol | 33 ++++++------- .../interfaces/ILM_PC_FundingPot_v1.sol | 8 +++- .../logicModule/LM_PC_FundingPot_v1.t.sol | 46 ++++++++++++------- 3 files changed, 49 insertions(+), 38 deletions(-) diff --git a/src/modules/logicModule/LM_PC_FundingPot_v1.sol b/src/modules/logicModule/LM_PC_FundingPot_v1.sol index 0a0ad34a5..5786842de 100644 --- a/src/modules/logicModule/LM_PC_FundingPot_v1.sol +++ b/src/modules/logicModule/LM_PC_FundingPot_v1.sol @@ -164,21 +164,14 @@ contract LM_PC_FundingPot_v1 is Round storage round = rounds[roundId_]; AccessCriteria storage accessCriteria = round.accessCriterias[id_]; - if (accessCriteria.accessCriteriaType == AccessCriteriaType.OPEN) { - return ( - true, - accessCriteria.nftContract, - accessCriteria.merkleRoot, - accessCriteria.allowedAddresses - ); - } else { - return ( - false, - accessCriteria.nftContract, - accessCriteria.merkleRoot, - accessCriteria.allowedAddresses - ); - } + bool isOpen = + (accessCriteria.accessCriteriaType == AccessCriteriaType.OPEN); + return ( + isOpen, + accessCriteria.nftContract, + accessCriteria.merkleRoot, + accessCriteria.allowedAddresses + ); } /// @inheritdoc ILM_PC_FundingPot_v1 @@ -220,6 +213,7 @@ contract LM_PC_FundingPot_v1 is roundEnd_, roundCap_, hookContract_, + hookFunction_, autoClosure_, globalAccumulativeCaps_ ); @@ -258,6 +252,7 @@ contract LM_PC_FundingPot_v1 is roundEnd_, roundCap_, hookContract_, + hookFunction_, autoClosure_, globalAccumulativeCaps_ ); @@ -266,7 +261,7 @@ contract LM_PC_FundingPot_v1 is /// @inheritdoc ILM_PC_FundingPot_v1 function setAccessCriteriaForRound( uint64 roundId_, - uint8 accessId_, + uint8 accessCriteriaId_, AccessCriteria memory accessCriteria_ ) external onlyModuleRole(FUNDING_POT_ADMIN_ROLE) { Round storage round = rounds[roundId_]; @@ -290,8 +285,8 @@ contract LM_PC_FundingPot_v1 is revert Module__LM_PC_FundingPot__MissingRequiredAccessCriteriaData(); } - round.accessCriterias[accessId_] = accessCriteria_; - emit AccessCriteriaSet(roundId_, accessId_, accessCriteria_); + round.accessCriterias[accessCriteriaId_] = accessCriteria_; + emit AccessCriteriaSet(roundId_, accessCriteriaId_, accessCriteria_); } // ------------------------------------------------------------------------- // Internal @@ -307,7 +302,7 @@ contract LM_PC_FundingPot_v1 is } // Validate that either end time or cap is set - if (round_.roundEnd == 0 || round_.roundCap == 0) { + if (round_.roundEnd == 0 && round_.roundCap == 0) { revert Module__LM_PC_FundingPot__RoundMustHaveEndTimeOrCap(); } diff --git a/src/modules/logicModule/interfaces/ILM_PC_FundingPot_v1.sol b/src/modules/logicModule/interfaces/ILM_PC_FundingPot_v1.sol index 92c14e24f..342b4e112 100644 --- a/src/modules/logicModule/interfaces/ILM_PC_FundingPot_v1.sol +++ b/src/modules/logicModule/interfaces/ILM_PC_FundingPot_v1.sol @@ -63,6 +63,7 @@ interface ILM_PC_FundingPot_v1 is IERC20PaymentClientBase_v2 { /// @param roundEnd_ The timestamp when the round ends. /// @param roundCap_ The maximum allocation or cap for the round. /// @param hookContract_ The address of an optional hook contract for custom logic. + /// @param hookFunction_ The encoded function call for the hook. /// @param closureMechanism_ A boolean indicating whether a specific closure mechanism is enabled. /// @param globalAccumulativeCaps_ A boolean indicating whether global accumulative caps are enforced. event RoundCreated( @@ -71,6 +72,7 @@ interface ILM_PC_FundingPot_v1 is IERC20PaymentClientBase_v2 { uint roundEnd_, uint roundCap_, address hookContract_, + bytes hookFunction_, bool closureMechanism_, bool globalAccumulativeCaps_ ); @@ -82,6 +84,7 @@ interface ILM_PC_FundingPot_v1 is IERC20PaymentClientBase_v2 { /// @param roundEnd_ The updated timestamp for when the round ends. /// @param roundCap_ The updated maximum allocation or cap for the round. /// @param hookContract_ The address of an optional hook contract for custom logic. + /// @param hookFunction_ The updated encoded function call for the hook. /// @param closureMechanism_ A boolean indicating whether a specific closure mechanism is enabled. /// @param globalAccumulativeCaps_ A boolean indicating whether global accumulative caps are enforced. event RoundEdited( @@ -90,6 +93,7 @@ interface ILM_PC_FundingPot_v1 is IERC20PaymentClientBase_v2 { uint roundEnd_, uint roundCap_, address hookContract_, + bytes hookFunction_, bool closureMechanism_, bool globalAccumulativeCaps_ ); @@ -228,11 +232,11 @@ interface ILM_PC_FundingPot_v1 is IERC20PaymentClientBase_v2 { /// @notice Set Access Control Check. /// @dev Only callable by funding pot admin and only before the round has started. /// @param roundId_ ID of the round. - /// @param accessId_ ID of the access criteria. + /// @param accessCriteriaId_ ID of the access criteria. /// @param accessCriteria_ Access criteria to set. function setAccessCriteriaForRound( uint64 roundId_, - uint8 accessId_, + uint8 accessCriteriaId_, AccessCriteria memory accessCriteria_ ) external; } diff --git a/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol b/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol index 6310698df..5a06e5817 100644 --- a/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol +++ b/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol @@ -44,8 +44,6 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { // ------------------------------------------------------------------------- // Constants - bytes32 internal constant FUNDING_POT_ADMIN_ROLE = "FUNDING_POT_ADMIN"; - // ------------------------------------------------------------------------- // State @@ -787,7 +785,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { testCreateRound(); uint64 roundId = fundingPot.getRoundCount(); - uint8 accessId = 1; + uint8 accessCriteriaId = 1; ILM_PC_FundingPot_v1.AccessCriteria memory accessCriteria = _helper_createAccessCriteria(accessCriteriaEnum_); @@ -801,7 +799,9 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { IModule_v1.Module__CallerNotAuthorized.selector, roleId, user_ ) ); - fundingPot.setAccessCriteriaForRound(roundId, accessId, accessCriteria); + fundingPot.setAccessCriteriaForRound( + roundId, accessCriteriaId, accessCriteria + ); } function testFuzzSetAccessCriteria_revertsGivenRoundDoesNotExist( @@ -810,7 +810,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { vm.assume(accessCriteriaEnum >= 0 && accessCriteriaEnum <= 3); uint64 roundId = fundingPot.getRoundCount(); - uint8 accessId = 1; + uint8 accessCriteriaId = 1; ILM_PC_FundingPot_v1.AccessCriteria memory accessCriteria = _helper_createAccessCriteria(accessCriteriaEnum); @@ -822,7 +822,9 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { .selector ) ); - fundingPot.setAccessCriteriaForRound(roundId, accessId, accessCriteria); + fundingPot.setAccessCriteriaForRound( + roundId, accessCriteriaId, accessCriteria + ); } function testFuzzSetAccessCriteria_revertsGivenRoundIsActive( @@ -831,7 +833,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { vm.assume(accessCriteriaEnum >= 0 && accessCriteriaEnum <= 3); testCreateRound(); uint64 roundId = fundingPot.getRoundCount(); - uint8 accessId = 1; + uint8 accessCriteriaId = 1; ILM_PC_FundingPot_v1.AccessCriteria memory accessCriteria = _helper_createAccessCriteria(accessCriteriaEnum); @@ -845,7 +847,9 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { .selector ) ); - fundingPot.setAccessCriteriaForRound(roundId, accessId, accessCriteria); + fundingPot.setAccessCriteriaForRound( + roundId, accessCriteriaId, accessCriteria + ); } function testSetAccessCriteria_revertsGivenAccessCriteriaIdIsNFTAndNftContractIsZero( @@ -854,7 +858,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { uint8(ILM_PC_FundingPot_v1.AccessCriteriaType.NFT); testCreateRound(); uint64 roundId = fundingPot.getRoundCount(); - uint8 accessId = 1; + uint8 accessCriteriaId = 1; ILM_PC_FundingPot_v1.AccessCriteria memory accessCriteria = _helper_createAccessCriteria(accessCriteriaEnum); @@ -867,7 +871,9 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { .selector ) ); - fundingPot.setAccessCriteriaForRound(roundId, accessId, accessCriteria); + fundingPot.setAccessCriteriaForRound( + roundId, accessCriteriaId, accessCriteria + ); } function testSetAccessCriteria_revertsGivenAccessCriteriaIdIsMerkleAndMerkleRootIsZero( @@ -876,7 +882,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { uint8(ILM_PC_FundingPot_v1.AccessCriteriaType.MERKLE); testCreateRound(); uint64 roundId = fundingPot.getRoundCount(); - uint8 accessId = 1; + uint8 accessCriteriaId = 1; ILM_PC_FundingPot_v1.AccessCriteria memory accessCriteria = _helper_createAccessCriteria(accessCriteriaEnum); @@ -889,7 +895,9 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { .selector ) ); - fundingPot.setAccessCriteriaForRound(roundId, accessId, accessCriteria); + fundingPot.setAccessCriteriaForRound( + roundId, accessCriteriaId, accessCriteria + ); } function testSetAccessCriteria_revertsGivenAccessCriteriaIdIsListAndAllowedAddressesIsEmpty( @@ -898,7 +906,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { uint8(ILM_PC_FundingPot_v1.AccessCriteriaType.LIST); testCreateRound(); uint64 roundId = fundingPot.getRoundCount(); - uint8 accessId = 1; + uint8 accessCriteriaId = 1; ILM_PC_FundingPot_v1.AccessCriteria memory accessCriteria = _helper_createAccessCriteria(accessCriteriaEnum); @@ -911,26 +919,30 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { .selector ) ); - fundingPot.setAccessCriteriaForRound(roundId, accessId, accessCriteria); + fundingPot.setAccessCriteriaForRound( + roundId, accessCriteriaId, accessCriteria + ); } function testFuzzSetAccessCriteria(uint8 accessCriteriaEnum) public { vm.assume(accessCriteriaEnum >= 0 && accessCriteriaEnum <= 3); testCreateRound(); uint64 roundId = fundingPot.getRoundCount(); - uint8 accessId = 1; + uint8 accessCriteriaId = 1; ILM_PC_FundingPot_v1.AccessCriteria memory accessCriteria = _helper_createAccessCriteria(accessCriteriaEnum); - fundingPot.setAccessCriteriaForRound(roundId, accessId, accessCriteria); + fundingPot.setAccessCriteriaForRound( + roundId, accessCriteriaId, accessCriteria + ); ( bool isOpen, address nftContract, bytes32 merkleRoot, address[] memory allowedAddresses - ) = fundingPot.getRoundAccessCriteria(roundId, accessId); + ) = fundingPot.getRoundAccessCriteria(roundId, accessCriteriaId); assertEq(isOpen, accessCriteriaEnum == 0); assertEq(nftContract, accessCriteria.nftContract); From eeb555093bd5f973349c8c5b6e94068eb11e4fd2 Mon Sep 17 00:00:00 2001 From: Zuhaib Mohammed Date: Sun, 30 Mar 2025 03:17:36 +0530 Subject: [PATCH 029/130] fix: PR Review Fix#3 --- .../logicModule/LM_PC_FundingPot_v1.sol | 41 +++- .../interfaces/ILM_PC_FundingPot_v1.sol | 34 ++- .../logicModule/LM_PC_FundingPot_v1.t.sol | 209 +++++++++++++++--- 3 files changed, 248 insertions(+), 36 deletions(-) diff --git a/src/modules/logicModule/LM_PC_FundingPot_v1.sol b/src/modules/logicModule/LM_PC_FundingPot_v1.sol index 5786842de..d25b0b562 100644 --- a/src/modules/logicModule/LM_PC_FundingPot_v1.sol +++ b/src/modules/logicModule/LM_PC_FundingPot_v1.sol @@ -87,6 +87,9 @@ contract LM_PC_FundingPot_v1 is /// @notice Stores all funding rounds by their unique ID. mapping(uint64 => Round) private rounds; + /// @notice Stores the access criteria ID for each round. + mapping(uint64 => uint8) private roundIdtoAccessId; + /// @notice The next available round ID. uint64 private nextRoundId; @@ -164,8 +167,7 @@ contract LM_PC_FundingPot_v1 is Round storage round = rounds[roundId_]; AccessCriteria storage accessCriteria = round.accessCriterias[id_]; - bool isOpen = - (accessCriteria.accessCriteriaType == AccessCriteriaType.OPEN); + isOpen = (accessCriteria.accessCriteriaType == AccessCriteriaType.OPEN); return ( isOpen, accessCriteria.nftContract, @@ -175,10 +177,19 @@ contract LM_PC_FundingPot_v1 is } /// @inheritdoc ILM_PC_FundingPot_v1 - function getRoundCount() external view returns (uint64) { + function getRoundCount() external view returns (uint64 roundCount_) { return nextRoundId; } + /// @inheritdoc ILM_PC_FundingPot_v1 + function getRoundAccessCriteriaCount(uint64 roundId_) + public + view + returns (uint8 accessCriteriaCount_) + { + return roundIdtoAccessId[roundId_]; + } + // ------------------------------------------------------------------------- // Public - Mutating @@ -261,7 +272,6 @@ contract LM_PC_FundingPot_v1 is /// @inheritdoc ILM_PC_FundingPot_v1 function setAccessCriteriaForRound( uint64 roundId_, - uint8 accessCriteriaId_, AccessCriteria memory accessCriteria_ ) external onlyModuleRole(FUNDING_POT_ADMIN_ROLE) { Round storage round = rounds[roundId_]; @@ -284,9 +294,30 @@ contract LM_PC_FundingPot_v1 is ) { revert Module__LM_PC_FundingPot__MissingRequiredAccessCriteriaData(); } + uint8 accessCriteriaId = roundIdtoAccessId[roundId_]; + round.accessCriterias[accessCriteriaId] = accessCriteria_; + + emit AccessCriteriaSet(roundId_, accessCriteriaId, accessCriteria_); + + roundIdtoAccessId[roundId_] += 1; + } + + /// @inheritdoc ILM_PC_FundingPot_v1 + function editAccessCriteriaForRound( + uint64 roundId_, + uint8 accessCriteriaId_, + AccessCriteria memory accessCriteria_ + ) external onlyModuleRole(FUNDING_POT_ADMIN_ROLE) { + if (accessCriteriaId_ >= roundIdtoAccessId[roundId_]) { + revert Module__LM_PC_FundingPot__InvalidAccessCriteriaId(); + } + Round storage round = rounds[roundId_]; + + _validateEditRoundParameters(round); round.accessCriterias[accessCriteriaId_] = accessCriteria_; - emit AccessCriteriaSet(roundId_, accessCriteriaId_, accessCriteria_); + + emit AccessCriteriaEdited(roundId_, accessCriteriaId_, accessCriteria_); } // ------------------------------------------------------------------------- // Internal diff --git a/src/modules/logicModule/interfaces/ILM_PC_FundingPot_v1.sol b/src/modules/logicModule/interfaces/ILM_PC_FundingPot_v1.sol index 342b4e112..f323e06b5 100644 --- a/src/modules/logicModule/interfaces/ILM_PC_FundingPot_v1.sol +++ b/src/modules/logicModule/interfaces/ILM_PC_FundingPot_v1.sol @@ -106,6 +106,14 @@ interface ILM_PC_FundingPot_v1 is IERC20PaymentClientBase_v2 { uint64 indexed roundId_, uint8 accessId_, AccessCriteria accessCriteria_ ); + /// @notice Emitted when access criteria is edited for a round. + /// @param roundId_ The unique identifier of the round. + /// @param accessId_ The identifier of the access criteria. + /// @param accessCriteria_ The access criteria. + event AccessCriteriaEdited( + uint64 indexed roundId_, uint8 accessId_, AccessCriteria accessCriteria_ + ); + // ------------------------------------------------------------------------- // Errors @@ -139,6 +147,9 @@ interface ILM_PC_FundingPot_v1 is IERC20PaymentClientBase_v2 { /// @notice Incorrect access criteria. error Module__LM_PC_FundingPot__MissingRequiredAccessCriteriaData(); + /// @notice Invalid access criteria ID. + error Module__LM_PC_FundingPot__InvalidAccessCriteriaId(); + // ------------------------------------------------------------------------- // Public - Getters @@ -182,8 +193,16 @@ interface ILM_PC_FundingPot_v1 is IERC20PaymentClientBase_v2 { ); /// @notice Retrieves the total number of funding rounds. - /// @return The total number of funding rounds. - function getRoundCount() external view returns (uint64); + /// @return roundCount_ The total number of funding rounds. + function getRoundCount() external view returns (uint64 roundCount_); + + /// @notice Retrieves the total number of access criteria for a specific round. + /// @param roundId_ The unique identifier of the round. + /// @return accessCriteriaCount_ The total number of access criteria for the round. + function getRoundAccessCriteriaCount(uint64 roundId_) + external + view + returns (uint8 accessCriteriaCount_); // ------------------------------------------------------------------------- // Public - Mutating @@ -232,9 +251,18 @@ interface ILM_PC_FundingPot_v1 is IERC20PaymentClientBase_v2 { /// @notice Set Access Control Check. /// @dev Only callable by funding pot admin and only before the round has started. /// @param roundId_ ID of the round. - /// @param accessCriteriaId_ ID of the access criteria. /// @param accessCriteria_ Access criteria to set. function setAccessCriteriaForRound( + uint64 roundId_, + AccessCriteria memory accessCriteria_ + ) external; + + /// @notice Edits an existing access criteria for a round. + /// @dev Only callable by funding pot admin and only before the round has started. + /// @param roundId_ ID of the round. + /// @param accessCriteriaId_ ID of the access criteria. + /// @param accessCriteria_ New access criteria. + function editAccessCriteriaForRound( uint64 roundId_, uint8 accessCriteriaId_, AccessCriteria memory accessCriteria_ diff --git a/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol b/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol index 5a06e5817..f2ca7d38d 100644 --- a/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol +++ b/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol @@ -785,7 +785,6 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { testCreateRound(); uint64 roundId = fundingPot.getRoundCount(); - uint8 accessCriteriaId = 1; ILM_PC_FundingPot_v1.AccessCriteria memory accessCriteria = _helper_createAccessCriteria(accessCriteriaEnum_); @@ -799,9 +798,8 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { IModule_v1.Module__CallerNotAuthorized.selector, roleId, user_ ) ); - fundingPot.setAccessCriteriaForRound( - roundId, accessCriteriaId, accessCriteria - ); + fundingPot.setAccessCriteriaForRound(roundId, accessCriteria); + vm.stopPrank(); } function testFuzzSetAccessCriteria_revertsGivenRoundDoesNotExist( @@ -810,7 +808,6 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { vm.assume(accessCriteriaEnum >= 0 && accessCriteriaEnum <= 3); uint64 roundId = fundingPot.getRoundCount(); - uint8 accessCriteriaId = 1; ILM_PC_FundingPot_v1.AccessCriteria memory accessCriteria = _helper_createAccessCriteria(accessCriteriaEnum); @@ -822,24 +819,23 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { .selector ) ); - fundingPot.setAccessCriteriaForRound( - roundId, accessCriteriaId, accessCriteria - ); + fundingPot.setAccessCriteriaForRound(roundId, accessCriteria); } function testFuzzSetAccessCriteria_revertsGivenRoundIsActive( uint8 accessCriteriaEnum ) public { vm.assume(accessCriteriaEnum >= 0 && accessCriteriaEnum <= 3); + testCreateRound(); uint64 roundId = fundingPot.getRoundCount(); - uint8 accessCriteriaId = 1; ILM_PC_FundingPot_v1.AccessCriteria memory accessCriteria = _helper_createAccessCriteria(accessCriteriaEnum); (uint roundStart,,,,,,) = fundingPot.getRoundGenericParameters(roundId); vm.warp(roundStart + 1); + vm.expectRevert( abi.encodeWithSelector( ILM_PC_FundingPot_v1 @@ -847,18 +843,16 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { .selector ) ); - fundingPot.setAccessCriteriaForRound( - roundId, accessCriteriaId, accessCriteria - ); + fundingPot.setAccessCriteriaForRound(roundId, accessCriteria); } function testSetAccessCriteria_revertsGivenAccessCriteriaIdIsNFTAndNftContractIsZero( ) public { uint8 accessCriteriaEnum = uint8(ILM_PC_FundingPot_v1.AccessCriteriaType.NFT); + testCreateRound(); uint64 roundId = fundingPot.getRoundCount(); - uint8 accessCriteriaId = 1; ILM_PC_FundingPot_v1.AccessCriteria memory accessCriteria = _helper_createAccessCriteria(accessCriteriaEnum); @@ -871,18 +865,16 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { .selector ) ); - fundingPot.setAccessCriteriaForRound( - roundId, accessCriteriaId, accessCriteria - ); + fundingPot.setAccessCriteriaForRound(roundId, accessCriteria); } function testSetAccessCriteria_revertsGivenAccessCriteriaIdIsMerkleAndMerkleRootIsZero( ) public { uint8 accessCriteriaEnum = uint8(ILM_PC_FundingPot_v1.AccessCriteriaType.MERKLE); + testCreateRound(); uint64 roundId = fundingPot.getRoundCount(); - uint8 accessCriteriaId = 1; ILM_PC_FundingPot_v1.AccessCriteria memory accessCriteria = _helper_createAccessCriteria(accessCriteriaEnum); @@ -895,18 +887,16 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { .selector ) ); - fundingPot.setAccessCriteriaForRound( - roundId, accessCriteriaId, accessCriteria - ); + fundingPot.setAccessCriteriaForRound(roundId, accessCriteria); } function testSetAccessCriteria_revertsGivenAccessCriteriaIdIsListAndAllowedAddressesIsEmpty( ) public { uint8 accessCriteriaEnum = uint8(ILM_PC_FundingPot_v1.AccessCriteriaType.LIST); + testCreateRound(); uint64 roundId = fundingPot.getRoundCount(); - uint8 accessCriteriaId = 1; ILM_PC_FundingPot_v1.AccessCriteria memory accessCriteria = _helper_createAccessCriteria(accessCriteriaEnum); @@ -919,23 +909,20 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { .selector ) ); - fundingPot.setAccessCriteriaForRound( - roundId, accessCriteriaId, accessCriteria - ); + fundingPot.setAccessCriteriaForRound(roundId, accessCriteria); } function testFuzzSetAccessCriteria(uint8 accessCriteriaEnum) public { vm.assume(accessCriteriaEnum >= 0 && accessCriteriaEnum <= 3); + testCreateRound(); uint64 roundId = fundingPot.getRoundCount(); - uint8 accessCriteriaId = 1; + uint8 accessCriteriaId = 0; ILM_PC_FundingPot_v1.AccessCriteria memory accessCriteria = _helper_createAccessCriteria(accessCriteriaEnum); - fundingPot.setAccessCriteriaForRound( - roundId, accessCriteriaId, accessCriteria - ); + fundingPot.setAccessCriteriaForRound(roundId, accessCriteria); ( bool isOpen, @@ -950,6 +937,159 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { assertEq(allowedAddresses, accessCriteria.allowedAddresses); } + /* test editAccessCriteriaForRound() + ├── Given user does not have FUNDING_POT_ADMIN_ROLE + │ └── When user attempts to edit access criteria + │ └── Then it should revert + ├── Given access criteria id is greater than the number of access criteria for the round + │ └── When user attempts to edit access criteria + │ └── Then it should revert + ├── Given round does not exist + │ └── When user attempts to set access criteria + │ └── Then it should revert + ├── Given round is active + │ └── When user attempts to set access criteria + │ └── Then it should revert + └── Given all the valid parameters are provided + └── When user attempts to edit access criteria + └── Then it should not revert + └── Then the access criteria should be updated + + */ + + function testFuzzEditAccessCriteriaForRound_revertsGivenUserDoesNotHaveFundingPotAdminRole( + uint8 accessCriteriaEnum, + address user_ + ) public { + vm.assume(accessCriteriaEnum >= 0 && accessCriteriaEnum <= 3); + vm.assume(user_ != address(0) && user_ != address(this)); + + _helper_setupRoundWithAccessCriteria(accessCriteriaEnum); + uint64 roundId = fundingPot.getRoundCount(); + uint8 accessCriteriaId = 0; + + ILM_PC_FundingPot_v1.AccessCriteria memory accessCriteria = + _helper_createAccessCriteria(accessCriteriaEnum); + + vm.startPrank(user_); + bytes32 roleId = _authorizer.generateRoleId( + address(fundingPot), fundingPot.FUNDING_POT_ADMIN_ROLE() + ); + vm.expectRevert( + abi.encodeWithSelector( + IModule_v1.Module__CallerNotAuthorized.selector, roleId, user_ + ) + ); + fundingPot.editAccessCriteriaForRound( + roundId, accessCriteriaId, accessCriteria + ); + vm.stopPrank(); + } + + function testFuzzEditAccessCriteriaForRound_revertsGivenAccessCriteriaIdIsGreaterThanAccessCriteriaForTheRound( + uint8 accessCriteriaEnum + ) public { + vm.assume(accessCriteriaEnum >= 0 && accessCriteriaEnum <= 3); + + _helper_setupRoundWithAccessCriteria(accessCriteriaEnum); + uint64 roundId = fundingPot.getRoundCount(); + uint8 accessCriteriaId = 1; // Invalid ID + + ILM_PC_FundingPot_v1.AccessCriteria memory accessCriteria = + _helper_createAccessCriteria(accessCriteriaEnum); + + vm.expectRevert( + abi.encodeWithSelector( + ILM_PC_FundingPot_v1 + .Module__LM_PC_FundingPot__InvalidAccessCriteriaId + .selector + ) + ); + fundingPot.editAccessCriteriaForRound( + roundId, accessCriteriaId, accessCriteria + ); + } + + function testFuzzEditAccessCriteriaForRound_revertsGivenRoundDoesNotExist( + uint8 accessCriteriaEnum + ) public { + vm.assume(accessCriteriaEnum >= 0 && accessCriteriaEnum <= 3); + uint64 roundId = fundingPot.getRoundCount(); + uint8 accessCriteriaId = 0; + + ILM_PC_FundingPot_v1.AccessCriteria memory accessCriteria = + _helper_createAccessCriteria(accessCriteriaEnum); + + vm.expectRevert(); + fundingPot.editAccessCriteriaForRound( + roundId, accessCriteriaId, accessCriteria + ); + } + + function testFuzzEditAccessCriteriaForRound_revertsGivenRoundIsActive( + uint8 accessCriteriaEnum + ) public { + vm.assume(accessCriteriaEnum >= 0 && accessCriteriaEnum <= 3); + + _helper_setupRoundWithAccessCriteria(accessCriteriaEnum); + uint64 roundId = fundingPot.getRoundCount(); + uint8 accessCriteriaId = 0; + + // Warp to make the round active + (uint roundStart,,,,,,) = fundingPot.getRoundGenericParameters(roundId); + vm.warp(roundStart + 1); + + ILM_PC_FundingPot_v1.AccessCriteria memory accessCriteria = + _helper_createAccessCriteria(accessCriteriaEnum); + + vm.expectRevert( + abi.encodeWithSelector( + ILM_PC_FundingPot_v1 + .Module__LM_PC_FundingPot__RoundAlreadyStarted + .selector + ) + ); + fundingPot.editAccessCriteriaForRound( + roundId, accessCriteriaId, accessCriteria + ); + } + + function testFuzzEditAccessCriteriaForRound( + uint8 accessCriteriaEnumOld, + uint8 accessCriteriaEnumNew + ) public { + vm.assume(accessCriteriaEnumOld >= 0 && accessCriteriaEnumOld <= 3); + vm.assume( + accessCriteriaEnumNew != accessCriteriaEnumOld + && accessCriteriaEnumNew >= 0 && accessCriteriaEnumNew <= 3 + ); + + _helper_setupRoundWithAccessCriteria(accessCriteriaEnumOld); + uint64 roundId = fundingPot.getRoundCount(); + uint8 accessCriteriaId = 0; + + // Create and apply new access criteria + ILM_PC_FundingPot_v1.AccessCriteria memory newAccessCriteria = + _helper_createAccessCriteria(accessCriteriaEnumNew); + + fundingPot.editAccessCriteriaForRound( + roundId, accessCriteriaId, newAccessCriteria + ); + + // Verify the access criteria was updated + ( + bool isOpen, + address nftContract, + bytes32 merkleRoot, + address[] memory allowedAddresses + ) = fundingPot.getRoundAccessCriteria(roundId, accessCriteriaId); + + assertEq(isOpen, accessCriteriaEnumNew == 0); + assertEq(nftContract, newAccessCriteria.nftContract); + assertEq(merkleRoot, newAccessCriteria.merkleRoot); + assertEq(allowedAddresses, newAccessCriteria.allowedAddresses); + } + // ------------------------------------------------------------------------- // Test: Internal Functions @@ -1105,4 +1245,17 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { } } } + + // Helper function to set up a round with access criteria + function _helper_setupRoundWithAccessCriteria(uint8 accessCriteriaEnum) + internal + { + testCreateRound(); + uint64 roundId = fundingPot.getRoundCount(); + + ILM_PC_FundingPot_v1.AccessCriteria memory accessCriteria = + _helper_createAccessCriteria(accessCriteriaEnum); + + fundingPot.setAccessCriteriaForRound(roundId, accessCriteria); + } } From 7c26bd49a623484ee371816fdce50189f34ac98e Mon Sep 17 00:00:00 2001 From: Zuhaib Mohammed Date: Sun, 30 Mar 2025 10:56:33 +0530 Subject: [PATCH 030/130] fix: PR Review Fix#4 --- .../logicModule/LM_PC_FundingPot_v1.sol | 7 +- .../interfaces/ILM_PC_FundingPot_v1.sol | 9 +- .../logicModule/LM_PC_FundingPot_v1.t.sol | 185 ++++++++++-------- 3 files changed, 115 insertions(+), 86 deletions(-) diff --git a/src/modules/logicModule/LM_PC_FundingPot_v1.sol b/src/modules/logicModule/LM_PC_FundingPot_v1.sol index d25b0b562..a30c4165d 100644 --- a/src/modules/logicModule/LM_PC_FundingPot_v1.sol +++ b/src/modules/logicModule/LM_PC_FundingPot_v1.sol @@ -167,7 +167,10 @@ contract LM_PC_FundingPot_v1 is Round storage round = rounds[roundId_]; AccessCriteria storage accessCriteria = round.accessCriterias[id_]; - isOpen = (accessCriteria.accessCriteriaType == AccessCriteriaType.OPEN); + isOpen = ( + accessCriteria.accessCriteriaType == AccessCriteriaType.UNSET + || accessCriteria.accessCriteriaType == AccessCriteriaType.OPEN + ); return ( isOpen, accessCriteria.nftContract, @@ -338,7 +341,7 @@ contract LM_PC_FundingPot_v1 is } // If end time is set, validate it's after start time - if (round_.roundEnd > 0 && round_.roundEnd <= round_.roundStart) { + if (round_.roundEnd > 0 && round_.roundEnd < round_.roundStart) { revert Module__LM_PC_FundingPot__RoundEndMustBeAfterStart(); } diff --git a/src/modules/logicModule/interfaces/ILM_PC_FundingPot_v1.sol b/src/modules/logicModule/interfaces/ILM_PC_FundingPot_v1.sol index f323e06b5..743eb78d9 100644 --- a/src/modules/logicModule/interfaces/ILM_PC_FundingPot_v1.sol +++ b/src/modules/logicModule/interfaces/ILM_PC_FundingPot_v1.sol @@ -46,10 +46,11 @@ interface ILM_PC_FundingPot_v1 is IERC20PaymentClientBase_v2 { /// @notice Enum used to identify the type of access criteria. enum AccessCriteriaType { - OPEN, // 0 - NFT, // 1 - MERKLE, // 2 - LIST // 3 + UNSET, // 0 + OPEN, // 1 + NFT, // 2 + MERKLE, // 3 + LIST // 4 } diff --git a/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol b/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol index f2ca7d38d..1a9d50ce3 100644 --- a/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol +++ b/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol @@ -97,26 +97,32 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { ├── Given user does not have FUNDING_POT_ADMIN_ROLE │ └── When user attempts to create a round │ └── Then it should revert + │ └── Given user has FUNDING_POT_ADMIN_ROLE ├── And round start < block.timestamp │ └── When user attempts to create a round │ └── Then it should revert - ├── And round end time == 0 - │ ├── And round cap == 0 - │ │ └── When user attempts to create a round - │ │ └── Then it should revert - ├── And round end time is set - │ ├── And round end != 0 - │ ├── And round end < round start - │ │ └── When user attempts to create a round - │ │ └── Then it should revert + │ + ├── And round end time == 0 + ├── And round cap == 0 + │ └── When user attempts to create a round + │ └── Then it should revert + │ + ├── And round end time is set + ├── And round end != 0 + ├── And round end < round start + │ └── When user attempts to create a round + │ └── Then it should revert + │ ├── And hook contract is set but hook function is not set │ └── When user attempts to create a round │ └── Then it should revert + │ ├── And hook function is set but hook contract is not set │ └── When user attempts to create a round │ └── Then it should revert - └── Given all the valid parameters are provided + │ + └── And all the valid parameters are provided └── When user attempts to create a round └── Then it should not be active and should return the round id */ @@ -317,7 +323,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { ├── Given all the valid parameters are provided │ └── When user attempts to create a round │ └── Then it should not be active and should return the round id - */ + */ function testCreateRound() public { ( @@ -363,31 +369,39 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { ├── Given user does not have FUNDING_POT_ADMIN_ROLE │ └── When user attempts to edit a round │ └── Then it should revert - ├── Given round does not exist - │ └── When user attempts to edit the round - │ └── Then it should revert - ├── Given round is active - │ └── When user attempts to edit the round - │ └── Then it should revert - ├── Given round start time is in the past - │ └── When user attempts to edit a round with the above parameter - │ └── Then it should revert - ├── Given round end time == 0 - │ ├── And round cap == 0 - │ └── When user attempts to edit a round with the above parameters - │ └── Then it should revert - ├── Given round end time is set - │ ├── And round end is before round start - │ └── When user attempts to edit the round - │ └── Then it should revert - ├── Given hook contract is set - │ ├── And hook function is empty - │ └── When user attempts to edit the round - │ └── Then it should revert - └── Given hook function is set - ├── And hook contract is empty + │ + └── Given user has FUNDING_POT_ADMIN_ROLE + ├── Given round does not exist + │ └── When user attempts to edit the round + │ └── Then it should revert + │ + ├── Given round is active + │ └── When user attempts to edit the round + │ └── Then it should revert + │ + ├── Given round start time is in the past + │ └── When user attempts to edit a round with this parameter + │ └── Then it should revert + │ + ├── Given round end time == 0 and round cap == 0 + │ └── When user attempts to edit a round with these parameters + │ └── Then it should revert + │ + ├── Given round end time is set and round end < round start + │ └── When user attempts to edit the round + │ └── Then it should revert + │ + ├── Given hook contract is set but hook function is empty + │ └── When user attempts to edit the round + │ └── Then it should revert + │ + ├── Given hook function is set but hook contract is empty + │ └── When user attempts to edit the round + │ └── Then it should revert + │ + └── Given all valid parameters are provided └── When user attempts to edit the round - └── Then it should revert + └── Then all round details should be successfully updated */ function testEditRound_revertsGivenUserIsNotFundingPotAdmin(address user_) @@ -698,7 +712,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { └── Given a round has been created ├── And the round is not active └── When an admin provides valid parameters to edit the round - └── Then all the round details should be successfully updated + └── Then all round details should be successfully updated ├── roundStart should be updated to the new value ├── roundEnd should be updated to the new value ├── roundCap should be updated to the new value @@ -756,24 +770,31 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { ├── Given user does not have FUNDING_POT_ADMIN_ROLE │ └── When user attempts to set access criteria │ └── Then it should revert - ├── Given round does not exist - │ └── When user attempts to set access criteria - │ └── Then it should revert - ├── Given round is active - │ └── When user attempts to set access criteria - │ └── Then it should revert - ├── Given AccessCriteriaId is NFT and nftContract is 0x0 - │ └── When user attempts to set access criteria - │ └── Then it should revert - ├── Given AccessCriteriaId is MERKLE and merkleRoot is 0x0 - │ └── When user attempts to set access criteria - │ └── Then it should revert - ├── Given AccessCriteriaId is LIST and allowedAddresses is empty - │ └── When user attempts to set access criteria - │ └── Then it should revert - └── Given all the valid parameters are provided - └── When user attempts to set access criteria - └── Then it should not revert + │ + └── Given user has FUNDING_POT_ADMIN_ROLE + ├── Given round does not exist + │ └── When user attempts to set access criteria + │ └── Then it should revert + │ + ├── Given round is active + │ └── When user attempts to set access criteria + │ └── Then it should revert + │ + ├── Given AccessCriteriaId is NFT and nftContract is 0x0 + │ └── When user attempts to set access criteria + │ └── Then it should revert + │ + ├── Given AccessCriteriaId is MERKLE and merkleRoot is 0x0 + │ └── When user attempts to set access criteria + │ └── Then it should revert + │ + ├── Given AccessCriteriaId is LIST and allowedAddresses is empty + │ └── When user attempts to set access criteria + │ └── Then it should revert + │ + └── Given all the valid parameters are provided + └── When user attempts to set access criteria + └── Then it should not revert */ function testFuzzSetAccessCriteria_revertsGivenUserDoesNotHaveFundingPotAdminRole( @@ -805,7 +826,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { function testFuzzSetAccessCriteria_revertsGivenRoundDoesNotExist( uint8 accessCriteriaEnum ) public { - vm.assume(accessCriteriaEnum >= 0 && accessCriteriaEnum <= 3); + vm.assume(accessCriteriaEnum >= 1 && accessCriteriaEnum <= 4); uint64 roundId = fundingPot.getRoundCount(); @@ -825,7 +846,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { function testFuzzSetAccessCriteria_revertsGivenRoundIsActive( uint8 accessCriteriaEnum ) public { - vm.assume(accessCriteriaEnum >= 0 && accessCriteriaEnum <= 3); + vm.assume(accessCriteriaEnum >= 1 && accessCriteriaEnum <= 4); testCreateRound(); uint64 roundId = fundingPot.getRoundCount(); @@ -913,7 +934,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { } function testFuzzSetAccessCriteria(uint8 accessCriteriaEnum) public { - vm.assume(accessCriteriaEnum >= 0 && accessCriteriaEnum <= 3); + vm.assume(accessCriteriaEnum >= 1 && accessCriteriaEnum <= 4); testCreateRound(); uint64 roundId = fundingPot.getRoundCount(); @@ -931,37 +952,41 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { address[] memory allowedAddresses ) = fundingPot.getRoundAccessCriteria(roundId, accessCriteriaId); - assertEq(isOpen, accessCriteriaEnum == 0); + assertEq(isOpen, accessCriteriaEnum == 1); assertEq(nftContract, accessCriteria.nftContract); assertEq(merkleRoot, accessCriteria.merkleRoot); assertEq(allowedAddresses, accessCriteria.allowedAddresses); } - /* test editAccessCriteriaForRound() + /* Test editAccessCriteriaForRound() ├── Given user does not have FUNDING_POT_ADMIN_ROLE │ └── When user attempts to edit access criteria │ └── Then it should revert - ├── Given access criteria id is greater than the number of access criteria for the round - │ └── When user attempts to edit access criteria - │ └── Then it should revert - ├── Given round does not exist - │ └── When user attempts to set access criteria - │ └── Then it should revert - ├── Given round is active - │ └── When user attempts to set access criteria - │ └── Then it should revert - └── Given all the valid parameters are provided - └── When user attempts to edit access criteria - └── Then it should not revert - └── Then the access criteria should be updated - + │ + └── Given user has FUNDING_POT_ADMIN_ROLE + ├── Given access criteria id is greater than the number of access criteria for the round + │ └── When user attempts to edit access criteria + │ └── Then it should revert + │ + ├── Given round does not exist + │ └── When user attempts to edit access criteria + │ └── Then it should revert + │ + ├── Given round is active + │ └── When user attempts to edit access criteria + │ └── Then it should revert + │ + └── Given all valid parameters are provided + └── When user attempts to edit access criteria + ├── Then it should not revert + └── Then the access criteria should be updated */ function testFuzzEditAccessCriteriaForRound_revertsGivenUserDoesNotHaveFundingPotAdminRole( uint8 accessCriteriaEnum, address user_ ) public { - vm.assume(accessCriteriaEnum >= 0 && accessCriteriaEnum <= 3); + vm.assume(accessCriteriaEnum >= 1 && accessCriteriaEnum <= 4); vm.assume(user_ != address(0) && user_ != address(this)); _helper_setupRoundWithAccessCriteria(accessCriteriaEnum); @@ -989,7 +1014,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { function testFuzzEditAccessCriteriaForRound_revertsGivenAccessCriteriaIdIsGreaterThanAccessCriteriaForTheRound( uint8 accessCriteriaEnum ) public { - vm.assume(accessCriteriaEnum >= 0 && accessCriteriaEnum <= 3); + vm.assume(accessCriteriaEnum >= 1 && accessCriteriaEnum <= 4); _helper_setupRoundWithAccessCriteria(accessCriteriaEnum); uint64 roundId = fundingPot.getRoundCount(); @@ -1013,7 +1038,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { function testFuzzEditAccessCriteriaForRound_revertsGivenRoundDoesNotExist( uint8 accessCriteriaEnum ) public { - vm.assume(accessCriteriaEnum >= 0 && accessCriteriaEnum <= 3); + vm.assume(accessCriteriaEnum >= 1 && accessCriteriaEnum <= 4); uint64 roundId = fundingPot.getRoundCount(); uint8 accessCriteriaId = 0; @@ -1029,7 +1054,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { function testFuzzEditAccessCriteriaForRound_revertsGivenRoundIsActive( uint8 accessCriteriaEnum ) public { - vm.assume(accessCriteriaEnum >= 0 && accessCriteriaEnum <= 3); + vm.assume(accessCriteriaEnum >= 1 && accessCriteriaEnum <= 4); _helper_setupRoundWithAccessCriteria(accessCriteriaEnum); uint64 roundId = fundingPot.getRoundCount(); @@ -1058,10 +1083,10 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { uint8 accessCriteriaEnumOld, uint8 accessCriteriaEnumNew ) public { - vm.assume(accessCriteriaEnumOld >= 0 && accessCriteriaEnumOld <= 3); + vm.assume(accessCriteriaEnumOld >= 1 && accessCriteriaEnumOld <= 4); vm.assume( accessCriteriaEnumNew != accessCriteriaEnumOld - && accessCriteriaEnumNew >= 0 && accessCriteriaEnumNew <= 3 + && accessCriteriaEnumNew >= 1 && accessCriteriaEnumNew <= 4 ); _helper_setupRoundWithAccessCriteria(accessCriteriaEnumOld); @@ -1084,7 +1109,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { address[] memory allowedAddresses ) = fundingPot.getRoundAccessCriteria(roundId, accessCriteriaId); - assertEq(isOpen, accessCriteriaEnumNew == 0); + assertEq(isOpen, accessCriteriaEnumNew == 1); assertEq(nftContract, newAccessCriteria.nftContract); assertEq(merkleRoot, newAccessCriteria.merkleRoot); assertEq(allowedAddresses, newAccessCriteria.allowedAddresses); From 72828fad77a915d778cb8d1e77fa4416535a9cfc Mon Sep 17 00:00:00 2001 From: Leandro Faria Date: Thu, 3 Apr 2025 13:11:50 -0500 Subject: [PATCH 031/130] fix:remove helper functions, fix stack too deep --- .../logicModule/LM_PC_FundingPot_v1.t.sol | 732 ++++++++---------- 1 file changed, 315 insertions(+), 417 deletions(-) diff --git a/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol b/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol index 1a9d50ce3..1dd4afdc2 100644 --- a/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol +++ b/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol @@ -50,6 +50,26 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { // SuT LM_PC_FundingPot_v1_Exposed fundingPot; + // Storage variables to avoid stack too deep + RoundParams private _testParams; + RoundParams private _storedParams; + uint64 private _testRoundId; + + // Default round parameters for testing + RoundParams private _defaultRoundParams; + RoundParams private _editedRoundParams; + + // Struct to hold round parameters + struct RoundParams { + uint roundStart; + uint roundEnd; + uint roundCap; + address hookContract; + bytes hookFunction; + bool autoClosure; + bool globalAccumulativeCaps; + } + // ------------------------------------------------------------------------- // Setup function setUp() public { @@ -67,6 +87,28 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { // Set the block timestamp vm.warp(block.timestamp + _orchestrator.MODULE_UPDATE_TIMELOCK()); + + // Initialize default round parameters + _defaultRoundParams = RoundParams({ + roundStart: block.timestamp + 1 days, + roundEnd: block.timestamp + 2 days, + roundCap: 1000, + hookContract: address(0), + hookFunction: bytes(""), + autoClosure: false, + globalAccumulativeCaps: false + }); + + // Initialize edited round parameters + _editedRoundParams = RoundParams({ + roundStart: block.timestamp + 3 days, + roundEnd: block.timestamp + 4 days, + roundCap: 2000, + hookContract: address(0x1), + hookFunction: bytes("test"), + autoClosure: true, + globalAccumulativeCaps: true + }); } // ------------------------------------------------------------------------- @@ -140,23 +182,15 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { IModule_v1.Module__CallerNotAuthorized.selector, roleId, user_ ) ); - ( - uint roundStart, - uint roundEnd, - uint roundCap, - address hookContract, - bytes memory hookFunction, - bool autoClosure, - bool globalAccumulativeCaps - ) = _helper_createDefaultFundingRound(); - _helper_callCreateRound( - roundStart, - roundEnd, - roundCap, - hookContract, - hookFunction, - autoClosure, - globalAccumulativeCaps + RoundParams memory params = _helper_createDefaultFundingRound(); + fundingPot.createRound( + params.roundStart, + params.roundEnd, + params.roundCap, + params.hookContract, + params.hookFunction, + params.autoClosure, + params.globalAccumulativeCaps ); vm.stopPrank(); } @@ -165,16 +199,8 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { public { vm.assume(roundStart_ < block.timestamp); - ( - uint roundStart, - uint roundEnd, - uint roundCap, - address hookContract, - bytes memory hookFunction, - bool autoClosure, - bool globalAccumulativeCaps - ) = _helper_createDefaultFundingRound(); - roundStart = roundStart_; + RoundParams memory params = _helper_createDefaultFundingRound(); + params.roundStart = roundStart_; vm.expectRevert( abi.encodeWithSelector( ILM_PC_FundingPot_v1 @@ -182,31 +208,23 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { .selector ) ); - _helper_callCreateRound( - roundStart, - roundEnd, - roundCap, - hookContract, - hookFunction, - autoClosure, - globalAccumulativeCaps + fundingPot.createRound( + params.roundStart, + params.roundEnd, + params.roundCap, + params.hookContract, + params.hookFunction, + params.autoClosure, + params.globalAccumulativeCaps ); } function testCreateRound_revertsGivenRoundEndTimeAndCapAreBothZero() public { - ( - uint roundStart, - uint roundEnd, - uint roundCap, - address hookContract, - bytes memory hookFunction, - bool autoClosure, - bool globalAccumulativeCaps - ) = _helper_createDefaultFundingRound(); - roundEnd = 0; - roundCap = 0; + RoundParams memory params = _helper_createDefaultFundingRound(); + params.roundEnd = 0; + params.roundCap = 0; vm.expectRevert( abi.encodeWithSelector( ILM_PC_FundingPot_v1 @@ -214,31 +232,23 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { .selector ) ); - _helper_callCreateRound( - roundStart, - roundEnd, - roundCap, - hookContract, - hookFunction, - autoClosure, - globalAccumulativeCaps + fundingPot.createRound( + params.roundStart, + params.roundEnd, + params.roundCap, + params.hookContract, + params.hookFunction, + params.autoClosure, + params.globalAccumulativeCaps ); } function testCreateRound_revertsGivenRoundEndTimeIsBeforeRoundStart( uint roundEnd_ ) public { - ( - uint roundStart, - uint roundEnd, - uint roundCap, - address hookContract, - bytes memory hookFunction, - bool autoClosure, - bool globalAccumulativeCaps - ) = _helper_createDefaultFundingRound(); - vm.assume(roundEnd_ != 0 && roundEnd_ < roundStart); - roundEnd = roundEnd_; + RoundParams memory params = _helper_createDefaultFundingRound(); + vm.assume(roundEnd_ != 0 && roundEnd_ < params.roundStart); + params.roundEnd = roundEnd_; vm.expectRevert( abi.encodeWithSelector( ILM_PC_FundingPot_v1 @@ -246,30 +256,22 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { .selector ) ); - _helper_callCreateRound( - roundStart, - roundEnd, - roundCap, - hookContract, - hookFunction, - autoClosure, - globalAccumulativeCaps + fundingPot.createRound( + params.roundStart, + params.roundEnd, + params.roundCap, + params.hookContract, + params.hookFunction, + params.autoClosure, + params.globalAccumulativeCaps ); } function testCreateRound_revertsGivenHookContractIsSetButHookFunctionIsEmpty( ) public { - ( - uint roundStart, - uint roundEnd, - uint roundCap, - address hookContract, - bytes memory hookFunction, - bool autoClosure, - bool globalAccumulativeCaps - ) = _helper_createDefaultFundingRound(); - hookContract = address(1); - hookFunction = bytes(""); + RoundParams memory params = _helper_createDefaultFundingRound(); + params.hookContract = address(1); + params.hookFunction = bytes(""); vm.expectRevert( abi.encodeWithSelector( ILM_PC_FundingPot_v1 @@ -277,30 +279,22 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { .selector ) ); - _helper_callCreateRound( - roundStart, - roundEnd, - roundCap, - hookContract, - hookFunction, - autoClosure, - globalAccumulativeCaps + fundingPot.createRound( + params.roundStart, + params.roundEnd, + params.roundCap, + params.hookContract, + params.hookFunction, + params.autoClosure, + params.globalAccumulativeCaps ); } function testCreateRound_revertsGivenHookFunctionIsSetButHookContractIsEmpty( ) public { - ( - uint roundStart, - uint roundEnd, - uint roundCap, - address hookContract, - bytes memory hookFunction, - bool autoClosure, - bool globalAccumulativeCaps - ) = _helper_createDefaultFundingRound(); - hookContract = address(0); - hookFunction = bytes("test"); + RoundParams memory params = _helper_createDefaultFundingRound(); + params.hookContract = address(0); + params.hookFunction = bytes("test"); vm.expectRevert( abi.encodeWithSelector( ILM_PC_FundingPot_v1 @@ -308,14 +302,14 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { .selector ) ); - _helper_callCreateRound( - roundStart, - roundEnd, - roundCap, - hookContract, - hookFunction, - autoClosure, - globalAccumulativeCaps + fundingPot.createRound( + params.roundStart, + params.roundEnd, + params.roundCap, + params.hookContract, + params.hookFunction, + params.autoClosure, + params.globalAccumulativeCaps ); } @@ -326,43 +320,36 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { */ function testCreateRound() public { - ( - uint roundStart, - uint roundEnd, - uint roundCap, - address hookContract, - bytes memory hookFunction, - bool autoClosure, - bool globalAccumulativeCaps - ) = _helper_createDefaultFundingRound(); - _helper_callCreateRound( - roundStart, - roundEnd, - roundCap, - hookContract, - hookFunction, - autoClosure, - globalAccumulativeCaps + _testParams = _defaultRoundParams; + + fundingPot.createRound( + _testParams.roundStart, + _testParams.roundEnd, + _testParams.roundCap, + _testParams.hookContract, + _testParams.hookFunction, + _testParams.autoClosure, + _testParams.globalAccumulativeCaps ); - uint64 lastRoundId = fundingPot.getRoundCount(); + _testRoundId = fundingPot.getRoundCount(); ( - uint roundStart_, - uint roundEnd_, - uint roundCap_, - address hookContract_, - bytes memory hookFunction_, - bool autoClosure_, - bool globalAccumulativeCaps_ - ) = fundingPot.getRoundGenericParameters(lastRoundId); - - assertEq(roundStart, roundStart_); - assertEq(roundEnd, roundEnd_); - assertEq(roundCap, roundCap_); - assertEq(hookContract, hookContract_); - assertEq(hookFunction, hookFunction_); - assertEq(autoClosure, autoClosure_); - assertEq(globalAccumulativeCaps, globalAccumulativeCaps_); + _storedParams.roundStart, + _storedParams.roundEnd, + _storedParams.roundCap, + _storedParams.hookContract, + _storedParams.hookFunction, + _storedParams.autoClosure, + _storedParams.globalAccumulativeCaps + ) = fundingPot.getRoundGenericParameters(_testRoundId); + + assertEq(_storedParams.roundStart, _testParams.roundStart); + assertEq(_storedParams.roundEnd, _testParams.roundEnd); + assertEq(_storedParams.roundCap, _testParams.roundCap); + assertEq(_storedParams.hookContract, _testParams.hookContract); + assertEq(_storedParams.hookFunction, _testParams.hookFunction); + assertEq(_storedParams.autoClosure, _testParams.autoClosure); + assertEq(_storedParams.globalAccumulativeCaps, _testParams.globalAccumulativeCaps); } /* Test editRound() @@ -412,6 +399,16 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { uint64 roundId = fundingPot.getRoundCount(); + RoundParams memory params = RoundParams({ + roundStart: block.timestamp + 3 days, + roundEnd: block.timestamp + 4 days, + roundCap: 2000, + hookContract: address(0x1), + hookFunction: bytes("test"), + autoClosure: true, + globalAccumulativeCaps: true + }); + vm.startPrank(user_); bytes32 roleId = _authorizer.generateRoleId( address(fundingPot), fundingPot.FUNDING_POT_ADMIN_ROLE() @@ -421,25 +418,15 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { IModule_v1.Module__CallerNotAuthorized.selector, roleId, user_ ) ); - ( - uint roundStart_, - uint roundEnd_, - uint roundCap_, - address hookContract_, - bytes memory hookFunction_, - bool autoClosure_, - bool globalAccumulativeCaps_ - ) = _helper_createEditedRoundParams(); - - _helper_callEditRound( - 0, - roundStart_, - roundEnd_, - roundCap_, - hookContract_, - hookFunction_, - autoClosure_, - globalAccumulativeCaps_ + fundingPot.editRound( + roundId, + params.roundStart, + params.roundEnd, + params.roundCap, + params.hookContract, + params.hookFunction, + params.autoClosure, + params.globalAccumulativeCaps ); vm.stopPrank(); } @@ -449,15 +436,15 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { uint64 roundId = fundingPot.getRoundCount(); - ( - uint roundStart_, - uint roundEnd_, - uint roundCap_, - address hookContract_, - bytes memory hookFunction_, - bool autoClosure_, - bool globalAccumulativeCaps_ - ) = _helper_createEditedRoundParams(); + RoundParams memory params = RoundParams({ + roundStart: block.timestamp + 3 days, + roundEnd: block.timestamp + 4 days, + roundCap: 2000, + hookContract: address(0x1), + hookFunction: bytes("test"), + autoClosure: true, + globalAccumulativeCaps: true + }); vm.expectRevert( abi.encodeWithSelector( @@ -466,15 +453,15 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { .selector ) ); - _helper_callEditRound( + fundingPot.editRound( roundId + 1, - roundStart_, - roundEnd_, - roundCap_, - hookContract_, - hookFunction_, - autoClosure_, - globalAccumulativeCaps_ + params.roundStart, + params.roundEnd, + params.roundCap, + params.hookContract, + params.hookFunction, + params.autoClosure, + params.globalAccumulativeCaps ); } @@ -482,26 +469,28 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { testCreateRound(); uint64 roundId = fundingPot.getRoundCount(); + RoundParams memory params; ( - uint roundStart, - uint roundEnd, - uint roundCap, - address hookContract, - bytes memory hookFunction, - bool autoClosure, - bool globalAccumulativeCaps + params.roundStart, + params.roundEnd, + params.roundCap, + params.hookContract, + params.hookFunction, + params.autoClosure, + params.globalAccumulativeCaps ) = fundingPot.getRoundGenericParameters(roundId); - vm.warp(roundStart + 1); + vm.warp(params.roundStart + 1); + + RoundParams memory params_ = RoundParams({ + roundStart: block.timestamp + 3 days, + roundEnd: block.timestamp + 4 days, + roundCap: 2000, + hookContract: address(0x1), + hookFunction: bytes("test"), + autoClosure: true, + globalAccumulativeCaps: true + }); - ( - uint roundStart_, - uint roundEnd_, - uint roundCap_, - address hookContract_, - bytes memory hookFunction_, - bool autoClosure_, - bool globalAccumulativeCaps_ - ) = _helper_createEditedRoundParams(); vm.expectRevert( abi.encodeWithSelector( ILM_PC_FundingPot_v1 @@ -509,15 +498,15 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { .selector ) ); - _helper_callEditRound( + fundingPot.editRound( roundId, - roundStart_, - roundEnd_, - roundCap_, - hookContract_, - hookFunction_, - autoClosure_, - globalAccumulativeCaps_ + params_.roundStart, + params_.roundEnd, + params_.roundCap, + params_.hookContract, + params_.hookFunction, + params_.autoClosure, + params_.globalAccumulativeCaps ); } @@ -528,16 +517,15 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { uint64 roundId = fundingPot.getRoundCount(); vm.assume(roundStartP_ < block.timestamp); - ( - uint roundStart_, - uint roundEnd_, - uint roundCap_, - address hookContract_, - bytes memory hookFunction_, - bool autoClosure_, - bool globalAccumulativeCaps_ - ) = _helper_createEditedRoundParams(); - roundStart_ = roundStartP_; + RoundParams memory params = RoundParams({ + roundStart: roundStartP_, + roundEnd: block.timestamp + 4 days, + roundCap: 2000, + hookContract: address(0x1), + hookFunction: bytes("test"), + autoClosure: true, + globalAccumulativeCaps: true + }); vm.expectRevert( abi.encodeWithSelector( @@ -547,15 +535,15 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { ) ); - _helper_callEditRound( + fundingPot.editRound( roundId, - roundStart_, - roundEnd_, - roundCap_, - hookContract_, - hookFunction_, - autoClosure_, - globalAccumulativeCaps_ + params.roundStart, + params.roundEnd, + params.roundCap, + params.hookContract, + params.hookFunction, + params.autoClosure, + params.globalAccumulativeCaps ); } @@ -563,17 +551,15 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { testCreateRound(); uint64 roundId = fundingPot.getRoundCount(); - ( - uint roundStart_, - uint roundEnd_, - uint roundCap_, - address hookContract_, - bytes memory hookFunction_, - bool autoClosure_, - bool globalAccumulativeCaps_ - ) = _helper_createEditedRoundParams(); - roundEnd_ = 0; - roundCap_ = 0; + RoundParams memory params = RoundParams({ + roundStart: block.timestamp + 3 days, + roundEnd: 0, + roundCap: 0, + hookContract: address(0x1), + hookFunction: bytes("test"), + autoClosure: true, + globalAccumulativeCaps: true + }); vm.expectRevert( abi.encodeWithSelector( @@ -583,15 +569,15 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { ) ); - _helper_callEditRound( + fundingPot.editRound( roundId, - roundStart_, - roundEnd_, - roundCap_, - hookContract_, - hookFunction_, - autoClosure_, - globalAccumulativeCaps_ + params.roundStart, + params.roundEnd, + params.roundCap, + params.hookContract, + params.hookFunction, + params.autoClosure, + params.globalAccumulativeCaps ); } @@ -601,16 +587,24 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { testCreateRound(); uint64 roundId = fundingPot.getRoundCount(); - ( - uint roundStart_, - uint roundEnd_, - uint roundCap_, - address hookContract_, - bytes memory hookFunction_, - bool autoClosure_, - bool globalAccumulativeCaps_ - ) = _helper_createEditedRoundParams(); - roundEnd_ = bound(roundEnd_, 0, roundStart_ - 1); + RoundParams memory params = RoundParams({ + roundStart: block.timestamp + 3 days, + roundEnd: roundEnd_, + roundCap: 2000, + hookContract: address(0x1), + hookFunction: bytes("test"), + autoClosure: true, + globalAccumulativeCaps: true + }); + + // Get the current round start time + (uint currentRoundStart,,,,,,) = fundingPot.getRoundGenericParameters(roundId); + + // Ensure roundEnd_ is less than current round start + vm.assume(roundEnd_ < currentRoundStart); + vm.assume(roundEnd_ != 0); + params.roundEnd = roundEnd_; + params.roundStart = currentRoundStart; vm.expectRevert( abi.encodeWithSelector( @@ -620,15 +614,15 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { ) ); - _helper_callEditRound( + fundingPot.editRound( roundId, - roundStart_, - roundEnd_, - roundCap_, - hookContract_, - hookFunction_, - autoClosure_, - globalAccumulativeCaps_ + params.roundStart, + params.roundEnd, + params.roundCap, + params.hookContract, + params.hookFunction, + params.autoClosure, + params.globalAccumulativeCaps ); } @@ -638,17 +632,15 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { testCreateRound(); uint64 roundId = fundingPot.getRoundCount(); - ( - uint roundStart_, - uint roundEnd_, - uint roundCap_, - address hookContract_, - bytes memory hookFunction_, - bool autoClosure_, - bool globalAccumulativeCaps_ - ) = _helper_createEditedRoundParams(); - hookContract_ = address(1); - hookFunction_ = bytes(""); + RoundParams memory params = RoundParams({ + roundStart: block.timestamp + 3 days, + roundEnd: block.timestamp + 4 days, + roundCap: 2000, + hookContract: address(1), + hookFunction: bytes(""), + autoClosure: true, + globalAccumulativeCaps: true + }); vm.expectRevert( abi.encodeWithSelector( @@ -658,15 +650,15 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { ) ); - _helper_callEditRound( + fundingPot.editRound( roundId, - roundStart_, - roundEnd_, - roundCap_, - hookContract_, - hookFunction_, - autoClosure_, - globalAccumulativeCaps_ + params.roundStart, + params.roundEnd, + params.roundCap, + params.hookContract, + params.hookFunction, + params.autoClosure, + params.globalAccumulativeCaps ); } @@ -676,17 +668,15 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { testCreateRound(); uint64 roundId = fundingPot.getRoundCount(); - ( - uint roundStart_, - uint roundEnd_, - uint roundCap_, - address hookContract_, - bytes memory hookFunction_, - bool autoClosure_, - bool globalAccumulativeCaps_ - ) = _helper_createEditedRoundParams(); - hookContract_ = address(0); - hookFunction_ = bytes("test"); + RoundParams memory params = RoundParams({ + roundStart: block.timestamp + 3 days, + roundEnd: block.timestamp + 4 days, + roundCap: 2000, + hookContract: address(0), + hookFunction: bytes("test"), + autoClosure: true, + globalAccumulativeCaps: true + }); vm.expectRevert( abi.encodeWithSelector( @@ -696,15 +686,15 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { ) ); - _helper_callEditRound( + fundingPot.editRound( roundId, - roundStart_, - roundEnd_, - roundCap_, - hookContract_, - hookFunction_, - autoClosure_, - globalAccumulativeCaps_ + params.roundStart, + params.roundEnd, + params.roundCap, + params.hookContract, + params.hookFunction, + params.autoClosure, + params.globalAccumulativeCaps ); } @@ -726,44 +716,36 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { testCreateRound(); uint64 lastRoundId = fundingPot.getRoundCount(); - ( - uint roundStart_, - uint roundEnd_, - uint roundCap_, - address hookContract_, - bytes memory hookFunction_, - bool autoClosure_, - bool globalAccumulativeCaps_ - ) = _helper_createEditedRoundParams(); - - _helper_callEditRound( + _testParams = _editedRoundParams; + + fundingPot.editRound( lastRoundId, - roundStart_, - roundEnd_, - roundCap_, - hookContract_, - hookFunction_, - autoClosure_, - globalAccumulativeCaps_ + _testParams.roundStart, + _testParams.roundEnd, + _testParams.roundCap, + _testParams.hookContract, + _testParams.hookFunction, + _testParams.autoClosure, + _testParams.globalAccumulativeCaps ); ( - uint roundStart, - uint roundEnd, - uint roundCap, - address hookContract, - bytes memory hookFunction, - bool autoClosure, - bool globalAccumulativeCaps + _storedParams.roundStart, + _storedParams.roundEnd, + _storedParams.roundCap, + _storedParams.hookContract, + _storedParams.hookFunction, + _storedParams.autoClosure, + _storedParams.globalAccumulativeCaps ) = fundingPot.getRoundGenericParameters(lastRoundId); - assertEq(roundStart, roundStart_); - assertEq(roundEnd, roundEnd_); - assertEq(roundCap, roundCap_); - assertEq(hookContract, hookContract_); - assertEq(hookFunction, hookFunction_); - assertEq(autoClosure, autoClosure_); - assertEq(globalAccumulativeCaps, globalAccumulativeCaps_); + assertEq(_storedParams.roundStart, _testParams.roundStart); + assertEq(_storedParams.roundEnd, _testParams.roundEnd); + assertEq(_storedParams.roundCap, _testParams.roundCap); + assertEq(_storedParams.hookContract, _testParams.hookContract); + assertEq(_storedParams.hookFunction, _testParams.hookFunction); + assertEq(_storedParams.autoClosure, _testParams.autoClosure); + assertEq(_storedParams.globalAccumulativeCaps, _testParams.globalAccumulativeCaps); } /* Test setAccessCriteria() @@ -1124,93 +1106,9 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { // @notice Creates a default funding round function _helper_createDefaultFundingRound() internal - returns (uint, uint, uint, address, bytes memory, bool, bool) - { - uint roundStart = block.timestamp + 1 days; - uint roundEnd = block.timestamp + 2 days; - uint roundCap = 1000; - address hookContract = address(0); - bytes memory hookFunction = bytes(""); - bool autoClosure = false; - bool globalAccumulativeCaps = false; - - return ( - roundStart, - roundEnd, - roundCap, - hookContract, - hookFunction, - autoClosure, - globalAccumulativeCaps - ); - } - - // @notice calls the create round function - function _helper_callCreateRound( - uint roundStart, - uint roundEnd, - uint roundCap, - address hookContract, - bytes memory hookFunction, - bool autoClosure, - bool globalAccumulativeCaps - ) internal { - fundingPot.createRound( - roundStart, - roundEnd, - roundCap, - hookContract, - hookFunction, - autoClosure, - globalAccumulativeCaps - ); - } - - // @notice Creates a predefined funding round with edited parameters for testing - function _helper_createEditedRoundParams() - internal - returns (uint, uint, uint, address, bytes memory, bool, bool) + returns (RoundParams memory) { - uint roundStart_ = block.timestamp + 3 days; - uint roundEnd_ = block.timestamp + 4 days; - uint roundCap_ = 2000; - address hookContract_ = address(0x1); - bytes memory hookFunction_ = bytes("test"); - bool autoClosure_ = true; - bool globalAccumulativeCaps_ = true; - - return ( - roundStart_, - roundEnd_, - roundCap_, - hookContract_, - hookFunction_, - autoClosure_, - globalAccumulativeCaps_ - ); - } - - // @notice calls the create round function - function _helper_callEditRound( - uint64 roundId, - uint roundStart, - uint roundEnd, - uint roundCap, - address hookContract, - bytes memory hookFunction, - bool autoClosure, - bool globalAccumulativeCaps - ) internal { - fundingPot.editRound( - roundId, - roundStart, - roundEnd, - roundCap, - hookContract, - hookFunction, - autoClosure, - globalAccumulativeCaps - ); + return _defaultRoundParams; } function _helper_createAccessCriteria(uint8 accessCriteriaEnum) From e27dd6e46a4a9fc875812a3de4af5315fc3f144a Mon Sep 17 00:00:00 2001 From: Leandro Faria Date: Thu, 3 Apr 2025 16:38:46 -0500 Subject: [PATCH 032/130] fix:support interface issue and redundant param variables --- .../logicModule/LM_PC_FundingPot_v1.sol | 5 +- .../interfaces/ILM_PC_FundingPot_v1.sol | 24 +- .../logicModule/LM_PC_FundingPot_v1.t.sol | 218 ++++++++++-------- 3 files changed, 133 insertions(+), 114 deletions(-) diff --git a/src/modules/logicModule/LM_PC_FundingPot_v1.sol b/src/modules/logicModule/LM_PC_FundingPot_v1.sol index a30c4165d..d9ddce1b4 100644 --- a/src/modules/logicModule/LM_PC_FundingPot_v1.sol +++ b/src/modules/logicModule/LM_PC_FundingPot_v1.sol @@ -168,8 +168,7 @@ contract LM_PC_FundingPot_v1 is AccessCriteria storage accessCriteria = round.accessCriterias[id_]; isOpen = ( - accessCriteria.accessCriteriaType == AccessCriteriaType.UNSET - || accessCriteria.accessCriteriaType == AccessCriteriaType.OPEN + accessCriteria.accessCriteriaType == AccessCriteriaType.OPEN ); return ( isOpen, @@ -360,7 +359,7 @@ contract LM_PC_FundingPot_v1 is } } - /// @notice Validates the round parameters. + /// @notice Validates the round parameters before editing. /// @param round_ The round to validate. /// @dev Reverts if the round parameters are invalid. function _validateEditRoundParameters(Round storage round_) internal view { diff --git a/src/modules/logicModule/interfaces/ILM_PC_FundingPot_v1.sol b/src/modules/logicModule/interfaces/ILM_PC_FundingPot_v1.sol index 743eb78d9..64db3b02a 100644 --- a/src/modules/logicModule/interfaces/ILM_PC_FundingPot_v1.sol +++ b/src/modules/logicModule/interfaces/ILM_PC_FundingPot_v1.sol @@ -65,7 +65,7 @@ interface ILM_PC_FundingPot_v1 is IERC20PaymentClientBase_v2 { /// @param roundCap_ The maximum allocation or cap for the round. /// @param hookContract_ The address of an optional hook contract for custom logic. /// @param hookFunction_ The encoded function call for the hook. - /// @param closureMechanism_ A boolean indicating whether a specific closure mechanism is enabled. + /// @param autoClosure_ A boolean indicating whether a specific closure mechanism is enabled. /// @param globalAccumulativeCaps_ A boolean indicating whether global accumulative caps are enforced. event RoundCreated( uint indexed roundId_, @@ -74,7 +74,7 @@ interface ILM_PC_FundingPot_v1 is IERC20PaymentClientBase_v2 { uint roundCap_, address hookContract_, bytes hookFunction_, - bool closureMechanism_, + bool autoClosure_, bool globalAccumulativeCaps_ ); @@ -86,7 +86,7 @@ interface ILM_PC_FundingPot_v1 is IERC20PaymentClientBase_v2 { /// @param roundCap_ The updated maximum allocation or cap for the round. /// @param hookContract_ The address of an optional hook contract for custom logic. /// @param hookFunction_ The updated encoded function call for the hook. - /// @param closureMechanism_ A boolean indicating whether a specific closure mechanism is enabled. + /// @param autoClosure_ A boolean indicating whether a specific closure mechanism is enabled. /// @param globalAccumulativeCaps_ A boolean indicating whether global accumulative caps are enforced. event RoundEdited( uint indexed roundId_, @@ -95,7 +95,7 @@ interface ILM_PC_FundingPot_v1 is IERC20PaymentClientBase_v2 { uint roundCap_, address hookContract_, bytes hookFunction_, - bool closureMechanism_, + bool autoClosure_, bool globalAccumulativeCaps_ ); @@ -161,7 +161,7 @@ interface ILM_PC_FundingPot_v1 is IERC20PaymentClientBase_v2 { /// @return roundCap_ The maximum contribution cap for the round. /// @return hookContract_ The address of the hook contract. /// @return hookFunction_ The encoded function call for the hook. - /// @return closureMechanism_ Whether hook closure coincides with contribution span end. + /// @return autoClosure_ Whether hook closure coincides with contribution span end. /// @return globalAccumulativeCaps_ Whether caps accumulate globally across rounds. function getRoundGenericParameters(uint64 roundId_) external @@ -172,18 +172,18 @@ interface ILM_PC_FundingPot_v1 is IERC20PaymentClientBase_v2 { uint roundCap_, address hookContract_, bytes memory hookFunction_, - bool closureMechanism_, + bool autoClosure_, bool globalAccumulativeCaps_ ); /// @notice Retrieves the access criteria for a specific funding round. /// @param roundId_ The unique identifier of the round to retrieve. - /// @param id_ The identifier of the access criteria to retrieve. + /// @param accessCriteriaId_ The identifier of the access criteria to retrieve. /// @return isOpen_ Whether the access criteria is open. /// @return nftContract_ The address of the NFT contract used for access control. /// @return merkleRoot_ The merkle root used for access verification. /// @return allowedAddresses_ The list of explicitly allowed addresses. - function getRoundAccessCriteria(uint64 roundId_, uint8 id_) + function getRoundAccessCriteria(uint64 roundId_, uint8 accessCriteriaId_) external view returns ( @@ -215,7 +215,7 @@ interface ILM_PC_FundingPot_v1 is IERC20PaymentClientBase_v2 { /// @param roundCap_ Maximum contribution cap in collateral tokens (0 if using roundEnd only). /// @param hookContract_ Address of contract to call after round closure. /// @param hookFunction_ Encoded function call for the hook. - /// @param closureMechanism_ Whether hook closure coincides with contribution span end. + /// @param autoClosure_ Whether hook closure coincides with contribution span end. /// @param globalAccumulativeCaps_ Whether caps accumulate globally. /// @return The ID of the newly created round. function createRound( @@ -224,7 +224,7 @@ interface ILM_PC_FundingPot_v1 is IERC20PaymentClientBase_v2 { uint roundCap_, address hookContract_, bytes memory hookFunction_, - bool closureMechanism_, + bool autoClosure_, bool globalAccumulativeCaps_ ) external returns (uint64); @@ -236,7 +236,7 @@ interface ILM_PC_FundingPot_v1 is IERC20PaymentClientBase_v2 { /// @param roundCap_ New maximum contribution cap. /// @param hookContract_ New hook contract address. /// @param hookFunction_ New encoded function call. - /// @param closureMechanism_ New closure mechanism setting. + /// @param autoClosure_ New closure mechanism setting. /// @param globalAccumulativeCaps_ New global accumulative caps setting. function editRound( uint64 roundId_, @@ -245,7 +245,7 @@ interface ILM_PC_FundingPot_v1 is IERC20PaymentClientBase_v2 { uint roundCap_, address hookContract_, bytes memory hookFunction_, - bool closureMechanism_, + bool autoClosure_, bool globalAccumulativeCaps_ ) external; diff --git a/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol b/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol index 1dd4afdc2..80e5f8741 100644 --- a/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol +++ b/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol @@ -51,8 +51,6 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { LM_PC_FundingPot_v1_Exposed fundingPot; // Storage variables to avoid stack too deep - RoundParams private _testParams; - RoundParams private _storedParams; uint64 private _testRoundId; // Default round parameters for testing @@ -100,15 +98,15 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { }); // Initialize edited round parameters - _editedRoundParams = RoundParams({ - roundStart: block.timestamp + 3 days, - roundEnd: block.timestamp + 4 days, - roundCap: 2000, - hookContract: address(0x1), - hookFunction: bytes("test"), - autoClosure: true, - globalAccumulativeCaps: true - }); + _editedRoundParams = _helper_createEditRoundParams( + block.timestamp + 3 days, + block.timestamp + 4 days, + 2000, + address(0x1), + bytes("test"), + true, + true + ); } // ------------------------------------------------------------------------- @@ -122,9 +120,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { assertTrue( fundingPot.supportsInterface(type(ILM_PC_FundingPot_v1).interfaceId) ); - assertTrue( - fundingPot.supportsInterface(type(ILM_PC_FundingPot_v1).interfaceId) - ); + } function testReinitFails() public override(ModuleTest) { @@ -320,36 +316,39 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { */ function testCreateRound() public { - _testParams = _defaultRoundParams; + RoundParams memory params = _defaultRoundParams; fundingPot.createRound( - _testParams.roundStart, - _testParams.roundEnd, - _testParams.roundCap, - _testParams.hookContract, - _testParams.hookFunction, - _testParams.autoClosure, - _testParams.globalAccumulativeCaps + params.roundStart, + params.roundEnd, + params.roundCap, + params.hookContract, + params.hookFunction, + params.autoClosure, + params.globalAccumulativeCaps ); _testRoundId = fundingPot.getRoundCount(); + + // Retrieve the stored parameters ( - _storedParams.roundStart, - _storedParams.roundEnd, - _storedParams.roundCap, - _storedParams.hookContract, - _storedParams.hookFunction, - _storedParams.autoClosure, - _storedParams.globalAccumulativeCaps + uint storedRoundStart, + uint storedRoundEnd, + uint storedRoundCap, + address storedHookContract, + bytes memory storedHookFunction, + bool storedAutoClosure, + bool storedGlobalAccumulativeCaps ) = fundingPot.getRoundGenericParameters(_testRoundId); - assertEq(_storedParams.roundStart, _testParams.roundStart); - assertEq(_storedParams.roundEnd, _testParams.roundEnd); - assertEq(_storedParams.roundCap, _testParams.roundCap); - assertEq(_storedParams.hookContract, _testParams.hookContract); - assertEq(_storedParams.hookFunction, _testParams.hookFunction); - assertEq(_storedParams.autoClosure, _testParams.autoClosure); - assertEq(_storedParams.globalAccumulativeCaps, _testParams.globalAccumulativeCaps); + // Compare with expected values + assertEq(storedRoundStart, params.roundStart); + assertEq(storedRoundEnd, params.roundEnd); + assertEq(storedRoundCap, params.roundCap); + assertEq(storedHookContract, params.hookContract); + assertEq(storedHookFunction, params.hookFunction); + assertEq(storedAutoClosure, params.autoClosure); + assertEq(storedGlobalAccumulativeCaps, params.globalAccumulativeCaps); } /* Test editRound() @@ -587,24 +586,22 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { testCreateRound(); uint64 roundId = fundingPot.getRoundCount(); - RoundParams memory params = RoundParams({ - roundStart: block.timestamp + 3 days, - roundEnd: roundEnd_, - roundCap: 2000, - hookContract: address(0x1), - hookFunction: bytes("test"), - autoClosure: true, - globalAccumulativeCaps: true - }); - // Get the current round start time (uint currentRoundStart,,,,,,) = fundingPot.getRoundGenericParameters(roundId); // Ensure roundEnd_ is less than current round start vm.assume(roundEnd_ < currentRoundStart); vm.assume(roundEnd_ != 0); - params.roundEnd = roundEnd_; - params.roundStart = currentRoundStart; + + RoundParams memory params = _helper_createEditRoundParams( + currentRoundStart, + roundEnd_, + 2000, + address(0x1), + bytes("test"), + true, + true + ); vm.expectRevert( abi.encodeWithSelector( @@ -632,15 +629,15 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { testCreateRound(); uint64 roundId = fundingPot.getRoundCount(); - RoundParams memory params = RoundParams({ - roundStart: block.timestamp + 3 days, - roundEnd: block.timestamp + 4 days, - roundCap: 2000, - hookContract: address(1), - hookFunction: bytes(""), - autoClosure: true, - globalAccumulativeCaps: true - }); + RoundParams memory params = _helper_createEditRoundParams( + block.timestamp + 3 days, + block.timestamp + 4 days, + 2000, + address(1), + bytes(""), + true, + true + ); vm.expectRevert( abi.encodeWithSelector( @@ -668,15 +665,15 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { testCreateRound(); uint64 roundId = fundingPot.getRoundCount(); - RoundParams memory params = RoundParams({ - roundStart: block.timestamp + 3 days, - roundEnd: block.timestamp + 4 days, - roundCap: 2000, - hookContract: address(0), - hookFunction: bytes("test"), - autoClosure: true, - globalAccumulativeCaps: true - }); + RoundParams memory params = _helper_createEditRoundParams( + block.timestamp + 3 days, + block.timestamp + 4 days, + 2000, + address(0), + bytes("test"), + true, + true + ); vm.expectRevert( abi.encodeWithSelector( @@ -716,36 +713,38 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { testCreateRound(); uint64 lastRoundId = fundingPot.getRoundCount(); - _testParams = _editedRoundParams; + RoundParams memory params = _editedRoundParams; fundingPot.editRound( lastRoundId, - _testParams.roundStart, - _testParams.roundEnd, - _testParams.roundCap, - _testParams.hookContract, - _testParams.hookFunction, - _testParams.autoClosure, - _testParams.globalAccumulativeCaps + params.roundStart, + params.roundEnd, + params.roundCap, + params.hookContract, + params.hookFunction, + params.autoClosure, + params.globalAccumulativeCaps ); + // Retrieve the stored parameters ( - _storedParams.roundStart, - _storedParams.roundEnd, - _storedParams.roundCap, - _storedParams.hookContract, - _storedParams.hookFunction, - _storedParams.autoClosure, - _storedParams.globalAccumulativeCaps + uint storedRoundStart, + uint storedRoundEnd, + uint storedRoundCap, + address storedHookContract, + bytes memory storedHookFunction, + bool storedAutoClosure, + bool storedGlobalAccumulativeCaps ) = fundingPot.getRoundGenericParameters(lastRoundId); - assertEq(_storedParams.roundStart, _testParams.roundStart); - assertEq(_storedParams.roundEnd, _testParams.roundEnd); - assertEq(_storedParams.roundCap, _testParams.roundCap); - assertEq(_storedParams.hookContract, _testParams.hookContract); - assertEq(_storedParams.hookFunction, _testParams.hookFunction); - assertEq(_storedParams.autoClosure, _testParams.autoClosure); - assertEq(_storedParams.globalAccumulativeCaps, _testParams.globalAccumulativeCaps); + // Compare with expected values + assertEq(storedRoundStart, params.roundStart); + assertEq(storedRoundEnd, params.roundEnd); + assertEq(storedRoundCap, params.roundCap); + assertEq(storedHookContract, params.hookContract); + assertEq(storedHookFunction, params.hookFunction); + assertEq(storedAutoClosure, params.autoClosure); + assertEq(storedGlobalAccumulativeCaps, params.globalAccumulativeCaps); } /* Test setAccessCriteria() @@ -783,7 +782,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { uint8 accessCriteriaEnum_, address user_ ) public { - vm.assume(accessCriteriaEnum_ >= 0 && accessCriteriaEnum_ <= 3); + vm.assume(accessCriteriaEnum_ >= 0 && accessCriteriaEnum_ <= 4); vm.assume(user_ != address(0) && user_ != address(this)); testCreateRound(); @@ -808,7 +807,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { function testFuzzSetAccessCriteria_revertsGivenRoundDoesNotExist( uint8 accessCriteriaEnum ) public { - vm.assume(accessCriteriaEnum >= 1 && accessCriteriaEnum <= 4); + vm.assume(accessCriteriaEnum >= 0 && accessCriteriaEnum <= 4); uint64 roundId = fundingPot.getRoundCount(); @@ -828,7 +827,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { function testFuzzSetAccessCriteria_revertsGivenRoundIsActive( uint8 accessCriteriaEnum ) public { - vm.assume(accessCriteriaEnum >= 1 && accessCriteriaEnum <= 4); + vm.assume(accessCriteriaEnum >= 0 && accessCriteriaEnum <= 4); testCreateRound(); uint64 roundId = fundingPot.getRoundCount(); @@ -916,7 +915,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { } function testFuzzSetAccessCriteria(uint8 accessCriteriaEnum) public { - vm.assume(accessCriteriaEnum >= 1 && accessCriteriaEnum <= 4); + vm.assume(accessCriteriaEnum >= 0 && accessCriteriaEnum <= 4); testCreateRound(); uint64 roundId = fundingPot.getRoundCount(); @@ -968,7 +967,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { uint8 accessCriteriaEnum, address user_ ) public { - vm.assume(accessCriteriaEnum >= 1 && accessCriteriaEnum <= 4); + vm.assume(accessCriteriaEnum >= 0 && accessCriteriaEnum <= 4); vm.assume(user_ != address(0) && user_ != address(this)); _helper_setupRoundWithAccessCriteria(accessCriteriaEnum); @@ -996,11 +995,11 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { function testFuzzEditAccessCriteriaForRound_revertsGivenAccessCriteriaIdIsGreaterThanAccessCriteriaForTheRound( uint8 accessCriteriaEnum ) public { - vm.assume(accessCriteriaEnum >= 1 && accessCriteriaEnum <= 4); + vm.assume(accessCriteriaEnum >= 0 && accessCriteriaEnum <= 4); _helper_setupRoundWithAccessCriteria(accessCriteriaEnum); uint64 roundId = fundingPot.getRoundCount(); - uint8 accessCriteriaId = 1; // Invalid ID + uint8 accessCriteriaId = 10; // Invalid ID ILM_PC_FundingPot_v1.AccessCriteria memory accessCriteria = _helper_createAccessCriteria(accessCriteriaEnum); @@ -1020,7 +1019,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { function testFuzzEditAccessCriteriaForRound_revertsGivenRoundDoesNotExist( uint8 accessCriteriaEnum ) public { - vm.assume(accessCriteriaEnum >= 1 && accessCriteriaEnum <= 4); + vm.assume(accessCriteriaEnum >= 0 && accessCriteriaEnum <= 4); uint64 roundId = fundingPot.getRoundCount(); uint8 accessCriteriaId = 0; @@ -1036,7 +1035,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { function testFuzzEditAccessCriteriaForRound_revertsGivenRoundIsActive( uint8 accessCriteriaEnum ) public { - vm.assume(accessCriteriaEnum >= 1 && accessCriteriaEnum <= 4); + vm.assume(accessCriteriaEnum >= 0 && accessCriteriaEnum <= 4); _helper_setupRoundWithAccessCriteria(accessCriteriaEnum); uint64 roundId = fundingPot.getRoundCount(); @@ -1065,10 +1064,10 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { uint8 accessCriteriaEnumOld, uint8 accessCriteriaEnumNew ) public { - vm.assume(accessCriteriaEnumOld >= 1 && accessCriteriaEnumOld <= 4); + vm.assume(accessCriteriaEnumOld >= 0 && accessCriteriaEnumOld <= 4); vm.assume( accessCriteriaEnumNew != accessCriteriaEnumOld - && accessCriteriaEnumNew >= 1 && accessCriteriaEnumNew <= 4 + && accessCriteriaEnumNew >= 0 && accessCriteriaEnumNew <= 4 ); _helper_setupRoundWithAccessCriteria(accessCriteriaEnumOld); @@ -1111,6 +1110,27 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { return _defaultRoundParams; } + // @notice Creates edit round parameters with customizable values + function _helper_createEditRoundParams( + uint roundStart_, + uint roundEnd_, + uint roundCap_, + address hookContract_, + bytes memory hookFunction_, + bool autoClosure_, + bool globalAccumulativeCaps_ + ) internal returns (RoundParams memory) { + return RoundParams({ + roundStart: roundStart_, + roundEnd: roundEnd_, + roundCap: roundCap_, + hookContract: hookContract_, + hookFunction: hookFunction_, + autoClosure: autoClosure_, + globalAccumulativeCaps: globalAccumulativeCaps_ + }); + } + function _helper_createAccessCriteria(uint8 accessCriteriaEnum) internal returns (ILM_PC_FundingPot_v1.AccessCriteria memory) From b1359dfbe7a6ef7f95aa0cbb4d81587bcb37f22b Mon Sep 17 00:00:00 2001 From: Fabian Scherer Date: Mon, 7 Apr 2025 18:59:37 +0200 Subject: [PATCH 033/130] chore: fmt --- src/modules/logicModule/LM_PC_FundingPot_v1.sol | 4 +--- .../modules/logicModule/LM_PC_FundingPot_v1.t.sol | 12 ++++++------ 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/src/modules/logicModule/LM_PC_FundingPot_v1.sol b/src/modules/logicModule/LM_PC_FundingPot_v1.sol index d9ddce1b4..862598740 100644 --- a/src/modules/logicModule/LM_PC_FundingPot_v1.sol +++ b/src/modules/logicModule/LM_PC_FundingPot_v1.sol @@ -167,9 +167,7 @@ contract LM_PC_FundingPot_v1 is Round storage round = rounds[roundId_]; AccessCriteria storage accessCriteria = round.accessCriterias[id_]; - isOpen = ( - accessCriteria.accessCriteriaType == AccessCriteriaType.OPEN - ); + isOpen = (accessCriteria.accessCriteriaType == AccessCriteriaType.OPEN); return ( isOpen, accessCriteria.nftContract, diff --git a/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol b/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol index 80e5f8741..70db051ca 100644 --- a/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol +++ b/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol @@ -120,7 +120,6 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { assertTrue( fundingPot.supportsInterface(type(ILM_PC_FundingPot_v1).interfaceId) ); - } function testReinitFails() public override(ModuleTest) { @@ -317,7 +316,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { function testCreateRound() public { RoundParams memory params = _defaultRoundParams; - + fundingPot.createRound( params.roundStart, params.roundEnd, @@ -329,7 +328,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { ); _testRoundId = fundingPot.getRoundCount(); - + // Retrieve the stored parameters ( uint storedRoundStart, @@ -587,12 +586,13 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { uint64 roundId = fundingPot.getRoundCount(); // Get the current round start time - (uint currentRoundStart,,,,,,) = fundingPot.getRoundGenericParameters(roundId); - + (uint currentRoundStart,,,,,,) = + fundingPot.getRoundGenericParameters(roundId); + // Ensure roundEnd_ is less than current round start vm.assume(roundEnd_ < currentRoundStart); vm.assume(roundEnd_ != 0); - + RoundParams memory params = _helper_createEditRoundParams( currentRoundStart, roundEnd_, From 7a2dc31f1388e90b37bdcc322ccc9a3eb39b2f4b Mon Sep 17 00:00:00 2001 From: Zuhaib Mohammed Date: Thu, 27 Mar 2025 01:07:15 +0530 Subject: [PATCH 034/130] feat: setAccessCriteriaPrivilages() implementation --- .../logicModule/LM_PC_FundingPot_v1.sol | 104 +++++++++++++++++- .../interfaces/ILM_PC_FundingPot_v1.sol | 50 ++++++++- .../logicModule/LM_PC_FundingPot_v1.t.sol | 1 - 3 files changed, 148 insertions(+), 7 deletions(-) diff --git a/src/modules/logicModule/LM_PC_FundingPot_v1.sol b/src/modules/logicModule/LM_PC_FundingPot_v1.sol index 862598740..cdb2b311b 100644 --- a/src/modules/logicModule/LM_PC_FundingPot_v1.sol +++ b/src/modules/logicModule/LM_PC_FundingPot_v1.sol @@ -89,6 +89,10 @@ contract LM_PC_FundingPot_v1 is /// @notice Stores the access criteria ID for each round. mapping(uint64 => uint8) private roundIdtoAccessId; + /// @notice Stores all access criteria privilages by their unique ID. + mapping( + uint64 roundId => mapping(uint8 accessId => AccessCriteriaPrivilages) + ) private accessCriteriaPrivilages; /// @notice The next available round ID. uint64 private nextRoundId; @@ -158,10 +162,10 @@ contract LM_PC_FundingPot_v1 is external view returns ( - bool isOpen, - address nftContract, - bytes32 merkleRoot, - address[] memory allowedAddresses + bool isRoundOpen_, + address nftContract_, + bytes32 merkleRoot_, + address[] memory allowedAddresses_ ) { Round storage round = rounds[roundId_]; @@ -177,7 +181,37 @@ contract LM_PC_FundingPot_v1 is } /// @inheritdoc ILM_PC_FundingPot_v1 - function getRoundCount() external view returns (uint64 roundCount_) { + function getRoundAccessCriteriaPrivilages(uint64 roundId_, uint8 accessId_) + external + view + returns ( + bool isRoundOpen_, + uint personalCap_, + bool overrideCap_, + uint start_, + uint cliff_, + uint end_ + ) + { + Round storage round = rounds[roundId_]; + AccessCriteria storage accessCriteria = round.accessCriterias[accessId_]; + + if (accessCriteria.accessCriteriaId == AccessCriteriaId.OPEN) { + return (true, 0, false, 0, 0, 0); + } + + return ( + false, + accessCriteriaPrivilages[roundId_][accessId_].personalCap, + accessCriteriaPrivilages[roundId_][accessId_].overrideCap, + accessCriteriaPrivilages[roundId_][accessId_].start, + accessCriteriaPrivilages[roundId_][accessId_].cliff, + accessCriteriaPrivilages[roundId_][accessId_].end + ); + } + + /// @inheritdoc ILM_PC_FundingPot_v1 + function getRoundCount() external view returns (uint64) { return nextRoundId; } @@ -319,6 +353,51 @@ contract LM_PC_FundingPot_v1 is emit AccessCriteriaEdited(roundId_, accessCriteriaId_, accessCriteria_); } + + /// @inheritdoc ILM_PC_FundingPot_v1 + function setAccessCriteriaPrivilages( + uint64 roundId_, + uint8 accessId_, + uint personalCap_, + bool overrideCap_, + uint _start, + uint _cliff, + uint _end + ) external onlyModuleRole(FUNDING_POT_ADMIN_ROLE) { + Round storage round = rounds[roundId_]; + + _validateEditRoundParameters(round); + + if ( + round.accessCriterias[accessId_].accessCriteriaId + == AccessCriteriaId.OPEN + ) { + revert + Module__LM_PC_FundingPot__CannotSetPrivilagesForOpenAccessCriteria(); + } + if (!_validTimes(_start, _cliff, _end)) { + revert Module__LM_PC_FundingPot__InvalidTimes(); + } + + AccessCriteriaPrivilages storage accessCriteriaPrivilages = + accessCriteriaPrivilages[roundId_][accessId_]; + + accessCriteriaPrivilages.personalCap = personalCap_; + accessCriteriaPrivilages.overrideCap = overrideCap_; + accessCriteriaPrivilages.start = _start; + accessCriteriaPrivilages.cliff = _cliff; + accessCriteriaPrivilages.end = _end; + + emit AccessCriteriaPrivilagesSet( + roundId_, + accessId_, + personalCap_, + overrideCap_, + _start, + _cliff, + _end + ); + } // ------------------------------------------------------------------------- // Internal @@ -369,4 +448,19 @@ contract LM_PC_FundingPot_v1 is revert Module__LM_PC_FundingPot__RoundAlreadyStarted(); } } + + /// @dev Validate uint start input. + /// @param _start uint to validate. + /// @param _cliff uint to validate. + /// @param _end uint to validate. + /// @return True if uint is valid. + function _validTimes(uint _start, uint _cliff, uint _end) + internal + pure + returns (bool) + { + // _start + _cliff should be less or equal to _end + // this already implies that _start is not greater than _end + return _start + _cliff <= _end; + } } diff --git a/src/modules/logicModule/interfaces/ILM_PC_FundingPot_v1.sol b/src/modules/logicModule/interfaces/ILM_PC_FundingPot_v1.sol index 64db3b02a..0733300d3 100644 --- a/src/modules/logicModule/interfaces/ILM_PC_FundingPot_v1.sol +++ b/src/modules/logicModule/interfaces/ILM_PC_FundingPot_v1.sol @@ -41,6 +41,14 @@ interface ILM_PC_FundingPot_v1 is IERC20PaymentClientBase_v2 { address[] allowedAddresses; // Explicit allowlist } + struct AccessCriteriaPrivilages { + uint personalCap; + bool overrideCap; + uint start; + uint cliff; + uint end; + } + // ------------------------------------------------------------------------- // Enums @@ -187,12 +195,33 @@ interface ILM_PC_FundingPot_v1 is IERC20PaymentClientBase_v2 { external view returns ( - bool isOpen_, + bool isRoundOpen_, address nftContract_, bytes32 merkleRoot_, address[] memory allowedAddresses_ ); + /// @notice Retrieves the access criteria privilages for a specific funding round. + /// @param roundId_ The unique identifier of the round. + /// @param accessId_ The identifier of the access criteria. + /// @return isRoundOpen_ Whether the round is open + /// @return personalCap_ The personal cap for the access criteria + /// @return overrideCap_ Whether to override the global cap + /// @return start_ The start timestamp for the access criteria + /// @return cliff_ The cliff timestamp for the access criteria + /// @return end_ The end timestamp for the access criteria + function getRoundAccessCriteriaPrivilages(uint64 roundId_, uint8 accessId_) + external + view + returns ( + bool isRoundOpen_, + uint personalCap_, + bool overrideCap_, + uint start_, + uint cliff_, + uint end_ + ); + /// @notice Retrieves the total number of funding rounds. /// @return roundCount_ The total number of funding rounds. function getRoundCount() external view returns (uint64 roundCount_); @@ -268,4 +297,23 @@ interface ILM_PC_FundingPot_v1 is IERC20PaymentClientBase_v2 { uint8 accessCriteriaId_, AccessCriteria memory accessCriteria_ ) external; + + /// @notice Set Access Criteria Privilages + /// @dev Only callable by funding pot admin and only before the round has started + /// @param roundId_ ID of the round + /// @param accessId_ ID of the access criteria + /// @param personalCap_ Personal cap for the access criteria + /// @param overrideCap_ Whether to override the global cap + /// @param _start Start timestamp for the access criteria + /// @param _cliff Cliff timestamp for the access criteria + /// @param _end End timestamp for the access criteria + function setAccessCriteriaPrivilages( + uint64 roundId_, + uint8 accessId_, + uint personalCap_, + bool overrideCap_, + uint _start, + uint _cliff, + uint _end + ) external; } diff --git a/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol b/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol index 70db051ca..7e949f210 100644 --- a/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol +++ b/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol @@ -394,7 +394,6 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { { vm.assume(user_ != address(0) && user_ != address(this)); testCreateRound(); - uint64 roundId = fundingPot.getRoundCount(); RoundParams memory params = RoundParams({ From f879c6173274728b6e242c30647087759d760785 Mon Sep 17 00:00:00 2001 From: JeffreyJoel Date: Thu, 27 Mar 2025 14:44:08 +0100 Subject: [PATCH 035/130] feat: update round contribution functionality --- .../logicModule/LM_PC_FundingPot_v1.sol | 195 ++++++++++++++++++ .../interfaces/ILM_PC_FundingPot_v1.sol | 49 +++++ 2 files changed, 244 insertions(+) diff --git a/src/modules/logicModule/LM_PC_FundingPot_v1.sol b/src/modules/logicModule/LM_PC_FundingPot_v1.sol index cdb2b311b..5d9e0b19b 100644 --- a/src/modules/logicModule/LM_PC_FundingPot_v1.sol +++ b/src/modules/logicModule/LM_PC_FundingPot_v1.sol @@ -17,10 +17,13 @@ import { // External import {IERC20} from "@oz/token/ERC20/IERC20.sol"; +import {IERC721} from "@oz/token/ERC721/IERC721.sol"; import {SafeERC20} from "@oz/token/ERC20/utils/SafeERC20.sol"; import {ERC165Upgradeable} from "@oz-up/utils/introspection/ERC165Upgradeable.sol"; +import "@oz/utils/cryptography/MerkleProof.sol"; + /** * @title Inverter Funding Pot Module * @@ -94,6 +97,12 @@ contract LM_PC_FundingPot_v1 is uint64 roundId => mapping(uint8 accessId => AccessCriteriaPrivilages) ) private accessCriteriaPrivilages; + /// @notice Maps round IDs to user addresses to contribution amounts + mapping(uint64 => mapping(address => uint)) private userContributions; + + /// @notice Maps round IDs to total contributions + mapping(uint64 => uint) private roundTotalContributions; + /// @notice The next available round ID. uint64 private nextRoundId; @@ -398,6 +407,73 @@ contract LM_PC_FundingPot_v1 is _end ); } + + function contribute( + uint64 roundId_, + uint amount_, + uint8 accessId_, + address contributionToken_, + bytes32[] calldata merkleProof_ + ) external { + // Validate input amount. + if (amount_ == 0) { + revert Module__LM_PC_FundingPot__InvalidDepositAmount(); + } + + Round storage round = rounds[roundId_]; + + // Validate round exists. + if (round.roundEnd == 0 && round.roundCap == 0) { + revert Module__LM_PC_FundingPot__RoundNotCreated(); + } + + // Validate round timing. + uint currentTime = block.timestamp; + if (currentTime < round.roundStart) { + revert Module__LM_PC_FundingPot__RoundHasNotStarted(); + } + if (round.roundEnd > 0 && currentTime > round.roundEnd) { + revert Module__LM_PC_FundingPot__RoundHasEnded(); + } + + // Validate access criteria. + _validateAccessCriteria(roundId_, accessId_, merkleProof_); + + // Retrieve user's previous contribution and calculate the personal cap. + address user = msg.sender; + uint userPreviousContribution = _getUserContribution(roundId_, user); + uint userPersonalCap = _getUserPersonalCap(roundId_, user); + + if (userPreviousContribution >= userPersonalCap) { + revert Module__LM_PC_FundingPot__PersonalCapReached(); + } + + // Adjust contribution if it would exceed the personal cap. + uint userRemainingCap = userPersonalCap - userPreviousContribution; + uint actualContributionAmount = amount_; + if (amount_ > userRemainingCap) { + actualContributionAmount = userRemainingCap; + } + + uint totalRoundContribution = _getTotalRoundContribution(roundId_); + if (round.roundCap > 0 && totalRoundContribution >= round.roundCap) { + revert Module__LM_PC_FundingPot__RoundCapReached(); + } + uint roundRemainingCap = round.roundCap - totalRoundContribution; + if (actualContributionAmount > roundRemainingCap) { + actualContributionAmount = roundRemainingCap; + } + + // Transfer funds. + IERC20(contributionToken_).safeTransferFrom( + user, address(this), actualContributionAmount + ); + + // Record the contribution. + _recordContribution(roundId_, user, actualContributionAmount); + emit ContributionMade(roundId_, user, actualContributionAmount); + } + // ------------------------------------------------------------------------- // Internal @@ -463,4 +539,123 @@ contract LM_PC_FundingPot_v1 is // this already implies that _start is not greater than _end return _start + _cliff <= _end; } + + function _validateAccessCriteria( + uint64 roundId_, + uint8 accessId_, + bytes32[] calldata merkleProof_ + ) internal view { + Round storage round = rounds[roundId_]; + AccessCriteria storage accessCriteria = round.accessCriterias[accessId_]; + + if (accessCriteria.accessCriteriaId == AccessCriteriaId.OPEN) { + return; + } + + bool accessGranted = false; + if (accessCriteria.accessCriteriaId == AccessCriteriaId.NFT) { + accessGranted = + _checkNftOwnership(accessCriteria.nftContract, msg.sender); + } else if (accessCriteria.accessCriteriaId == AccessCriteriaId.MERKLE) { + //TODO: Should I move this into a helper function + + bytes32 leaf = keccak256(abi.encodePacked(msg.sender, roundId_)); + accessGranted = MerkleProof.verify( + merkleProof_, accessCriteria.merkleRoot, leaf + ); + } else if (accessCriteria.accessCriteriaId == AccessCriteriaId.LIST) { + accessGranted = _checkAllowedAddressList( + accessCriteria.allowedAddresses, msg.sender + ); + } + + if (!accessGranted) { + revert Module__LM_PC_FundingPot__AccessNotPermitted(); + } + } + + function _getTotalRoundContribution(uint64 roundId_) + internal + view + returns (uint) + { + return roundTotalContributions[roundId_]; + } + + function _getUserContribution(uint64 roundId_, address user_) + internal + view + returns (uint) + { + return userContributions[roundId_][user_]; + } + + function _getUserPersonalCap(uint64 roundId_, address user_) + internal + view + returns (uint) + { + uint basePersonalCap = 1000 ether; + Round storage round = rounds[roundId_]; + + if (round.globalAccumulativeCaps) { + uint unusedCapacity = + _getUnusedCapacityFromPreviousRounds(user_, roundId_); + return basePersonalCap + unusedCapacity; + } + return basePersonalCap; + } + + function _getUnusedCapacityFromPreviousRounds( + address user_, + uint64 currentRoundId_ + ) internal view returns (uint) { + uint totalUnusedCapacity = 0; + for (uint64 i = 1; i < currentRoundId_; i++) { + Round storage prevRound = rounds[i]; + if (!prevRound.globalAccumulativeCaps) { + continue; + } + uint personalCap = 1000 ether; + uint userContribution = _getUserContribution(i, user_); + if (userContribution < personalCap) { + totalUnusedCapacity += (personalCap - userContribution); + } + } + return totalUnusedCapacity; + } + + function _recordContribution(uint64 roundId_, address user_, uint amount_) + internal + { + userContributions[roundId_][user_] += amount_; + roundTotalContributions[roundId_] += amount_; + } + + function _checkAllowedAddressList( + address[] memory allowedAddresses, + address sender + ) internal pure returns (bool) { + for (uint i = 0; i < allowedAddresses.length; i++) { + if (allowedAddresses[i] == sender) { + return true; + } + } + return false; + } + + function _checkNftOwnership(address nftContract_, address user_) + internal + view + returns (bool) + { + if (nftContract_ == address(0) || user_ == address(0)) { + return false; + } + try IERC721(nftContract_).balanceOf(user_) returns (uint balance) { + return balance > 0; + } catch { + return false; + } + } } diff --git a/src/modules/logicModule/interfaces/ILM_PC_FundingPot_v1.sol b/src/modules/logicModule/interfaces/ILM_PC_FundingPot_v1.sol index 0733300d3..33fd9c958 100644 --- a/src/modules/logicModule/interfaces/ILM_PC_FundingPot_v1.sol +++ b/src/modules/logicModule/interfaces/ILM_PC_FundingPot_v1.sol @@ -123,6 +123,12 @@ interface ILM_PC_FundingPot_v1 is IERC20PaymentClientBase_v2 { uint64 indexed roundId_, uint8 accessId_, AccessCriteria accessCriteria_ ); + /// @notice Emitted when a contribution is made to a round + /// @param roundId_ The ID of the round + /// @param contributor_ The address of the contributor + /// @param amount_ The amount contributed + event ContributionMade(uint64 roundId_, address contributor_, uint amount_); + // ------------------------------------------------------------------------- // Errors @@ -159,6 +165,33 @@ interface ILM_PC_FundingPot_v1 is IERC20PaymentClientBase_v2 { /// @notice Invalid access criteria ID. error Module__LM_PC_FundingPot__InvalidAccessCriteriaId(); + /// @notice Round has not started yet + error Module__LM_PC_FundingPot__RoundHasNotStarted(); + + /// @notice Round has already ended + error Module__LM_PC_FundingPot__RoundHasEnded(); + + /// @notice User does not meet the NFT access criteria + error Module__LM_PC_FundingPot__AccessCriteriaNftFailed(); + + /// @notice User does not meet the merkle proof access criteria + error Module__LM_PC_FundingPot__AccessCriteriaMerkleFailed(); + + /// @notice User is not on the allowlist + error Module__LM_PC_FundingPot__AccessCriteriaListFailed(); + + /// @notice Invalid access criteria type + error Module__LM_PC_FundingPot__InvalidAccessCriteriaType(); + + /// @notice Access not permitted + error Module__LM_PC_FundingPot__AccessNotPermitted(); + + /// @notice User has reached their personal contribution cap + error Module__LM_PC_FundingPot__PersonalCapReached(); + + /// @notice Round contribution cap has been reached + error Module__LM_PC_FundingPot__RoundCapReached(); + // ------------------------------------------------------------------------- // Public - Getters @@ -316,4 +349,20 @@ interface ILM_PC_FundingPot_v1 is IERC20PaymentClientBase_v2 { uint _cliff, uint _end ) external; + + /// @notice Allows a user to contribute to a specific funding round. + /// @dev Verifies the contribution eligibility based on the provided Merkle proof. + /// @param roundId_ The unique identifier of the funding round. + /// @param amount_ The amount of tokens being contributed. + /// @param accessCriteriaId_ The identifier for the access criteria to validate eligibility. + /// @param contributionToken_ The address of the token used for contribution. + /// @param merkleProof_ The Merkle proof used to verify the contributor's eligibility. + + function contribute( + uint64 roundId_, + uint amount_, + uint8 accessCriteriaId_, + address contributionToken_, + bytes32[] calldata merkleProof_ + ) external; } From e7a62d604dc35a57bce512bbf643b3ae9364b405 Mon Sep 17 00:00:00 2001 From: JeffreyJoel Date: Fri, 28 Mar 2025 22:19:20 +0100 Subject: [PATCH 036/130] test: add unhappy paths for open access criteria --- .../logicModule/LM_PC_FundingPot_v1.sol | 91 +++++++++++++------ .../interfaces/ILM_PC_FundingPot_v1.sol | 3 +- .../LM_PC_FundingPot_v2NFTMock.sol | 44 +++++++++ .../logicModule/LM_PC_FundingPot_v1.t.sol | 56 ++++++++---- 4 files changed, 148 insertions(+), 46 deletions(-) create mode 100644 test/mocks/modules/logicModule/LM_PC_FundingPot_v2NFTMock.sol diff --git a/src/modules/logicModule/LM_PC_FundingPot_v1.sol b/src/modules/logicModule/LM_PC_FundingPot_v1.sol index 5d9e0b19b..74d829c4f 100644 --- a/src/modules/logicModule/LM_PC_FundingPot_v1.sol +++ b/src/modules/logicModule/LM_PC_FundingPot_v1.sol @@ -209,13 +209,17 @@ contract LM_PC_FundingPot_v1 is return (true, 0, false, 0, 0, 0); } + // Store the privileges in a local variable to reduce stack usage. + AccessCriteriaPrivilages storage privs = + accessCriteriaPrivilages[roundId_][accessId_]; + return ( false, - accessCriteriaPrivilages[roundId_][accessId_].personalCap, - accessCriteriaPrivilages[roundId_][accessId_].overrideCap, - accessCriteriaPrivilages[roundId_][accessId_].start, - accessCriteriaPrivilages[roundId_][accessId_].cliff, - accessCriteriaPrivilages[roundId_][accessId_].end + privs.personalCap, + privs.overrideCap, + privs.start, + privs.cliff, + privs.end ); } @@ -408,7 +412,8 @@ contract LM_PC_FundingPot_v1 is ); } - function contribute( + /// @inheritdoc ILM_PC_FundingPot_v1 + function contributeToRound( uint64 roundId_, uint amount_, uint8 accessId_, @@ -440,38 +445,32 @@ contract LM_PC_FundingPot_v1 is _validateAccessCriteria(roundId_, accessId_, merkleProof_); // Retrieve user's previous contribution and calculate the personal cap. - address user = msg.sender; - uint userPreviousContribution = _getUserContribution(roundId_, user); - uint userPersonalCap = _getUserPersonalCap(roundId_, user); - - if (userPreviousContribution >= userPersonalCap) { - revert Module__LM_PC_FundingPot__PersonalCapReached(); - } + uint userPreviousContribution = + _getUserContribution(roundId_, msg.sender); + uint userPersonalCap = _getUserPersonalCap(roundId_, msg.sender); - // Adjust contribution if it would exceed the personal cap. + // Revert contribution if it would exceed the personal cap. uint userRemainingCap = userPersonalCap - userPreviousContribution; - uint actualContributionAmount = amount_; if (amount_ > userRemainingCap) { - actualContributionAmount = userRemainingCap; + revert Module__LM_PC_FundingPot__PersonalCapReached(); } uint totalRoundContribution = _getTotalRoundContribution(roundId_); - if (round.roundCap > 0 && totalRoundContribution >= round.roundCap) { + if ( + round.roundCap > 0 + && totalRoundContribution + amount_ >= round.roundCap + ) { revert Module__LM_PC_FundingPot__RoundCapReached(); } - uint roundRemainingCap = round.roundCap - totalRoundContribution; - if (actualContributionAmount > roundRemainingCap) { - actualContributionAmount = roundRemainingCap; - } // Transfer funds. IERC20(contributionToken_).safeTransferFrom( - user, address(this), actualContributionAmount + msg.sender, address(this), amount_ ); // Record the contribution. - _recordContribution(roundId_, user, actualContributionAmount); - emit ContributionMade(roundId_, user, actualContributionAmount); + _recordContribution(roundId_, msg.sender, amount_); + emit ContributionMade(roundId_, msg.sender, amount_); } // ------------------------------------------------------------------------- @@ -540,6 +539,11 @@ contract LM_PC_FundingPot_v1 is return _start + _cliff <= _end; } + /// @notice Validates access criteria for a specific round and access type + /// @dev Checks if a user meets the access requirements based on the round's access criteria + /// @param roundId_ The ID of the round being validated + /// @param accessId_ The ID of the specific access criteria + /// @param merkleProof_ Merkle proof for Merkle tree-based access (optional) function _validateAccessCriteria( uint64 roundId_, uint8 accessId_, @@ -558,8 +562,7 @@ contract LM_PC_FundingPot_v1 is _checkNftOwnership(accessCriteria.nftContract, msg.sender); } else if (accessCriteria.accessCriteriaId == AccessCriteriaId.MERKLE) { //TODO: Should I move this into a helper function - - bytes32 leaf = keccak256(abi.encodePacked(msg.sender, roundId_)); + bytes32 leaf = keccak256(abi.encodePacked(msg.sender)); accessGranted = MerkleProof.verify( merkleProof_, accessCriteria.merkleRoot, leaf ); @@ -574,6 +577,10 @@ contract LM_PC_FundingPot_v1 is } } + /// @notice Retrieves the total contribution for a specific round + /// @dev Returns the accumulated contributions for the given round + /// @param roundId_ The ID of the round to check contributions for + /// @return The total contributions for the specified round function _getTotalRoundContribution(uint64 roundId_) internal view @@ -582,6 +589,11 @@ contract LM_PC_FundingPot_v1 is return roundTotalContributions[roundId_]; } + /// @notice Retrieves the contribution amount for a specific user in a round + /// @dev Returns the individual user's contribution for the given round + /// @param roundId_ The ID of the round to check contributions for + /// @param user_ The address of the user + /// @return The user's contribution amount for the specified round function _getUserContribution(uint64 roundId_, address user_) internal view @@ -590,12 +602,17 @@ contract LM_PC_FundingPot_v1 is return userContributions[roundId_][user_]; } + /// @notice Calculates the personal contribution cap for a user in a specific round + /// @dev Determines the maximum amount a user can contribute based on global or round-specific rules + /// @param roundId_ The ID of the current round + /// @param user_ The address of the user + /// @return The personal contribution cap for the user function _getUserPersonalCap(uint64 roundId_, address user_) internal view returns (uint) { - uint basePersonalCap = 1000 ether; + uint basePersonalCap = 500; Round storage round = rounds[roundId_]; if (round.globalAccumulativeCaps) { @@ -606,6 +623,11 @@ contract LM_PC_FundingPot_v1 is return basePersonalCap; } + /// @notice Calculates unused contribution capacity from previous rounds + /// @dev Aggregates unused contribution caps from previous rounds with global accumulative caps + /// @param user_ The address of the user + /// @param currentRoundId_ The ID of the current round + /// @return Total unused contribution capacity from previous rounds function _getUnusedCapacityFromPreviousRounds( address user_, uint64 currentRoundId_ @@ -625,6 +647,11 @@ contract LM_PC_FundingPot_v1 is return totalUnusedCapacity; } + /// @notice Records a contribution for a user in a specific round + /// @dev Updates the user's contribution and the total round contribution + /// @param roundId_ The ID of the round + /// @param user_ The address of the user making the contribution + /// @param amount_ The amount of the contribution function _recordContribution(uint64 roundId_, address user_, uint amount_) internal { @@ -632,6 +659,11 @@ contract LM_PC_FundingPot_v1 is roundTotalContributions[roundId_] += amount_; } + ///@notice Checks if a sender is in a list of allowed addresses + /// @dev Performs a linear search to validate address inclusion + /// @param allowedAddresses Array of addresses permitted to participate + /// @param sender Address to check for permission + /// @return Boolean indicating whether the sender is in the allowed list function _checkAllowedAddressList( address[] memory allowedAddresses, address sender @@ -644,6 +676,11 @@ contract LM_PC_FundingPot_v1 is return false; } + /// @notice Verifies NFT ownership for access control + /// @dev Safely checks the NFT balance of a user using a try-catch block + /// @param nftContract_ Address of the NFT contract + /// @param user_ Address of the user to check for NFT ownership + /// @return Boolean indicating whether the user owns an NFT function _checkNftOwnership(address nftContract_, address user_) internal view diff --git a/src/modules/logicModule/interfaces/ILM_PC_FundingPot_v1.sol b/src/modules/logicModule/interfaces/ILM_PC_FundingPot_v1.sol index 33fd9c958..f7742ac0c 100644 --- a/src/modules/logicModule/interfaces/ILM_PC_FundingPot_v1.sol +++ b/src/modules/logicModule/interfaces/ILM_PC_FundingPot_v1.sol @@ -357,8 +357,7 @@ interface ILM_PC_FundingPot_v1 is IERC20PaymentClientBase_v2 { /// @param accessCriteriaId_ The identifier for the access criteria to validate eligibility. /// @param contributionToken_ The address of the token used for contribution. /// @param merkleProof_ The Merkle proof used to verify the contributor's eligibility. - - function contribute( + function contributeToRound( uint64 roundId_, uint amount_, uint8 accessCriteriaId_, diff --git a/test/mocks/modules/logicModule/LM_PC_FundingPot_v2NFTMock.sol b/test/mocks/modules/logicModule/LM_PC_FundingPot_v2NFTMock.sol new file mode 100644 index 000000000..4966d75e4 --- /dev/null +++ b/test/mocks/modules/logicModule/LM_PC_FundingPot_v2NFTMock.sol @@ -0,0 +1,44 @@ +// // SPDX-License-Identifier: LGPL-3.0-only +// pragma solidity 0.8.23; + +// // External Dependencies +// import "@oz/token/ERC721/ERC721.sol"; +// import "@oz/token/ERC721/extensions/ERC721URIStorage.sol"; +// import "@oz/access/Ownable.sol"; +// import "@oz/utils/Counters.sol"; + +// contract MockNFT is ERC721URIStorage, Ownable { +// using Counters for Counters.Counter; + +// Counters.Counter private _tokenIds; + +// string public baseURI; + +// constructor(string memory name, string memory symbol) +// ERC721(name, symbol) +// Ownable(msg.sender) +// {} + +// function _baseURI() internal view override returns (string memory) { +// return baseURI; +// } + +// function setBaseURI(string memory newBaseURI) public onlyOwner { +// baseURI = newBaseURI; +// } + +// function mint(address to) public onlyOwner returns (uint) { +// uint newTokenId = _tokenIds.current(); +// _safeMint(to, newTokenId); +// _tokenIds.increment(); + +// return newTokenId; +// } + +// function setTokenURI(uint tokenId, string memory tokenURI) +// public +// onlyOwner +// { +// _setTokenURI(tokenId, tokenURI); +// } +// } diff --git a/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol b/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol index 7e949f210..c4572916c 100644 --- a/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol +++ b/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol @@ -44,10 +44,22 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { // ------------------------------------------------------------------------- // Constants + bytes32 internal constant FUNDING_POT_ADMIN_ROLE = "FUNDING_POT_ADMIN"; + address contributor_; + // ------------------------------------------------------------------------- // State - + struct RoundParameters { + uint roundStart; + uint roundEnd; + uint roundCap; + address hookContract; + bytes hookFunction; + bool closureMechanism; + bool globalAccumulativeCaps; + } // SuT + LM_PC_FundingPot_v1_Exposed fundingPot; // Storage variables to avoid stack too deep @@ -75,6 +87,10 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { address impl = address(new LM_PC_FundingPot_v1_Exposed()); fundingPot = LM_PC_FundingPot_v1_Exposed(Clones.clone(impl)); + contributor_ = address(0xBeef); + + fundingPotToken.mint(contributor_, 10_000); + // Setup the module to test _setUpOrchestrator(fundingPot); @@ -308,7 +324,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { ); } - /* Test createRound() + /* Test Fuzz createRound() ├── Given all the valid parameters are provided │ └── When user attempts to create a round │ └── Then it should not be active and should return the round id @@ -392,8 +408,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { function testEditRound_revertsGivenUserIsNotFundingPotAdmin(address user_) public { - vm.assume(user_ != address(0) && user_ != address(this)); - testCreateRound(); + testCreateRound(1000); uint64 roundId = fundingPot.getRoundCount(); RoundParams memory params = RoundParams({ @@ -429,7 +444,8 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { } function testEditRound_revertsGivenRoundIsNotCreated() public { - testCreateRound(); + testCreateRound(1000); + uint64 roundId = fundingPot.getRoundCount() + 1; uint64 roundId = fundingPot.getRoundCount(); @@ -462,8 +478,8 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { ); } - function testEditRound_revertsGivenRoundIsActive(uint roundStart_) public { - testCreateRound(); + function testEditRound_revertsGivenRoundIsActive() public { + testCreateRound(1000); uint64 roundId = fundingPot.getRoundCount(); RoundParams memory params; @@ -510,9 +526,10 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { function testEditRound_revertsGivenRoundStartIsInThePast(uint roundStartP_) public { - testCreateRound(); + testCreateRound(1000); uint64 roundId = fundingPot.getRoundCount(); + RoundParameters memory editedParams = _helper_createEditedRoundParams(); vm.assume(roundStartP_ < block.timestamp); RoundParams memory params = RoundParams({ roundStart: roundStartP_, @@ -545,7 +562,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { } function testEditRound_revertsGivenRoundEndTimeAndCapAreBothZero() public { - testCreateRound(); + testCreateRound(1000); uint64 roundId = fundingPot.getRoundCount(); RoundParams memory params = RoundParams({ @@ -579,9 +596,14 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { } function testEditRound_revertsGivenRoundEndTimeIsBeforeRoundStart( - uint roundEnd_ + uint roundEnd_, + uint roundStart_ ) public { - testCreateRound(); + vm.assume( + roundEnd_ != 0 && roundStart_ > block.timestamp + && roundEnd_ < roundStart_ + ); + testCreateRound(1000); uint64 roundId = fundingPot.getRoundCount(); // Get the current round start time @@ -625,7 +647,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { function testEditRound_revertsGivenHookContractIsSetButHookFunctionIsEmpty() public { - testCreateRound(); + testCreateRound(1000); uint64 roundId = fundingPot.getRoundCount(); RoundParams memory params = _helper_createEditRoundParams( @@ -661,7 +683,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { function testEditRound_revertsGivenHookFunctionIsSetButHookContractIsEmpty() public { - testCreateRound(); + testCreateRound(1000); uint64 roundId = fundingPot.getRoundCount(); RoundParams memory params = _helper_createEditRoundParams( @@ -709,7 +731,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { */ function testEditRound() public { - testCreateRound(); + testCreateRound(1000); uint64 lastRoundId = fundingPot.getRoundCount(); RoundParams memory params = _editedRoundParams; @@ -784,7 +806,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { vm.assume(accessCriteriaEnum_ >= 0 && accessCriteriaEnum_ <= 4); vm.assume(user_ != address(0) && user_ != address(this)); - testCreateRound(); + testCreateRound(1000); uint64 roundId = fundingPot.getRoundCount(); ILM_PC_FundingPot_v1.AccessCriteria memory accessCriteria = @@ -1050,7 +1072,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { vm.expectRevert( abi.encodeWithSelector( ILM_PC_FundingPot_v1 - .Module__LM_PC_FundingPot__RoundAlreadyStarted + .Module__LM_PC_FundingPot__PersonalCapReached .selector ) ); @@ -1102,7 +1124,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { // Helper Functions // @notice Creates a default funding round - function _helper_createDefaultFundingRound() + function _helper_createDefaultFundingRound(uint roundCap_) internal returns (RoundParams memory) { From 9bb1b6889fdb82f0399d9524f2e172b88bca731d Mon Sep 17 00:00:00 2001 From: JeffreyJoel Date: Sat, 29 Mar 2025 00:35:52 +0100 Subject: [PATCH 037/130] test: add unhappy paths for NFT, merkle root and allowed list access criteria --- .../logicModule/LM_PC_FundingPot_v1.sol | 31 +++-- .../LM_PC_FundingPot_v2NFTMock.sol | 84 ++++++------ .../logicModule/LM_PC_FundingPot_v1.t.sol | 129 +++++++++++++++++- 3 files changed, 187 insertions(+), 57 deletions(-) diff --git a/src/modules/logicModule/LM_PC_FundingPot_v1.sol b/src/modules/logicModule/LM_PC_FundingPot_v1.sol index 74d829c4f..711082e73 100644 --- a/src/modules/logicModule/LM_PC_FundingPot_v1.sol +++ b/src/modules/logicModule/LM_PC_FundingPot_v1.sol @@ -561,20 +561,14 @@ contract LM_PC_FundingPot_v1 is accessGranted = _checkNftOwnership(accessCriteria.nftContract, msg.sender); } else if (accessCriteria.accessCriteriaId == AccessCriteriaId.MERKLE) { - //TODO: Should I move this into a helper function - bytes32 leaf = keccak256(abi.encodePacked(msg.sender)); - accessGranted = MerkleProof.verify( - merkleProof_, accessCriteria.merkleRoot, leaf + accessGranted = _validateMerkleProof( + accessCriteria.merkleRoot, msg.sender, merkleProof_ ); } else if (accessCriteria.accessCriteriaId == AccessCriteriaId.LIST) { accessGranted = _checkAllowedAddressList( accessCriteria.allowedAddresses, msg.sender ); } - - if (!accessGranted) { - revert Module__LM_PC_FundingPot__AccessNotPermitted(); - } } /// @notice Retrieves the total contribution for a specific round @@ -673,6 +667,7 @@ contract LM_PC_FundingPot_v1 is return true; } } + revert Module__LM_PC_FundingPot__AccessCriteriaListFailed(); return false; } @@ -689,10 +684,26 @@ contract LM_PC_FundingPot_v1 is if (nftContract_ == address(0) || user_ == address(0)) { return false; } + try IERC721(nftContract_).balanceOf(user_) returns (uint balance) { - return balance > 0; + if (balance == 0) { + revert Module__LM_PC_FundingPot__AccessCriteriaNftFailed(); + } + return true; } catch { - return false; + revert Module__LM_PC_FundingPot__AccessCriteriaNftFailed(); + } + } + + function _validateMerkleProof( + bytes32 root_, + address user_, + bytes32[] calldata merkleProof_ + ) internal pure returns (bool) { + bytes32 leaf = keccak256(abi.encodePacked(user_)); + + if (!MerkleProof.verify(merkleProof_, root_, leaf)) { + revert Module__LM_PC_FundingPot__AccessCriteriaMerkleFailed(); } } } diff --git a/test/mocks/modules/logicModule/LM_PC_FundingPot_v2NFTMock.sol b/test/mocks/modules/logicModule/LM_PC_FundingPot_v2NFTMock.sol index 4966d75e4..c7513d5cf 100644 --- a/test/mocks/modules/logicModule/LM_PC_FundingPot_v2NFTMock.sol +++ b/test/mocks/modules/logicModule/LM_PC_FundingPot_v2NFTMock.sol @@ -1,44 +1,40 @@ -// // SPDX-License-Identifier: LGPL-3.0-only -// pragma solidity 0.8.23; - -// // External Dependencies -// import "@oz/token/ERC721/ERC721.sol"; -// import "@oz/token/ERC721/extensions/ERC721URIStorage.sol"; -// import "@oz/access/Ownable.sol"; -// import "@oz/utils/Counters.sol"; - -// contract MockNFT is ERC721URIStorage, Ownable { -// using Counters for Counters.Counter; - -// Counters.Counter private _tokenIds; - -// string public baseURI; - -// constructor(string memory name, string memory symbol) -// ERC721(name, symbol) -// Ownable(msg.sender) -// {} - -// function _baseURI() internal view override returns (string memory) { -// return baseURI; -// } - -// function setBaseURI(string memory newBaseURI) public onlyOwner { -// baseURI = newBaseURI; -// } - -// function mint(address to) public onlyOwner returns (uint) { -// uint newTokenId = _tokenIds.current(); -// _safeMint(to, newTokenId); -// _tokenIds.increment(); - -// return newTokenId; -// } - -// function setTokenURI(uint tokenId, string memory tokenURI) -// public -// onlyOwner -// { -// _setTokenURI(tokenId, tokenURI); -// } -// } +// SPDX-License-Identifier: LGPL-3.0-only +pragma solidity 0.8.23; + +// External Dependencies +import "@oz/token/ERC721/ERC721.sol"; +import "@oz/token/ERC721/extensions/ERC721URIStorage.sol"; +import "@oz/access/Ownable.sol"; + +contract ERC721Mock is ERC721URIStorage, Ownable { + uint private _nextTokenId; + string public baseURI; + + constructor(string memory name, string memory symbol) + ERC721(name, symbol) + Ownable(msg.sender) + {} + + function _baseURI() internal view override returns (string memory) { + return baseURI; + } + + function setBaseURI(string memory newBaseURI) public onlyOwner { + baseURI = newBaseURI; + } + + function mint(address to) public onlyOwner returns (uint) { + uint tokenId = _nextTokenId; + _safeMint(to, tokenId); + _nextTokenId++; + + return tokenId; + } + + function setTokenURI(uint tokenId, string memory tokenURI) + public + onlyOwner + { + _setTokenURI(tokenId, tokenURI); + } +} diff --git a/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol b/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol index c4572916c..5c1ca8c47 100644 --- a/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol +++ b/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol @@ -18,6 +18,8 @@ import { ERC20PaymentClientBaseV2Mock, ERC20Mock } from "test/utils/mocks/modules/paymentClient/ERC20PaymentClientBaseV2Mock.sol"; +import {ERC721Mock} from + "test/utils/mocks/modules/logicModules/LM_PC_FundingPot_v2NFTMock.sol"; // System under Test (SuT) import {LM_PC_FundingPot_v1_Exposed} from @@ -47,6 +49,14 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { bytes32 internal constant FUNDING_POT_ADMIN_ROLE = "FUNDING_POT_ADMIN"; address contributor_; + bytes32 PROOF_ONE = + 0x0fd7c981d39bece61f7499702bf59b3114a90e66b51ba2c53abdf7b62986c00a; + bytes32 PROOF_TWO = + 0xe5ebd1e1b5a5478a944ecab36a9a954ac3b6b8216875f6524caa7a1d87096576; + bytes32[] PROOF = [PROOF_ONE, PROOF_TWO]; + bytes32 ROOT = + 0xaa5d581231e596618465a56aa0f5870ba6e20785fe436d5bfb82b08662ccc7c4; + // ------------------------------------------------------------------------- // State struct RoundParameters { @@ -80,6 +90,8 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { bool globalAccumulativeCaps; } + ERC721Mock mockNFTContract = new ERC721Mock("NFT Mock", "NFT"); + // ------------------------------------------------------------------------- // Setup function setUp() public { @@ -87,8 +99,8 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { address impl = address(new LM_PC_FundingPot_v1_Exposed()); fundingPot = LM_PC_FundingPot_v1_Exposed(Clones.clone(impl)); + // Mint tokens to the contributor contributor_ = address(0xBeef); - fundingPotToken.mint(contributor_, 10_000); // Setup the module to test @@ -1117,6 +1129,117 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { assertEq(allowedAddresses, newAccessCriteria.allowedAddresses); } + function testFuzzContributeToRound_revertsWhenNFTAccessCriteriaIsNotMet() + public + { + testCreateRound(1000); + + uint64 roundId = fundingPot.getRoundCount(); + uint8 accessId = 1; + uint amount = 250; + + ILM_PC_FundingPot_v1.AccessCriteria memory accessCriteria = + _helper_createAccessCriteria(1); + + fundingPot.setAccessCriteriaForRound(roundId, accessId, accessCriteria); + + (uint roundStart,,,,,,) = fundingPot.getRoundGenericParameters(roundId); + vm.warp(roundStart + 1); + + // Approve + vm.prank(contributor_); + fundingPotToken.approve(address(fundingPot), amount); + + vm.expectRevert( + abi.encodeWithSelector( + ILM_PC_FundingPot_v1 + .Module__LM_PC_FundingPot__AccessCriteriaNftFailed + .selector + ) + ); + + vm.prank(contributor_); + fundingPot.contributeToRound( + roundId, + amount, + accessId, + address(fundingPotToken), + new bytes32[](0) + ); + } + + function testFuzzContributeToRound_revertsWhenMerkleRootAccessCriteriaIsNotMet( + ) public { + testCreateRound(1000); + + uint64 roundId = fundingPot.getRoundCount(); + uint8 accessId = 1; + uint amount = 250; + + ILM_PC_FundingPot_v1.AccessCriteria memory accessCriteria = + _helper_createAccessCriteria(2); + + fundingPot.setAccessCriteriaForRound(roundId, accessId, accessCriteria); + + (uint roundStart,,,,,,) = fundingPot.getRoundGenericParameters(roundId); + vm.warp(roundStart + 1); + + // Approve + vm.prank(contributor_); + fundingPotToken.approve(address(fundingPot), amount); + + vm.expectRevert( + abi.encodeWithSelector( + ILM_PC_FundingPot_v1 + .Module__LM_PC_FundingPot__AccessCriteriaMerkleFailed + .selector + ) + ); + + vm.prank(contributor_); + fundingPot.contributeToRound( + roundId, amount, accessId, address(fundingPotToken), PROOF + ); + } + + function testFuzzContributeToRound_revertsWhenAllowedListAccessCriteriaIsNotMet( + ) public { + testCreateRound(1000); + + uint64 roundId = fundingPot.getRoundCount(); + uint8 accessId = 1; + uint amount = 250; + + ILM_PC_FundingPot_v1.AccessCriteria memory accessCriteria = + _helper_createAccessCriteria(3); + + fundingPot.setAccessCriteriaForRound(roundId, accessId, accessCriteria); + + (uint roundStart,,,,,,) = fundingPot.getRoundGenericParameters(roundId); + vm.warp(roundStart + 1); + + // Approve + vm.prank(contributor_); + fundingPotToken.approve(address(fundingPot), amount); + + vm.expectRevert( + abi.encodeWithSelector( + ILM_PC_FundingPot_v1 + .Module__LM_PC_FundingPot__AccessCriteriaListFailed + .selector + ) + ); + + vm.prank(contributor_); + fundingPot.contributeToRound( + roundId, + amount, + accessId, + address(fundingPotToken), + new bytes32[](0) + ); + } + // ------------------------------------------------------------------------- // Test: Internal Functions @@ -1171,7 +1294,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { accessCriteriaEnum == uint8(ILM_PC_FundingPot_v1.AccessCriteriaType.NFT) ) { - address nftContract = address(0x1); + address nftContract = address(mockNFTContract); return ILM_PC_FundingPot_v1.AccessCriteria( ILM_PC_FundingPot_v1.AccessCriteriaType.NFT, @@ -1183,7 +1306,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { accessCriteriaEnum == uint8(ILM_PC_FundingPot_v1.AccessCriteriaType.MERKLE) ) { - bytes32 merkleRoot = bytes32(uint(0x1)); + bytes32 merkleRoot = ROOT; return ILM_PC_FundingPot_v1.AccessCriteria( ILM_PC_FundingPot_v1.AccessCriteriaType.MERKLE, From cdc89eb963256b245d74423bf8196c37f9215f37 Mon Sep 17 00:00:00 2001 From: JeffreyJoel Date: Sat, 29 Mar 2025 01:00:59 +0100 Subject: [PATCH 038/130] test: add all unhappy paths --- .../logicModule/LM_PC_FundingPot_v1.t.sol | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol b/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol index 5c1ca8c47..0dfe0cf54 100644 --- a/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol +++ b/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol @@ -1240,6 +1240,52 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { ); } + function testFuzzContributeToRound_revertsWhenContributionExceedsPersonalCap( + ) public { + testCreateRound(1000); + + uint64 roundId = fundingPot.getRoundCount(); + uint8 accessId = 1; + uint amount = 250; + + ILM_PC_FundingPot_v1.AccessCriteria memory accessCriteria = + _helper_createAccessCriteria(1); + + fundingPot.setAccessCriteriaForRound(roundId, accessId, accessCriteria); + + mockNFTContract.mint(contributor_); + + (uint roundStart,,,,,,) = fundingPot.getRoundGenericParameters(roundId); + vm.warp(roundStart + 1); + + // Approve + vm.prank(contributor_); + fundingPotToken.approve(address(fundingPot), 500); + + vm.prank(contributor_); + fundingPot.contributeToRound( + roundId, + amount, + accessId, + address(fundingPotToken), + new bytes32[](0) + ); + + // Attempt to contribute beyond personal cap + vm.expectRevert( + abi.encodeWithSelector( + ILM_PC_FundingPot_v1 + .Module__LM_PC_FundingPot__PersonalCapReached + .selector + ) + ); + vm.prank(contributor_); + + fundingPot.contributeToRound( + roundId, 251, 0, address(fundingPotToken), new bytes32[](0) + ); + } + // ------------------------------------------------------------------------- // Test: Internal Functions From 1148b7a53d3fd6652c433359fb47944fb996adcb Mon Sep 17 00:00:00 2001 From: JeffreyJoel Date: Mon, 31 Mar 2025 17:21:22 +0100 Subject: [PATCH 039/130] test: add happy paths based on the technical specifications --- .../logicModule/LM_PC_FundingPot_v1.sol | 321 +++++++--- .../logicModule/LM_PC_FundingPot_v1.t.sol | 592 ++++++++++++++++-- .../LM_PC_FundingPot_v1_Exposed.sol | 41 +- 3 files changed, 826 insertions(+), 128 deletions(-) diff --git a/src/modules/logicModule/LM_PC_FundingPot_v1.sol b/src/modules/logicModule/LM_PC_FundingPot_v1.sol index 711082e73..b2d2eca6f 100644 --- a/src/modules/logicModule/LM_PC_FundingPot_v1.sol +++ b/src/modules/logicModule/LM_PC_FundingPot_v1.sol @@ -84,6 +84,9 @@ contract LM_PC_FundingPot_v1 is /// @notice The payment processor flag for the end timestamp. uint8 internal constant FLAG_END = 3; + /// @notice Maximum amount for the base personal cap + uint internal constant BASE_PERSONAL_CAP = 500; + // ------------------------------------------------------------------------- // State @@ -425,52 +428,20 @@ contract LM_PC_FundingPot_v1 is revert Module__LM_PC_FundingPot__InvalidDepositAmount(); } - Round storage round = rounds[roundId_]; - - // Validate round exists. - if (round.roundEnd == 0 && round.roundCap == 0) { - revert Module__LM_PC_FundingPot__RoundNotCreated(); - } - - // Validate round timing. - uint currentTime = block.timestamp; - if (currentTime < round.roundStart) { - revert Module__LM_PC_FundingPot__RoundHasNotStarted(); - } - if (round.roundEnd > 0 && currentTime > round.roundEnd) { - revert Module__LM_PC_FundingPot__RoundHasEnded(); - } + // Validate round and access criteria + Round storage round = + _validateRoundAndAccessCriteria(roundId_, accessId_, merkleProof_); - // Validate access criteria. - _validateAccessCriteria(roundId_, accessId_, merkleProof_); + // Check timing and caps based on privileges + (uint adjustedAmount, bool canOverrideTimeAndCap) = + _validateTimingAndCaps(roundId_, accessId_, amount_, round); - // Retrieve user's previous contribution and calculate the personal cap. - uint userPreviousContribution = - _getUserContribution(roundId_, msg.sender); - uint userPersonalCap = _getUserPersonalCap(roundId_, msg.sender); - - // Revert contribution if it would exceed the personal cap. - uint userRemainingCap = userPersonalCap - userPreviousContribution; - if (amount_ > userRemainingCap) { - revert Module__LM_PC_FundingPot__PersonalCapReached(); - } - - uint totalRoundContribution = _getTotalRoundContribution(roundId_); - if ( - round.roundCap > 0 - && totalRoundContribution + amount_ >= round.roundCap - ) { - revert Module__LM_PC_FundingPot__RoundCapReached(); - } - - // Transfer funds. IERC20(contributionToken_).safeTransferFrom( - msg.sender, address(this), amount_ + msg.sender, address(this), adjustedAmount ); - // Record the contribution. - _recordContribution(roundId_, msg.sender, amount_); - emit ContributionMade(roundId_, msg.sender, amount_); + _recordContribution(roundId_, msg.sender, adjustedAmount); + emit ContributionMade(roundId_, msg.sender, adjustedAmount); } // ------------------------------------------------------------------------- @@ -539,6 +510,184 @@ contract LM_PC_FundingPot_v1 is return _start + _cliff <= _end; } + /// @notice Validates the round existence and access criteria + /// @param roundId_ ID of the round to validate + /// @param accessId_ ID of the access criteria to check + /// @param merkleProof_ Merkle proof for validation if needed + /// @return round The round storage object + function _validateRoundAndAccessCriteria( + uint64 roundId_, + uint8 accessId_, + bytes32[] calldata merkleProof_ + ) internal view returns (Round storage round) { + round = rounds[roundId_]; + + // Validate round exists. + if (round.roundEnd == 0 && round.roundCap == 0) { + revert Module__LM_PC_FundingPot__RoundNotCreated(); + } + + // Validate access criteria. + _validateAccessCriteria(roundId_, accessId_, merkleProof_); + + return round; + } + + /// @notice Validates timing and cap constraints, adjusts amount if needed + /// @param roundId_ ID of the round + /// @param accessId_ ID of the access criteria + /// @param amount_ Requested contribution amount + /// @param round Round storage object + /// @return adjustedAmount The potentially adjusted contribution amount + /// @return canOverrideTimeAndCap Whether the user can override time and cap constraints + function _validateTimingAndCaps( + uint64 roundId_, + uint8 accessId_, + uint amount_, + Round storage round + ) internal view returns (uint adjustedAmount, bool canOverrideTimeAndCap) { + adjustedAmount = amount_; + + // Get access criteria privileges + AccessCriteriaPrivilages storage privileges = + accessCriteriaPrivilages[roundId_][accessId_]; + + canOverrideTimeAndCap = privileges.overrideCap; + + _validateTiming(round, privileges, canOverrideTimeAndCap); + + // Handle cap validation and amount adjustment + adjustedAmount = _validateAndAdjustCaps( + roundId_, amount_, round, privileges, canOverrideTimeAndCap + ); + + return (adjustedAmount, canOverrideTimeAndCap); + } + + /// @notice Validates timing constraints based on privileges + /// @param round Round storage object + /// @param privileges Access criteria privileges + /// @param canOverrideTimeAndCap Whether the user can override time constraints + function _validateTiming( + Round storage round, + AccessCriteriaPrivilages storage privileges, + bool canOverrideTimeAndCap + ) internal view { + if (canOverrideTimeAndCap) { + return; + } + + uint currentTime = block.timestamp; + + // Check custom timing for this access level if defined + uint effectiveStart = + privileges.start > 0 ? privileges.start : round.roundStart; + uint effectiveEnd = privileges.end > 0 ? privileges.end : round.roundEnd; + + if (currentTime < effectiveStart) { + revert Module__LM_PC_FundingPot__RoundHasNotStarted(); + } + if (effectiveEnd > 0 && currentTime > effectiveEnd) { + revert Module__LM_PC_FundingPot__RoundHasEnded(); + } + } + + /// @notice Validates cap constraints and adjusts amount if needed + /// @param roundId_ ID of the round + /// @param amount_ Requested contribution amount + /// @param round Round storage object + /// @param privileges Access criteria privileges + /// @param canOverrideTimeAndCap Whether the user can override cap constraints + /// @return adjustedAmount The potentially adjusted contribution amount + function _validateAndAdjustCaps( + uint64 roundId_, + uint amount_, + Round storage round, + AccessCriteriaPrivilages storage privileges, + bool canOverrideTimeAndCap + ) internal view returns (uint adjustedAmount) { + adjustedAmount = amount_; + + if (!canOverrideTimeAndCap && round.roundCap > 0) { + uint totalRoundContribution = _getTotalRoundContribution(roundId_); + uint effectiveRoundCap = round.roundCap; + + // If global accumulative caps are enabled, add unused capacity from previous rounds + if (round.globalAccumulativeCaps) { + uint unusedCapacityFromPrevious = 0; + for (uint64 i = 1; i < roundId_; i++) { + Round storage prevRound = rounds[i]; + if (!prevRound.globalAccumulativeCaps) continue; + + uint prevRoundTotal = _getTotalRoundContribution(i); + if (prevRoundTotal < prevRound.roundCap) { + unusedCapacityFromPrevious += + (prevRound.roundCap - prevRoundTotal); + } + } + effectiveRoundCap += unusedCapacityFromPrevious; + } + + if (totalRoundContribution >= effectiveRoundCap) { + revert Module__LM_PC_FundingPot__RoundCapReached(); + } + + // Adjust for remaining round cap + uint remainingRoundCap = effectiveRoundCap - totalRoundContribution; + if (adjustedAmount > remainingRoundCap) { + adjustedAmount = remainingRoundCap; + } + } + + // Check and adjust for personal cap + uint userPreviousContribution = + _getUserContribution(roundId_, msg.sender); + uint userPersonalCap = privileges.personalCap > 0 + ? privileges.personalCap + : _getUserPersonalCap(roundId_, msg.sender); + + if (userPreviousContribution + adjustedAmount > userPersonalCap) { + if (userPreviousContribution < userPersonalCap) { + adjustedAmount = userPersonalCap - userPreviousContribution; + } else { + revert Module__LM_PC_FundingPot__PersonalCapReached(); + } + } + + return adjustedAmount; + } + + /// @notice Helper function to check if a user meets specific access criteria + /// @param accessCriteria_ The access criteria to check against + /// @param user_ The user address to validate + /// @param merkleProof_ Optional merkle proof for merkle-based validation + /// @return (bool, AccessCriteriaId) Returns success and the access criteria type used + function _checkAccessCriteriaEligibility( + AccessCriteria storage accessCriteria_, + address user_, + bytes32[] memory merkleProof_ + ) internal view returns (bool, AccessCriteriaId) { + AccessCriteriaId criteriaId = accessCriteria_.accessCriteriaId; + + if (criteriaId == AccessCriteriaId.OPEN) { + return (true, criteriaId); + } + bool isValid; + if (criteriaId == AccessCriteriaId.NFT) { + isValid = _checkNftOwnership(accessCriteria_.nftContract, user_); + } else if (criteriaId == AccessCriteriaId.MERKLE) { + isValid = _validateMerkleProof( + accessCriteria_.merkleRoot, user_, merkleProof_ + ); + } else if (criteriaId == AccessCriteriaId.LIST) { + isValid = _checkAllowedAddressList( + accessCriteria_.allowedAddresses, user_ + ); + } + + return (isValid, criteriaId); + } + /// @notice Validates access criteria for a specific round and access type /// @dev Checks if a user meets the access requirements based on the round's access criteria /// @param roundId_ The ID of the round being validated @@ -556,18 +705,19 @@ contract LM_PC_FundingPot_v1 is return; } - bool accessGranted = false; - if (accessCriteria.accessCriteriaId == AccessCriteriaId.NFT) { - accessGranted = - _checkNftOwnership(accessCriteria.nftContract, msg.sender); - } else if (accessCriteria.accessCriteriaId == AccessCriteriaId.MERKLE) { - accessGranted = _validateMerkleProof( - accessCriteria.merkleRoot, msg.sender, merkleProof_ - ); - } else if (accessCriteria.accessCriteriaId == AccessCriteriaId.LIST) { - accessGranted = _checkAllowedAddressList( - accessCriteria.allowedAddresses, msg.sender - ); + (bool isValid, AccessCriteriaId criteriaId) = + _checkAccessCriteriaEligibility( + accessCriteria, msg.sender, merkleProof_ + ); + + if (!isValid) { + if (criteriaId == AccessCriteriaId.NFT) { + revert Module__LM_PC_FundingPot__AccessCriteriaNftFailed(); + } else if (criteriaId == AccessCriteriaId.MERKLE) { + revert Module__LM_PC_FundingPot__AccessCriteriaMerkleFailed(); + } else if (criteriaId == AccessCriteriaId.LIST) { + revert Module__LM_PC_FundingPot__AccessCriteriaListFailed(); + } } } @@ -606,15 +756,14 @@ contract LM_PC_FundingPot_v1 is view returns (uint) { - uint basePersonalCap = 500; Round storage round = rounds[roundId_]; if (round.globalAccumulativeCaps) { uint unusedCapacity = _getUnusedCapacityFromPreviousRounds(user_, roundId_); - return basePersonalCap + unusedCapacity; + return BASE_PERSONAL_CAP + unusedCapacity; } - return basePersonalCap; + return BASE_PERSONAL_CAP; } /// @notice Calculates unused contribution capacity from previous rounds @@ -627,12 +776,31 @@ contract LM_PC_FundingPot_v1 is uint64 currentRoundId_ ) internal view returns (uint) { uint totalUnusedCapacity = 0; + bytes32[] memory emptyProof = new bytes32[](0); + for (uint64 i = 1; i < currentRoundId_; i++) { Round storage prevRound = rounds[i]; - if (!prevRound.globalAccumulativeCaps) { - continue; + if (!prevRound.globalAccumulativeCaps) continue; + + uint personalCap = BASE_PERSONAL_CAP; + + // Check if there were specific privileges for this user in previous rounds + for (uint8 j = 0; j < 4; j++) { + AccessCriteria storage accessCriteria = + prevRound.accessCriterias[j]; + (bool isValid,) = _checkAccessCriteriaEligibility( + accessCriteria, user_, emptyProof + ); + + if (isValid) { + AccessCriteriaPrivilages storage privileges = + accessCriteriaPrivilages[i][j]; + if (privileges.personalCap > personalCap) { + personalCap = privileges.personalCap; + } + } } - uint personalCap = 1000 ether; + uint userContribution = _getUserContribution(i, user_); if (userContribution < personalCap) { totalUnusedCapacity += (personalCap - userContribution); @@ -641,18 +809,6 @@ contract LM_PC_FundingPot_v1 is return totalUnusedCapacity; } - /// @notice Records a contribution for a user in a specific round - /// @dev Updates the user's contribution and the total round contribution - /// @param roundId_ The ID of the round - /// @param user_ The address of the user making the contribution - /// @param amount_ The amount of the contribution - function _recordContribution(uint64 roundId_, address user_, uint amount_) - internal - { - userContributions[roundId_][user_] += amount_; - roundTotalContributions[roundId_] += amount_; - } - ///@notice Checks if a sender is in a list of allowed addresses /// @dev Performs a linear search to validate address inclusion /// @param allowedAddresses Array of addresses permitted to participate @@ -668,7 +824,6 @@ contract LM_PC_FundingPot_v1 is } } revert Module__LM_PC_FundingPot__AccessCriteriaListFailed(); - return false; } /// @notice Verifies NFT ownership for access control @@ -695,15 +850,35 @@ contract LM_PC_FundingPot_v1 is } } + /// @notice Verifies a Merkle proof for access control + /// @dev Validates that the user's address is part of the Merkle tree + /// @param root_ The Merkle root to validate against + /// @param user_ The address of the user to check + /// @param merkleProof_ The Merkle proof to verify + /// @return Boolean indicating whether the proof is valid function _validateMerkleProof( bytes32 root_, address user_, - bytes32[] calldata merkleProof_ + bytes32[] memory merkleProof_ ) internal pure returns (bool) { bytes32 leaf = keccak256(abi.encodePacked(user_)); if (!MerkleProof.verify(merkleProof_, root_, leaf)) { revert Module__LM_PC_FundingPot__AccessCriteriaMerkleFailed(); } + + return true; + } + + /// @notice Records a contribution for a user in a specific round + /// @dev Updates the user's contribution and the total round contribution + /// @param roundId_ The ID of the round + /// @param user_ The address of the user making the contribution + /// @param amount_ The amount of the contribution + function _recordContribution(uint64 roundId_, address user_, uint amount_) + internal + { + userContributions[roundId_][user_] += amount_; + roundTotalContributions[roundId_] += amount_; } } diff --git a/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol b/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol index 0dfe0cf54..58db38a22 100644 --- a/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol +++ b/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol @@ -47,7 +47,9 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { // Constants bytes32 internal constant FUNDING_POT_ADMIN_ROLE = "FUNDING_POT_ADMIN"; - address contributor_; + address contributor1_; + address contributor2_; + address contributor3_; bytes32 PROOF_ONE = 0x0fd7c981d39bece61f7499702bf59b3114a90e66b51ba2c53abdf7b62986c00a; @@ -99,9 +101,14 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { address impl = address(new LM_PC_FundingPot_v1_Exposed()); fundingPot = LM_PC_FundingPot_v1_Exposed(Clones.clone(impl)); - // Mint tokens to the contributor - contributor_ = address(0xBeef); - fundingPotToken.mint(contributor_, 10_000); + // Mint tokens to the contributors + contributor1_ = address(0xBeef); + contributor2_ = address(0xDEAD); + contributor3_ = address(0xCAFE); + + fundingPotToken.mint(contributor1_, 10_000); + fundingPotToken.mint(contributor2_, 10_000); + fundingPotToken.mint(contributor3_, 10_000); // Setup the module to test _setUpOrchestrator(fundingPot); @@ -995,6 +1002,10 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { ├── Then it should not revert └── Then the access criteria should be updated */ + function testFuzzContributeToRound_revertsGivenRoundContributionCapReached( + uint roundCap_ + ) public { + testCreateRound(10); function testFuzzEditAccessCriteriaForRound_revertsGivenUserDoesNotHaveFundingPotAdminRole( uint8 accessCriteriaEnum, @@ -1078,8 +1089,14 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { (uint roundStart,,,,,,) = fundingPot.getRoundGenericParameters(roundId); vm.warp(roundStart + 1); - ILM_PC_FundingPot_v1.AccessCriteria memory accessCriteria = - _helper_createAccessCriteria(accessCriteriaEnum); + // Approve + vm.prank(contributor1_); + fundingPotToken.approve(address(fundingPot), 110); + + vm.prank(contributor1_); + fundingPot.contributeToRound( + roundId, 10, accessId, address(fundingPotToken), new bytes32[](0) + ); vm.expectRevert( abi.encodeWithSelector( @@ -1088,48 +1105,94 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { .selector ) ); - fundingPot.editAccessCriteriaForRound( - roundId, accessCriteriaId, accessCriteria + + vm.prank(contributor1_); + fundingPot.contributeToRound( + roundId, + amount, + accessId, + address(fundingPotToken), + new bytes32[](0) ); } - function testFuzzEditAccessCriteriaForRound( - uint8 accessCriteriaEnumOld, - uint8 accessCriteriaEnumNew + function testFuzzContributeToRound_revertsGivenContributionIsBeforeRoundStart( ) public { - vm.assume(accessCriteriaEnumOld >= 0 && accessCriteriaEnumOld <= 4); - vm.assume( - accessCriteriaEnumNew != accessCriteriaEnumOld - && accessCriteriaEnumNew >= 0 && accessCriteriaEnumNew <= 4 - ); + testCreateRound(1000); - _helper_setupRoundWithAccessCriteria(accessCriteriaEnumOld); uint64 roundId = fundingPot.getRoundCount(); - uint8 accessCriteriaId = 0; + uint8 accessId = 1; + uint amount = 250; - // Create and apply new access criteria - ILM_PC_FundingPot_v1.AccessCriteria memory newAccessCriteria = - _helper_createAccessCriteria(accessCriteriaEnumNew); + ILM_PC_FundingPot_v1.AccessCriteria memory accessCriteria = + _helper_createAccessCriteria(0); - fundingPot.editAccessCriteriaForRound( - roundId, accessCriteriaId, newAccessCriteria + fundingPot.setAccessCriteriaForRound(roundId, accessId, accessCriteria); + + (uint roundStart,,,,,,) = fundingPot.getRoundGenericParameters(roundId); + + // Approve + vm.prank(contributor1_); + fundingPotToken.approve(address(fundingPot), 500); + + vm.expectRevert( + abi.encodeWithSelector( + ILM_PC_FundingPot_v1 + .Module__LM_PC_FundingPot__RoundHasNotStarted + .selector + ) ); - // Verify the access criteria was updated - ( - bool isOpen, - address nftContract, - bytes32 merkleRoot, - address[] memory allowedAddresses - ) = fundingPot.getRoundAccessCriteria(roundId, accessCriteriaId); + vm.prank(contributor1_); + fundingPot.contributeToRound( + roundId, + amount, + accessId, + address(fundingPotToken), + new bytes32[](0) + ); + } + + function testFuzzContributeToRound_revertsGivenContributionIsAfterRoundEnd() + public + { + testCreateRound(1000); - assertEq(isOpen, accessCriteriaEnumNew == 1); - assertEq(nftContract, newAccessCriteria.nftContract); - assertEq(merkleRoot, newAccessCriteria.merkleRoot); - assertEq(allowedAddresses, newAccessCriteria.allowedAddresses); + uint64 roundId = fundingPot.getRoundCount(); + uint8 accessId = 1; + uint amount = 250; + + ILM_PC_FundingPot_v1.AccessCriteria memory accessCriteria = + _helper_createAccessCriteria(0); + + fundingPot.setAccessCriteriaForRound(roundId, accessId, accessCriteria); + + (uint roundStart,,,,,,) = fundingPot.getRoundGenericParameters(roundId); + vm.warp(roundStart + 10 days); + + // Approve + vm.prank(contributor1_); + fundingPotToken.approve(address(fundingPot), 500); + + vm.expectRevert( + abi.encodeWithSelector( + ILM_PC_FundingPot_v1 + .Module__LM_PC_FundingPot__RoundHasEnded + .selector + ) + ); + + vm.prank(contributor1_); + fundingPot.contributeToRound( + roundId, + amount, + accessId, + address(fundingPotToken), + new bytes32[](0) + ); } - function testFuzzContributeToRound_revertsWhenNFTAccessCriteriaIsNotMet() + function testFuzzContributeToRound_revertsGivenNFTAccessCriteriaIsNotMet() public { testCreateRound(1000); @@ -1147,7 +1210,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { vm.warp(roundStart + 1); // Approve - vm.prank(contributor_); + vm.prank(contributor1_); fundingPotToken.approve(address(fundingPot), amount); vm.expectRevert( @@ -1158,7 +1221,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { ) ); - vm.prank(contributor_); + vm.prank(contributor1_); fundingPot.contributeToRound( roundId, amount, @@ -1168,7 +1231,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { ); } - function testFuzzContributeToRound_revertsWhenMerkleRootAccessCriteriaIsNotMet( + function testFuzzContributeToRound_revertsGivenMerkleRootAccessCriteriaIsNotMet( ) public { testCreateRound(1000); @@ -1185,7 +1248,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { vm.warp(roundStart + 1); // Approve - vm.prank(contributor_); + vm.prank(contributor1_); fundingPotToken.approve(address(fundingPot), amount); vm.expectRevert( @@ -1196,13 +1259,13 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { ) ); - vm.prank(contributor_); + vm.prank(contributor1_); fundingPot.contributeToRound( roundId, amount, accessId, address(fundingPotToken), PROOF ); } - function testFuzzContributeToRound_revertsWhenAllowedListAccessCriteriaIsNotMet( + function testFuzzContributeToRound_revertsGivenAllowedListAccessCriteriaIsNotMet( ) public { testCreateRound(1000); @@ -1219,7 +1282,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { vm.warp(roundStart + 1); // Approve - vm.prank(contributor_); + vm.prank(contributor1_); fundingPotToken.approve(address(fundingPot), amount); vm.expectRevert( @@ -1230,7 +1293,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { ) ); - vm.prank(contributor_); + vm.prank(contributor1_); fundingPot.contributeToRound( roundId, amount, @@ -1240,29 +1303,29 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { ); } - function testFuzzContributeToRound_revertsWhenContributionExceedsPersonalCap( + function testFuzzContributeToRound_revertsGivenPreviousContributionExceedsPersonalCap( ) public { testCreateRound(1000); uint64 roundId = fundingPot.getRoundCount(); uint8 accessId = 1; - uint amount = 250; + uint amount = 500; ILM_PC_FundingPot_v1.AccessCriteria memory accessCriteria = _helper_createAccessCriteria(1); fundingPot.setAccessCriteriaForRound(roundId, accessId, accessCriteria); - mockNFTContract.mint(contributor_); + mockNFTContract.mint(contributor1_); (uint roundStart,,,,,,) = fundingPot.getRoundGenericParameters(roundId); vm.warp(roundStart + 1); // Approve - vm.prank(contributor_); - fundingPotToken.approve(address(fundingPot), 500); + vm.prank(contributor1_); + fundingPotToken.approve(address(fundingPot), 1000); - vm.prank(contributor_); + vm.prank(contributor1_); fundingPot.contributeToRound( roundId, amount, @@ -1271,6 +1334,10 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { new bytes32[](0) ); + // Get the base personal cap from the contract + uint personalCap = + fundingPot.exposed_getUserPersonalCap(roundId, contributor1_); + // Attempt to contribute beyond personal cap vm.expectRevert( abi.encodeWithSelector( @@ -1279,15 +1346,434 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { .selector ) ); - vm.prank(contributor_); + vm.prank(contributor1_); + uint remainingCap = personalCap - amount; fundingPot.contributeToRound( - roundId, 251, 0, address(fundingPotToken), new bytes32[](0) + roundId, 251, accessId, address(fundingPotToken), new bytes32[](0) ); } - // ------------------------------------------------------------------------- - // Test: Internal Functions + /* + ├── Given a round has been configured with generic round configuration and access criteria + │ And the round has started + │ And the user fulfills the access criteria + │ And the user doesn't violate any privileges + │ And the user doesn't violate generic round parameters + │ And the user has approved the collateral token + │ └── When the user contributes to the round + │ └── Then the funds are transferred to the funding pot + │ And the contribution is recorded + ├── Given the round contribution cap is not reached + │ └── When the user contributes to the round so that it exceeds the round contribution cap + │ └── Then only the valid contribution amount is transferred to the funding pot + │ And the contribution is recorded + │ And round closure is initiated + │ + ├── Given the user fulfills the access criteria + │ └── And the user has already contributed their personal cap partially + │ └── When the user attempts to contribute more than their personal cap + │ └── Then only the amount up to the cap is accepted as contribution + │ And the contribution is recorded + │ + ├── Given the user fulfills the access criteria + │ └── And their access criteria has the privilege to override the contribution span + │ └── When + │ └── And the user attempts to contribute + │ └── Then the contribution is still recorded + │ + ├── Given the user fulfills the access criteria + │ And the round is set to have global accumulative caps + │ And the user has not fully utilized their personal contribution potential in previous rounds + │ └── When the user wants to contribute to the current round + │ └── Then they can contribute up to their personal limit of the current round plus unfilled potential from previous rounds + │ + ├── Given the round has been configured with global accumulative caps + │ And in the previous round the round contribution cap was X + │ And in total Y had been contributed in the previous round + │ And the round contribution cap for the current round is Z + │ └── When users attempt to contribute + │ └── Then they can in total contribute Z + X - Y + │ And the funds are transferred into the funding pot + │ + */ + function testFuzzContributeToRound_worksGivenAllConditionsMet() public { + testCreateRound(1000); + + uint64 roundId = fundingPot.getRoundCount(); + uint8 accessId = 1; + uint amount = 250; + + ILM_PC_FundingPot_v1.AccessCriteria memory accessCriteria = + _helper_createAccessCriteria(1); + + fundingPot.setAccessCriteriaForRound(roundId, accessId, accessCriteria); + + mockNFTContract.mint(contributor1_); + + (uint roundStart,,,,,,) = fundingPot.getRoundGenericParameters(roundId); + vm.warp(roundStart + 1); + + // Approve + vm.prank(contributor1_); + fundingPotToken.approve(address(fundingPot), 500); + + vm.prank(contributor1_); + fundingPot.contributeToRound( + roundId, + amount, + accessId, + address(fundingPotToken), + new bytes32[](0) + ); + + uint totalContributions = + fundingPot.exposed_getTotalRoundContributions(roundId); + + assertEq(totalContributions, amount); + + uint personalContributions = + fundingPot.exposed_getUserContribution(roundId, contributor1_); + assertEq(personalContributions, amount); + } + + function testFuzzContributeToRound_worksGivenUserCurrentContributionExceedsTheRoundCap( + uint roundCap_ + ) public { + vm.assume(accessCriteriaEnumOld >= 0 && accessCriteriaEnumOld <= 4); + vm.assume( + accessCriteriaEnumNew != accessCriteriaEnumOld + && accessCriteriaEnumNew >= 0 && accessCriteriaEnumNew <= 4 + ); + + _helper_setupRoundWithAccessCriteria(accessCriteriaEnumOld); + uint64 roundId = fundingPot.getRoundCount(); + uint8 accessCriteriaId = 0; + + // Create and apply new access criteria + ILM_PC_FundingPot_v1.AccessCriteria memory newAccessCriteria = + _helper_createAccessCriteria(accessCriteriaEnumNew); + + fundingPot.setAccessCriteriaForRound(roundId, accessId, accessCriteria); + + (uint roundStart,, uint roundCap,,,,) = + fundingPot.getRoundGenericParameters(roundId); + vm.warp(roundStart + 1); + + // Approve + vm.prank(contributor1_); + fundingPotToken.approve(address(fundingPot), amount); + + vm.prank(contributor1_); + fundingPot.contributeToRound( + roundId, + amount, + accessId, + address(fundingPotToken), + new bytes32[](0) + ); + + // only the amount that does not exceed the roundcap is contributed + assertEq( + fundingPot.exposed_getUserContribution(roundId, contributor1_), 200 + ); + } + + function testFuzzContributeToRound_worksGivenContributionPartiallyExceedingPersonalCap( + ) public { + testCreateRound(1000); + + uint64 roundId = fundingPot.getRoundCount(); + uint8 accessId = 1; + + uint firstAmount = 400; + + ILM_PC_FundingPot_v1.AccessCriteria memory accessCriteria = + _helper_createAccessCriteria(1); + + fundingPot.setAccessCriteriaForRound(roundId, accessId, accessCriteria); + + mockNFTContract.mint(contributor1_); + + (uint roundStart,,,,,,) = fundingPot.getRoundGenericParameters(roundId); + vm.warp(roundStart + 1); + + // Approve + vm.prank(contributor1_); + fundingPotToken.approve(address(fundingPot), 1000 ether); + + // First contribution + vm.prank(contributor1_); + fundingPot.contributeToRound( + roundId, + firstAmount, + accessId, + address(fundingPotToken), + new bytes32[](0) + ); + + // Get the personal cap + uint personalCap = + fundingPot.exposed_getUserPersonalCap(roundId, contributor1_); + + uint secondAmount = 200; + + vm.prank(contributor1_); + fundingPot.contributeToRound( + roundId, + secondAmount, + accessId, + address(fundingPotToken), + new bytes32[](0) + ); + + uint totalContribution = + fundingPot.exposed_getUserContribution(roundId, contributor1_); + assertEq(totalContribution, personalCap); + } + + function testFuzzContributeToRound_worksGivenUserCanOverrideTimeConstraints( + ) public { + testCreateRound(1000); + + uint64 roundId = fundingPot.getRoundCount(); + uint8 accessId = 1; + uint amount = 250; + + ILM_PC_FundingPot_v1.AccessCriteria memory accessCriteria = + _helper_createAccessCriteria(1); + + fundingPot.setAccessCriteriaForRound(roundId, accessId, accessCriteria); + + // Set privileges with override capability + fundingPot.setAccessCriteriaPrivilages( + roundId, + accessId, + 500, // personalCap + true, // overrideCap (can override time constraints) + 0, // start + 0, // cliff + 0 // end + ); + + mockNFTContract.mint(contributor1_); + + (uint roundStart,,,,,,) = fundingPot.getRoundGenericParameters(roundId); + + vm.warp(roundStart + 10 days); + + // Approve + vm.prank(contributor1_); + fundingPotToken.approve(address(fundingPot), amount); + + // This should succeed despite being after round end, due to override privilege + vm.prank(contributor1_); + fundingPot.contributeToRound( + roundId, + amount, + accessId, + address(fundingPotToken), + new bytes32[](0) + ); + + // Verify the contribution was recorded + uint totalContribution = + fundingPot.exposed_getTotalRoundContributions(roundId); + assertEq(totalContribution, amount); + } + + function testContributeToRound_worksGivenPersonalCapAccumulation() public { + ( + uint roundStart, + uint roundEnd, + uint roundCap, + address hookContract, + bytes memory hookFunction, + bool closureMechanism, + bool globalAccumulativeCaps + ) = _helper_createDefaultFundingRound(1000); + globalAccumulativeCaps = true; // global accumulative caps enabled + + // Round 1 + fundingPot.createRound( + roundStart, + roundEnd, + roundCap, + hookContract, + hookFunction, + closureMechanism, + globalAccumulativeCaps + ); + uint64 round1Id = fundingPot.getRoundCount(); + + uint8 accessId = 1; + ILM_PC_FundingPot_v1.AccessCriteria memory accessCriteria = + _helper_createAccessCriteria(1); + fundingPot.setAccessCriteriaForRound(round1Id, accessId, accessCriteria); + mockNFTContract.mint(contributor1_); + + // Create Round 2 + fundingPot.createRound( + roundStart + 3 days, + roundEnd + 3 days, + roundCap, + hookContract, + hookFunction, + closureMechanism, + globalAccumulativeCaps + ); + uint64 round2Id = fundingPot.getRoundCount(); + fundingPot.setAccessCriteriaForRound(round2Id, accessId, accessCriteria); + + vm.startPrank(contributor1_); + fundingPotToken.approve(address(fundingPot), 1500); + + // Contribute to Round 1 + vm.warp(roundStart + 1); + uint round1Contribution = 200; + fundingPot.contributeToRound( + round1Id, + round1Contribution, + accessId, + address(fundingPotToken), + new bytes32[](0) + ); + + // Move to Round 2 + vm.warp(roundStart + 3 days + 1); + + // Calculate unused capacity from Round 1 + uint basePersonalCap = + fundingPot.exposed_getUserPersonalCap(round1Id, contributor1_); + uint unusedCapacityFromRound1 = basePersonalCap - round1Contribution; + + uint round2MaxContribution = basePersonalCap + unusedCapacityFromRound1; + fundingPot.contributeToRound( + round2Id, + round2MaxContribution, + accessId, + address(fundingPotToken), + new bytes32[](0) + ); + vm.stopPrank(); + + assertEq( + fundingPot.exposed_getUserContribution(round1Id, contributor1_), + round1Contribution + ); + assertEq( + fundingPot.exposed_getUserContribution(round2Id, contributor1_), + round2MaxContribution + ); + } + + function testContributeToRound_worksGivenTotalRoundCapAccumulation() + public + { + ( + uint roundStart, + uint roundEnd, + uint roundCap, + address hookContract, + bytes memory hookFunction, + bool closureMechanism, + bool globalAccumulativeCaps + ) = _helper_createDefaultFundingRound(1000); + globalAccumulativeCaps = true; + + // Create Round 1 + fundingPot.createRound( + roundStart, + roundEnd, + roundCap, + hookContract, + hookFunction, + closureMechanism, + globalAccumulativeCaps + ); + uint64 round1Id = fundingPot.getRoundCount(); + + uint8 accessId = 0; + ILM_PC_FundingPot_v1.AccessCriteria memory accessCriteria = + _helper_createAccessCriteria(0); + fundingPot.setAccessCriteriaForRound(round1Id, accessId, accessCriteria); + + // Round 2 with a different cap + uint round2Cap = 500; + fundingPot.createRound( + roundStart + 3 days, + roundEnd + 3 days, + round2Cap, + hookContract, + hookFunction, + closureMechanism, + globalAccumulativeCaps + ); + uint64 round2Id = fundingPot.getRoundCount(); + fundingPot.setAccessCriteriaForRound(round2Id, accessId, accessCriteria); + // Round 1: Multiple users contribute, but don't reach the cap + vm.warp(roundStart + 1); + + vm.startPrank(contributor1_); + fundingPotToken.approve(address(fundingPot), 300); + fundingPot.contributeToRound( + round1Id, 300, accessId, address(fundingPotToken), new bytes32[](0) + ); + vm.stopPrank(); + + vm.startPrank(contributor2_); + fundingPotToken.approve(address(fundingPot), 200); + fundingPot.contributeToRound( + round1Id, 200, accessId, address(fundingPotToken), new bytes32[](0) + ); + vm.stopPrank(); + + // Move to Round 2 + vm.warp(roundStart + 3 days + 1); + + uint unusedCapacityFromRound1 = + roundCap - fundingPot.exposed_getTotalRoundContributions(round1Id); + uint totalAvailableCapacityRound2 = round2Cap + unusedCapacityFromRound1; + + // Round 2: Contributors try to use the accumulated capacity + + vm.startPrank(contributor2_); + fundingPotToken.approve(address(fundingPot), 400); + fundingPot.contributeToRound( + round2Id, 400, accessId, address(fundingPotToken), new bytes32[](0) + ); + vm.stopPrank(); + + vm.startPrank(contributor3_); + fundingPotToken.approve(address(fundingPot), 600); + fundingPot.contributeToRound( + round2Id, 600, accessId, address(fundingPotToken), new bytes32[](0) + ); + vm.stopPrank(); + + // Verify Round 1 contributions + assertEq(fundingPot.exposed_getTotalRoundContributions(round1Id), 500); + assertEq( + fundingPot.exposed_getUserContribution(round1Id, contributor1_), 300 + ); + assertEq( + fundingPot.exposed_getUserContribution(round1Id, contributor2_), 200 + ); + + // Verify Round 2 contributions + assertEq(fundingPot.exposed_getTotalRoundContributions(round2Id), 1000); + assertEq( + fundingPot.exposed_getUserContribution(round2Id, contributor2_), 400 + ); + assertEq( + fundingPot.exposed_getUserContribution(round2Id, contributor3_), 600 + ); + + assertEq( + fundingPot.exposed_getTotalRoundContributions(round2Id), + totalAvailableCapacityRound2 + ); + } // ------------------------------------------------------------------------- // Helper Functions diff --git a/test/unit/modules/logicModule/LM_PC_FundingPot_v1_Exposed.sol b/test/unit/modules/logicModule/LM_PC_FundingPot_v1_Exposed.sol index a2557f4dc..4b939ab46 100644 --- a/test/unit/modules/logicModule/LM_PC_FundingPot_v1_Exposed.sol +++ b/test/unit/modules/logicModule/LM_PC_FundingPot_v1_Exposed.sol @@ -7,6 +7,43 @@ import {LM_PC_FundingPot_v1} from // Access Mock of the PP_Template_v1 contract for Testing. contract LM_PC_FundingPot_v1_Exposed is LM_PC_FundingPot_v1 { -// Use the `exposed_` prefix for functions to expose internal functions for -// testing. + // Use the `exposed_` prefix for functions to expose internal functions for + // testing. + + function exposed_getTotalRoundContributions(uint64 roundId_) + external + view + returns (uint) + { + return _getTotalRoundContribution(roundId_); + } + + function exposed_getUserContribution(uint64 roundId_, address user_) + external + view + returns (uint) + { + return _getUserContribution(roundId_, user_); + } + + /** + * @notice Exposes the internal _getUserPersonalCap function for testing + */ + function exposed_getUserPersonalCap(uint64 roundId_, address user_) + external + view + returns (uint) + { + return _getUserPersonalCap(roundId_, user_); + } + + /** + * @notice Exposes the internal _getUnusedCapacityFromPreviousRounds function for testing + */ + function exposed_getUnusedCapacityFromPreviousRounds( + address user_, + uint64 currentRoundId_ + ) external view returns (uint) { + return _getUnusedCapacityFromPreviousRounds(user_, currentRoundId_); + } } From 2165837e875b79608c7835271ada5af3c921eb3d Mon Sep 17 00:00:00 2001 From: JeffreyJoel Date: Wed, 2 Apr 2025 14:52:03 +0100 Subject: [PATCH 040/130] fix:update code based on internal review --- .../logicModule/LM_PC_FundingPot_v1.sol | 166 +++++++++--------- .../interfaces/ILM_PC_FundingPot_v1.sol | 47 +++-- .../logicModule/LM_PC_FundingPot_v1.t.sol | 108 +++--------- 3 files changed, 142 insertions(+), 179 deletions(-) diff --git a/src/modules/logicModule/LM_PC_FundingPot_v1.sol b/src/modules/logicModule/LM_PC_FundingPot_v1.sol index b2d2eca6f..efdf89db6 100644 --- a/src/modules/logicModule/LM_PC_FundingPot_v1.sol +++ b/src/modules/logicModule/LM_PC_FundingPot_v1.sol @@ -97,8 +97,8 @@ contract LM_PC_FundingPot_v1 is mapping(uint64 => uint8) private roundIdtoAccessId; /// @notice Stores all access criteria privilages by their unique ID. mapping( - uint64 roundId => mapping(uint8 accessId => AccessCriteriaPrivilages) - ) private accessCriteriaPrivilages; + uint64 roundId => mapping(uint8 accessId => AccessCriteriaPrivileges) + ) private accessCriteriaPrivileges; /// @notice Maps round IDs to user addresses to contribution amounts mapping(uint64 => mapping(address => uint)) private userContributions; @@ -106,8 +106,11 @@ contract LM_PC_FundingPot_v1 is /// @notice Maps round IDs to total contributions mapping(uint64 => uint) private roundTotalContributions; - /// @notice The next available round ID. - uint64 private nextRoundId; + /// @notice The current round count. + uint64 private roundCount; + + /// @notice The token used for contributions. + IERC20 private contributionToken; /// @notice Storage gap for future upgrades. uint[50] private __gap; @@ -130,7 +133,7 @@ contract LM_PC_FundingPot_v1 is bytes memory configData_ ) external override(Module_v1) initializer { __Module_init(orchestrator_, metadata_); - + address fundingPotToken; // Set the flags for the PaymentOrders (this module uses 3 flags). bytes32 flags; flags |= bytes32(1 << FLAG_START); @@ -138,6 +141,8 @@ contract LM_PC_FundingPot_v1 is flags |= bytes32(1 << FLAG_END); __ERC20PaymentClientBase_v2_init(flags); + + contributionToken = IERC20(address(fundingPotToken)); } // ------------------------------------------------------------------------- @@ -193,7 +198,7 @@ contract LM_PC_FundingPot_v1 is } /// @inheritdoc ILM_PC_FundingPot_v1 - function getRoundAccessCriteriaPrivilages(uint64 roundId_, uint8 accessId_) + function getRoundAccessCriteriaPrivileges(uint64 roundId_, uint8 accessId_) external view returns ( @@ -213,8 +218,8 @@ contract LM_PC_FundingPot_v1 is } // Store the privileges in a local variable to reduce stack usage. - AccessCriteriaPrivilages storage privs = - accessCriteriaPrivilages[roundId_][accessId_]; + AccessCriteriaPrivileges storage privs = + accessCriteriaPrivileges[roundId_][accessId_]; return ( false, @@ -228,7 +233,7 @@ contract LM_PC_FundingPot_v1 is /// @inheritdoc ILM_PC_FundingPot_v1 function getRoundCount() external view returns (uint64) { - return nextRoundId; + return roundCount; } /// @inheritdoc ILM_PC_FundingPot_v1 @@ -253,9 +258,9 @@ contract LM_PC_FundingPot_v1 is bool autoClosure_, bool globalAccumulativeCaps_ ) external onlyModuleRole(FUNDING_POT_ADMIN_ROLE) returns (uint64) { - nextRoundId++; + roundCount++; - uint64 roundId = nextRoundId; + uint64 roundId = roundCount; Round storage round = rounds[roundId]; round.roundStart = roundStart_; @@ -371,14 +376,14 @@ contract LM_PC_FundingPot_v1 is } /// @inheritdoc ILM_PC_FundingPot_v1 - function setAccessCriteriaPrivilages( + function setAccessCriteriaPrivileges( uint64 roundId_, uint8 accessId_, uint personalCap_, bool overrideCap_, - uint _start, - uint _cliff, - uint _end + uint start_, + uint cliff_, + uint end_ ) external onlyModuleRole(FUNDING_POT_ADMIN_ROLE) { Round storage round = rounds[roundId_]; @@ -389,29 +394,29 @@ contract LM_PC_FundingPot_v1 is == AccessCriteriaId.OPEN ) { revert - Module__LM_PC_FundingPot__CannotSetPrivilagesForOpenAccessCriteria(); + Module__LM_PC_FundingPot__CannotSetPrivilegesForOpenAccessCriteria(); } - if (!_validTimes(_start, _cliff, _end)) { + if (!_validTimes(start_, cliff_, end_)) { revert Module__LM_PC_FundingPot__InvalidTimes(); } - AccessCriteriaPrivilages storage accessCriteriaPrivilages = - accessCriteriaPrivilages[roundId_][accessId_]; + AccessCriteriaPrivileges storage accessCriteriaPrivileges = + accessCriteriaPrivileges[roundId_][accessId_]; - accessCriteriaPrivilages.personalCap = personalCap_; - accessCriteriaPrivilages.overrideCap = overrideCap_; - accessCriteriaPrivilages.start = _start; - accessCriteriaPrivilages.cliff = _cliff; - accessCriteriaPrivilages.end = _end; + accessCriteriaPrivileges.personalCap = personalCap_; + accessCriteriaPrivileges.overrideCap = overrideCap_; + accessCriteriaPrivileges.start = start_; + accessCriteriaPrivileges.cliff = cliff_; + accessCriteriaPrivileges.end = end_; - emit AccessCriteriaPrivilagesSet( + emit AccessCriteriaPrivilegesSet( roundId_, accessId_, personalCap_, overrideCap_, - _start, - _cliff, - _end + start_, + cliff_, + end_ ); } @@ -419,8 +424,7 @@ contract LM_PC_FundingPot_v1 is function contributeToRound( uint64 roundId_, uint amount_, - uint8 accessId_, - address contributionToken_, + uint8 accessCriteriaId_, bytes32[] calldata merkleProof_ ) external { // Validate input amount. @@ -429,18 +433,20 @@ contract LM_PC_FundingPot_v1 is } // Validate round and access criteria - Round storage round = - _validateRoundAndAccessCriteria(roundId_, accessId_, merkleProof_); + Round storage round = _validateRoundAndAccessCriteria( + roundId_, accessCriteriaId_, merkleProof_ + ); // Check timing and caps based on privileges (uint adjustedAmount, bool canOverrideTimeAndCap) = - _validateTimingAndCaps(roundId_, accessId_, amount_, round); + _validateTimingAndCaps(roundId_, accessCriteriaId_, amount_, round); + + _recordContribution(roundId_, msg.sender, adjustedAmount); - IERC20(contributionToken_).safeTransferFrom( + IERC20(contributionToken).safeTransferFrom( msg.sender, address(this), adjustedAmount ); - _recordContribution(roundId_, msg.sender, adjustedAmount); emit ContributionMade(roundId_, msg.sender, adjustedAmount); } @@ -496,18 +502,18 @@ contract LM_PC_FundingPot_v1 is } /// @dev Validate uint start input. - /// @param _start uint to validate. - /// @param _cliff uint to validate. - /// @param _end uint to validate. + /// @param start_ uint to validate. + /// @param cliff_ uint to validate. + /// @param end_ uint to validate. /// @return True if uint is valid. - function _validTimes(uint _start, uint _cliff, uint _end) + function _validTimes(uint start_, uint cliff_, uint end_) internal pure returns (bool) { - // _start + _cliff should be less or equal to _end - // this already implies that _start is not greater than _end - return _start + _cliff <= _end; + // start_ + cliff_ should be less or equal to end_ + // this already implies that start_ is not greater than end_ + return start_ + cliff_ <= end_; } /// @notice Validates the round existence and access criteria @@ -537,43 +543,43 @@ contract LM_PC_FundingPot_v1 is /// @param roundId_ ID of the round /// @param accessId_ ID of the access criteria /// @param amount_ Requested contribution amount - /// @param round Round storage object + /// @param round_ Round storage object /// @return adjustedAmount The potentially adjusted contribution amount /// @return canOverrideTimeAndCap Whether the user can override time and cap constraints function _validateTimingAndCaps( uint64 roundId_, uint8 accessId_, uint amount_, - Round storage round + Round storage round_ ) internal view returns (uint adjustedAmount, bool canOverrideTimeAndCap) { adjustedAmount = amount_; // Get access criteria privileges - AccessCriteriaPrivilages storage privileges = - accessCriteriaPrivilages[roundId_][accessId_]; + AccessCriteriaPrivileges storage privileges = + accessCriteriaPrivileges[roundId_][accessId_]; canOverrideTimeAndCap = privileges.overrideCap; - _validateTiming(round, privileges, canOverrideTimeAndCap); + _validateTiming(round_, privileges, canOverrideTimeAndCap); // Handle cap validation and amount adjustment adjustedAmount = _validateAndAdjustCaps( - roundId_, amount_, round, privileges, canOverrideTimeAndCap + roundId_, amount_, round_, privileges, canOverrideTimeAndCap ); return (adjustedAmount, canOverrideTimeAndCap); } /// @notice Validates timing constraints based on privileges - /// @param round Round storage object - /// @param privileges Access criteria privileges - /// @param canOverrideTimeAndCap Whether the user can override time constraints + /// @param round_ Round storage object + /// @param privileges_ Access criteria privileges + /// @param canOverrideTimeAndCap_ Whether the user can override time constraints function _validateTiming( - Round storage round, - AccessCriteriaPrivilages storage privileges, - bool canOverrideTimeAndCap + Round storage round_, + AccessCriteriaPrivileges storage privileges_, + bool canOverrideTimeAndCap_ ) internal view { - if (canOverrideTimeAndCap) { + if (canOverrideTimeAndCap_) { return; } @@ -581,8 +587,9 @@ contract LM_PC_FundingPot_v1 is // Check custom timing for this access level if defined uint effectiveStart = - privileges.start > 0 ? privileges.start : round.roundStart; - uint effectiveEnd = privileges.end > 0 ? privileges.end : round.roundEnd; + privileges_.start > 0 ? privileges_.start : round_.roundStart; + uint effectiveEnd = + privileges_.end > 0 ? privileges_.end : round_.roundEnd; if (currentTime < effectiveStart) { revert Module__LM_PC_FundingPot__RoundHasNotStarted(); @@ -595,27 +602,27 @@ contract LM_PC_FundingPot_v1 is /// @notice Validates cap constraints and adjusts amount if needed /// @param roundId_ ID of the round /// @param amount_ Requested contribution amount - /// @param round Round storage object - /// @param privileges Access criteria privileges - /// @param canOverrideTimeAndCap Whether the user can override cap constraints + /// @param round_ Round storage object + /// @param privileges_ Access criteria privileges + /// @param canOverrideTimeAndCap_ Whether the user can override cap constraints /// @return adjustedAmount The potentially adjusted contribution amount function _validateAndAdjustCaps( uint64 roundId_, uint amount_, - Round storage round, - AccessCriteriaPrivilages storage privileges, - bool canOverrideTimeAndCap + Round storage round_, + AccessCriteriaPrivileges storage privileges_, + bool canOverrideTimeAndCap_ ) internal view returns (uint adjustedAmount) { adjustedAmount = amount_; - if (!canOverrideTimeAndCap && round.roundCap > 0) { + if (!canOverrideTimeAndCap_ && round_.roundCap > 0) { uint totalRoundContribution = _getTotalRoundContribution(roundId_); - uint effectiveRoundCap = round.roundCap; + uint effectiveRoundCap = round_.roundCap; // If global accumulative caps are enabled, add unused capacity from previous rounds - if (round.globalAccumulativeCaps) { + if (round_.globalAccumulativeCaps) { uint unusedCapacityFromPrevious = 0; - for (uint64 i = 1; i < roundId_; i++) { + for (uint64 i = 1; i < roundId_; ++i) { Round storage prevRound = rounds[i]; if (!prevRound.globalAccumulativeCaps) continue; @@ -642,8 +649,8 @@ contract LM_PC_FundingPot_v1 is // Check and adjust for personal cap uint userPreviousContribution = _getUserContribution(roundId_, msg.sender); - uint userPersonalCap = privileges.personalCap > 0 - ? privileges.personalCap + uint userPersonalCap = privileges_.personalCap > 0 + ? privileges_.personalCap : _getUserPersonalCap(roundId_, msg.sender); if (userPreviousContribution + adjustedAmount > userPersonalCap) { @@ -778,14 +785,14 @@ contract LM_PC_FundingPot_v1 is uint totalUnusedCapacity = 0; bytes32[] memory emptyProof = new bytes32[](0); - for (uint64 i = 1; i < currentRoundId_; i++) { + for (uint64 i = 1; i < currentRoundId_; ++i) { Round storage prevRound = rounds[i]; if (!prevRound.globalAccumulativeCaps) continue; uint personalCap = BASE_PERSONAL_CAP; // Check if there were specific privileges for this user in previous rounds - for (uint8 j = 0; j < 4; j++) { + for (uint8 j = 0; j < 4; ++j) { AccessCriteria storage accessCriteria = prevRound.accessCriterias[j]; (bool isValid,) = _checkAccessCriteriaEligibility( @@ -793,8 +800,8 @@ contract LM_PC_FundingPot_v1 is ); if (isValid) { - AccessCriteriaPrivilages storage privileges = - accessCriteriaPrivilages[i][j]; + AccessCriteriaPrivileges storage privileges = + accessCriteriaPrivileges[i][j]; if (privileges.personalCap > personalCap) { personalCap = privileges.personalCap; } @@ -811,15 +818,16 @@ contract LM_PC_FundingPot_v1 is ///@notice Checks if a sender is in a list of allowed addresses /// @dev Performs a linear search to validate address inclusion - /// @param allowedAddresses Array of addresses permitted to participate - /// @param sender Address to check for permission + /// @param allowedAddresses_ Array of addresses permitted to participate + /// @param sender_ Address to check for permission /// @return Boolean indicating whether the sender is in the allowed list function _checkAllowedAddressList( - address[] memory allowedAddresses, - address sender + address[] memory allowedAddresses_, + address sender_ ) internal pure returns (bool) { - for (uint i = 0; i < allowedAddresses.length; i++) { - if (allowedAddresses[i] == sender) { + uint lengthOfAddresses = allowedAddresses_.length; + for (uint i = 0; i < lengthOfAddresses; ++i) { + if (allowedAddresses_[i] == sender_) { return true; } } diff --git a/src/modules/logicModule/interfaces/ILM_PC_FundingPot_v1.sol b/src/modules/logicModule/interfaces/ILM_PC_FundingPot_v1.sol index f7742ac0c..31f691ef7 100644 --- a/src/modules/logicModule/interfaces/ILM_PC_FundingPot_v1.sol +++ b/src/modules/logicModule/interfaces/ILM_PC_FundingPot_v1.sol @@ -41,7 +41,7 @@ interface ILM_PC_FundingPot_v1 is IERC20PaymentClientBase_v2 { address[] allowedAddresses; // Explicit allowlist } - struct AccessCriteriaPrivilages { + struct AccessCriteriaPrivileges { uint personalCap; bool overrideCap; uint start; @@ -123,6 +123,24 @@ interface ILM_PC_FundingPot_v1 is IERC20PaymentClientBase_v2 { uint64 indexed roundId_, uint8 accessId_, AccessCriteria accessCriteria_ ); + /// @notice Emitted when access criteria Privileges are set for a round. + /// @param roundId_ The unique identifier of the round. + /// @param accessId_ The identifier of the access criteria. + /// @param personalCap_ The personal cap for the access criteria. + /// @param overrideCap_ Whether to override the global cap. + /// @param start_ The start timestamp for the access criteria. + /// @param cliff_ The cliff timestamp for the access criteria. + /// @param end_ The end timestamp for the access criteria. + event AccessCriteriaPrivilegesSet( + uint64 indexed roundId_, + uint8 accessId_, + uint personalCap_, + bool overrideCap_, + uint start_, + uint cliff_, + uint end_ + ); + /// @notice Emitted when a contribution is made to a round /// @param roundId_ The ID of the round /// @param contributor_ The address of the contributor @@ -164,6 +182,11 @@ interface ILM_PC_FundingPot_v1 is IERC20PaymentClientBase_v2 { /// @notice Invalid access criteria ID. error Module__LM_PC_FundingPot__InvalidAccessCriteriaId(); + /// @notice Cannot set Privileges for open access criteria + error Module__LM_PC_FundingPot__CannotSetPrivilegesForOpenAccessCriteria(); + + /// @notice Invalid times + error Module__LM_PC_FundingPot__InvalidTimes(); /// @notice Round has not started yet error Module__LM_PC_FundingPot__RoundHasNotStarted(); @@ -234,7 +257,7 @@ interface ILM_PC_FundingPot_v1 is IERC20PaymentClientBase_v2 { address[] memory allowedAddresses_ ); - /// @notice Retrieves the access criteria privilages for a specific funding round. + /// @notice Retrieves the access criteria Privileges for a specific funding round. /// @param roundId_ The unique identifier of the round. /// @param accessId_ The identifier of the access criteria. /// @return isRoundOpen_ Whether the round is open @@ -243,7 +266,7 @@ interface ILM_PC_FundingPot_v1 is IERC20PaymentClientBase_v2 { /// @return start_ The start timestamp for the access criteria /// @return cliff_ The cliff timestamp for the access criteria /// @return end_ The end timestamp for the access criteria - function getRoundAccessCriteriaPrivilages(uint64 roundId_, uint8 accessId_) + function getRoundAccessCriteriaPrivileges(uint64 roundId_, uint8 accessId_) external view returns ( @@ -331,23 +354,23 @@ interface ILM_PC_FundingPot_v1 is IERC20PaymentClientBase_v2 { AccessCriteria memory accessCriteria_ ) external; - /// @notice Set Access Criteria Privilages + /// @notice Set Access Criteria Privileges /// @dev Only callable by funding pot admin and only before the round has started /// @param roundId_ ID of the round /// @param accessId_ ID of the access criteria /// @param personalCap_ Personal cap for the access criteria /// @param overrideCap_ Whether to override the global cap - /// @param _start Start timestamp for the access criteria - /// @param _cliff Cliff timestamp for the access criteria - /// @param _end End timestamp for the access criteria - function setAccessCriteriaPrivilages( + /// @param start_ Start timestamp for the access criteria + /// @param cliff_ Cliff timestamp for the access criteria + /// @param end_ End timestamp for the access criteria + function setAccessCriteriaPrivileges( uint64 roundId_, uint8 accessId_, uint personalCap_, bool overrideCap_, - uint _start, - uint _cliff, - uint _end + uint start_, + uint cliff_, + uint end_ ) external; /// @notice Allows a user to contribute to a specific funding round. @@ -355,13 +378,11 @@ interface ILM_PC_FundingPot_v1 is IERC20PaymentClientBase_v2 { /// @param roundId_ The unique identifier of the funding round. /// @param amount_ The amount of tokens being contributed. /// @param accessCriteriaId_ The identifier for the access criteria to validate eligibility. - /// @param contributionToken_ The address of the token used for contribution. /// @param merkleProof_ The Merkle proof used to verify the contributor's eligibility. function contributeToRound( uint64 roundId_, uint amount_, uint8 accessCriteriaId_, - address contributionToken_, bytes32[] calldata merkleProof_ ) external; } diff --git a/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol b/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol index 58db38a22..7aa062c24 100644 --- a/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol +++ b/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol @@ -1094,9 +1094,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { fundingPotToken.approve(address(fundingPot), 110); vm.prank(contributor1_); - fundingPot.contributeToRound( - roundId, 10, accessId, address(fundingPotToken), new bytes32[](0) - ); + fundingPot.contributeToRound(roundId, 10, accessId, new bytes32[](0)); vm.expectRevert( abi.encodeWithSelector( @@ -1108,11 +1106,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { vm.prank(contributor1_); fundingPot.contributeToRound( - roundId, - amount, - accessId, - address(fundingPotToken), - new bytes32[](0) + roundId, amount, accessId, new bytes32[](0) ); } @@ -1145,11 +1139,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { vm.prank(contributor1_); fundingPot.contributeToRound( - roundId, - amount, - accessId, - address(fundingPotToken), - new bytes32[](0) + roundId, amount, accessId, new bytes32[](0) ); } @@ -1184,11 +1174,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { vm.prank(contributor1_); fundingPot.contributeToRound( - roundId, - amount, - accessId, - address(fundingPotToken), - new bytes32[](0) + roundId, amount, accessId, new bytes32[](0) ); } @@ -1223,11 +1209,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { vm.prank(contributor1_); fundingPot.contributeToRound( - roundId, - amount, - accessId, - address(fundingPotToken), - new bytes32[](0) + roundId, amount, accessId, new bytes32[](0) ); } @@ -1260,9 +1242,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { ); vm.prank(contributor1_); - fundingPot.contributeToRound( - roundId, amount, accessId, address(fundingPotToken), PROOF - ); + fundingPot.contributeToRound(roundId, amount, accessId, PROOF); } function testFuzzContributeToRound_revertsGivenAllowedListAccessCriteriaIsNotMet( @@ -1295,11 +1275,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { vm.prank(contributor1_); fundingPot.contributeToRound( - roundId, - amount, - accessId, - address(fundingPotToken), - new bytes32[](0) + roundId, amount, accessId, new bytes32[](0) ); } @@ -1327,11 +1303,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { vm.prank(contributor1_); fundingPot.contributeToRound( - roundId, - amount, - accessId, - address(fundingPotToken), - new bytes32[](0) + roundId, amount, accessId, new bytes32[](0) ); // Get the base personal cap from the contract @@ -1349,9 +1321,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { vm.prank(contributor1_); uint remainingCap = personalCap - amount; - fundingPot.contributeToRound( - roundId, 251, accessId, address(fundingPotToken), new bytes32[](0) - ); + fundingPot.contributeToRound(roundId, 251, accessId, new bytes32[](0)); } /* @@ -1420,11 +1390,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { vm.prank(contributor1_); fundingPot.contributeToRound( - roundId, - amount, - accessId, - address(fundingPotToken), - new bytes32[](0) + roundId, amount, accessId, new bytes32[](0) ); uint totalContributions = @@ -1466,11 +1432,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { vm.prank(contributor1_); fundingPot.contributeToRound( - roundId, - amount, - accessId, - address(fundingPotToken), - new bytes32[](0) + roundId, amount, accessId, new bytes32[](0) ); // only the amount that does not exceed the roundcap is contributed @@ -1505,11 +1467,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { // First contribution vm.prank(contributor1_); fundingPot.contributeToRound( - roundId, - firstAmount, - accessId, - address(fundingPotToken), - new bytes32[](0) + roundId, firstAmount, accessId, new bytes32[](0) ); // Get the personal cap @@ -1520,11 +1478,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { vm.prank(contributor1_); fundingPot.contributeToRound( - roundId, - secondAmount, - accessId, - address(fundingPotToken), - new bytes32[](0) + roundId, secondAmount, accessId, new bytes32[](0) ); uint totalContribution = @@ -1546,7 +1500,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { fundingPot.setAccessCriteriaForRound(roundId, accessId, accessCriteria); // Set privileges with override capability - fundingPot.setAccessCriteriaPrivilages( + fundingPot.setAccessCriteriaPrivileges( roundId, accessId, 500, // personalCap @@ -1569,11 +1523,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { // This should succeed despite being after round end, due to override privilege vm.prank(contributor1_); fundingPot.contributeToRound( - roundId, - amount, - accessId, - address(fundingPotToken), - new bytes32[](0) + roundId, amount, accessId, new bytes32[](0) ); // Verify the contribution was recorded @@ -1632,11 +1582,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { vm.warp(roundStart + 1); uint round1Contribution = 200; fundingPot.contributeToRound( - round1Id, - round1Contribution, - accessId, - address(fundingPotToken), - new bytes32[](0) + round1Id, round1Contribution, accessId, new bytes32[](0) ); // Move to Round 2 @@ -1649,11 +1595,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { uint round2MaxContribution = basePersonalCap + unusedCapacityFromRound1; fundingPot.contributeToRound( - round2Id, - round2MaxContribution, - accessId, - address(fundingPotToken), - new bytes32[](0) + round2Id, round2MaxContribution, accessId, new bytes32[](0) ); vm.stopPrank(); @@ -1716,16 +1658,12 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { vm.startPrank(contributor1_); fundingPotToken.approve(address(fundingPot), 300); - fundingPot.contributeToRound( - round1Id, 300, accessId, address(fundingPotToken), new bytes32[](0) - ); + fundingPot.contributeToRound(round1Id, 300, accessId, new bytes32[](0)); vm.stopPrank(); vm.startPrank(contributor2_); fundingPotToken.approve(address(fundingPot), 200); - fundingPot.contributeToRound( - round1Id, 200, accessId, address(fundingPotToken), new bytes32[](0) - ); + fundingPot.contributeToRound(round1Id, 200, accessId, new bytes32[](0)); vm.stopPrank(); // Move to Round 2 @@ -1739,16 +1677,12 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { vm.startPrank(contributor2_); fundingPotToken.approve(address(fundingPot), 400); - fundingPot.contributeToRound( - round2Id, 400, accessId, address(fundingPotToken), new bytes32[](0) - ); + fundingPot.contributeToRound(round2Id, 400, accessId, new bytes32[](0)); vm.stopPrank(); vm.startPrank(contributor3_); fundingPotToken.approve(address(fundingPot), 600); - fundingPot.contributeToRound( - round2Id, 600, accessId, address(fundingPotToken), new bytes32[](0) - ); + fundingPot.contributeToRound(round2Id, 600, accessId, new bytes32[](0)); vm.stopPrank(); // Verify Round 1 contributions From c1dc60e17c9281eb23f72738de5646c3823502fa Mon Sep 17 00:00:00 2001 From: JeffreyJoel Date: Wed, 2 Apr 2025 15:07:28 +0100 Subject: [PATCH 041/130] fix:initialise contributing token properly --- src/modules/logicModule/LM_PC_FundingPot_v1.sol | 1 + test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/modules/logicModule/LM_PC_FundingPot_v1.sol b/src/modules/logicModule/LM_PC_FundingPot_v1.sol index efdf89db6..257ba8bd9 100644 --- a/src/modules/logicModule/LM_PC_FundingPot_v1.sol +++ b/src/modules/logicModule/LM_PC_FundingPot_v1.sol @@ -134,6 +134,7 @@ contract LM_PC_FundingPot_v1 is ) external override(Module_v1) initializer { __Module_init(orchestrator_, metadata_); address fundingPotToken; + (fundingPotToken) = abi.decode(configData_, (address)); // Set the flags for the PaymentOrders (this module uses 3 flags). bytes32 flags; flags |= bytes32(1 << FLAG_START); diff --git a/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol b/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol index 7aa062c24..bd42c0527 100644 --- a/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol +++ b/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol @@ -114,7 +114,9 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { _setUpOrchestrator(fundingPot); // Initiate the Logic Module with the metadata and config data - fundingPot.init(_orchestrator, _METADATA, abi.encode("")); + fundingPot.init( + _orchestrator, _METADATA, abi.encode(address(fundingPotToken)) + ); _authorizer.setIsAuthorized(address(this), true); From ec45c8348e6b0e2c68e8b9cb8e4083871c183a66 Mon Sep 17 00:00:00 2001 From: JeffreyJoel Date: Wed, 2 Apr 2025 23:56:19 +0100 Subject: [PATCH 042/130] feat: implement personal cap based on access criteria --- .../logicModule/LM_PC_FundingPot_v1.sol | 295 ++++++++---------- .../interfaces/ILM_PC_FundingPot_v1.sol | 16 +- .../logicModule/LM_PC_FundingPot_v1.t.sol | 176 +++++++---- .../LM_PC_FundingPot_v1_Exposed.sol | 20 +- 4 files changed, 266 insertions(+), 241 deletions(-) diff --git a/src/modules/logicModule/LM_PC_FundingPot_v1.sol b/src/modules/logicModule/LM_PC_FundingPot_v1.sol index 257ba8bd9..0dc7bbd40 100644 --- a/src/modules/logicModule/LM_PC_FundingPot_v1.sol +++ b/src/modules/logicModule/LM_PC_FundingPot_v1.sol @@ -84,9 +84,6 @@ contract LM_PC_FundingPot_v1 is /// @notice The payment processor flag for the end timestamp. uint8 internal constant FLAG_END = 3; - /// @notice Maximum amount for the base personal cap - uint internal constant BASE_PERSONAL_CAP = 500; - // ------------------------------------------------------------------------- // State @@ -205,7 +202,7 @@ contract LM_PC_FundingPot_v1 is returns ( bool isRoundOpen_, uint personalCap_, - bool overrideCap_, + bool overrideContributionSpan_, uint start_, uint cliff_, uint end_ @@ -225,7 +222,7 @@ contract LM_PC_FundingPot_v1 is return ( false, privs.personalCap, - privs.overrideCap, + privs.overrideContributionSpan, privs.start, privs.cliff, privs.end @@ -381,22 +378,57 @@ contract LM_PC_FundingPot_v1 is uint64 roundId_, uint8 accessId_, uint personalCap_, - bool overrideCap_, + uint capByNFT_, + uint capByMerkle_, + uint capByList_, + bool overrideContributionSpan_, uint start_, uint cliff_, uint end_ ) external onlyModuleRole(FUNDING_POT_ADMIN_ROLE) { Round storage round = rounds[roundId_]; + uint highestCap = 0; + _validateEditRoundParameters(round); if ( round.accessCriterias[accessId_].accessCriteriaId == AccessCriteriaId.OPEN ) { - revert - Module__LM_PC_FundingPot__CannotSetPrivilegesForOpenAccessCriteria(); + highestCap = personalCap_; + } + + if ( + round.accessCriterias[accessId_].accessCriteriaId + == AccessCriteriaId.NFT && capByNFT_ > 0 + ) { + uint nftCap = personalCap_ + capByNFT_; + if (nftCap > highestCap) { + highestCap = nftCap; + } + } + + if ( + round.accessCriterias[accessId_].accessCriteriaId + == AccessCriteriaId.MERKLE && capByMerkle_ > 0 + ) { + uint merkleCap = personalCap_ + capByMerkle_; + if (merkleCap > highestCap) { + highestCap = merkleCap; + } } + + if ( + round.accessCriterias[accessId_].accessCriteriaId + == AccessCriteriaId.LIST && capByList_ > 0 + ) { + uint listCap = personalCap_ + capByList_; + if (listCap > highestCap) { + highestCap = listCap; + } + } + if (!_validTimes(start_, cliff_, end_)) { revert Module__LM_PC_FundingPot__InvalidTimes(); } @@ -405,7 +437,8 @@ contract LM_PC_FundingPot_v1 is accessCriteriaPrivileges[roundId_][accessId_]; accessCriteriaPrivileges.personalCap = personalCap_; - accessCriteriaPrivileges.overrideCap = overrideCap_; + accessCriteriaPrivileges.overrideContributionSpan = + overrideContributionSpan_; accessCriteriaPrivileges.start = start_; accessCriteriaPrivileges.cliff = cliff_; accessCriteriaPrivileges.end = end_; @@ -414,7 +447,7 @@ contract LM_PC_FundingPot_v1 is roundId_, accessId_, personalCap_, - overrideCap_, + overrideContributionSpan_, start_, cliff_, end_ @@ -428,20 +461,10 @@ contract LM_PC_FundingPot_v1 is uint8 accessCriteriaId_, bytes32[] calldata merkleProof_ ) external { - // Validate input amount. - if (amount_ == 0) { - revert Module__LM_PC_FundingPot__InvalidDepositAmount(); - } - - // Validate round and access criteria - Round storage round = _validateRoundAndAccessCriteria( - roundId_, accessCriteriaId_, merkleProof_ + uint adjustedAmount = _validateRoundContribution( + roundId_, accessCriteriaId_, merkleProof_, amount_ ); - // Check timing and caps based on privileges - (uint adjustedAmount, bool canOverrideTimeAndCap) = - _validateTimingAndCaps(roundId_, accessCriteriaId_, amount_, round); - _recordContribution(roundId_, msg.sender, adjustedAmount); IERC20(contributionToken).safeTransferFrom( @@ -519,108 +542,80 @@ contract LM_PC_FundingPot_v1 is /// @notice Validates the round existence and access criteria /// @param roundId_ ID of the round to validate - /// @param accessId_ ID of the access criteria to check + /// @param accessCriteriaId_ ID of the access criteria to check /// @param merkleProof_ Merkle proof for validation if needed - /// @return round The round storage object - function _validateRoundAndAccessCriteria( + /// @param amount_ The amount sent by the user + /// @return adjustedAmount The potentially adjusted contribution amount based on personal and round caps + function _validateRoundContribution( uint64 roundId_, - uint8 accessId_, - bytes32[] calldata merkleProof_ - ) internal view returns (Round storage round) { - round = rounds[roundId_]; + uint8 accessCriteriaId_, + bytes32[] calldata merkleProof_, + uint amount_ + ) internal view returns (uint adjustedAmount) { + Round storage round = rounds[roundId_]; + uint currentTime = block.timestamp; + uint adjustedAmount = amount_; + if (amount_ == 0) { + revert Module__LM_PC_FundingPot__InvalidDepositAmount(); + } // Validate round exists. if (round.roundEnd == 0 && round.roundCap == 0) { revert Module__LM_PC_FundingPot__RoundNotCreated(); } - // Validate access criteria. - _validateAccessCriteria(roundId_, accessId_, merkleProof_); - - return round; - } + // Validate contribution timing + if (currentTime < round.roundStart) { + revert Module__LM_PC_FundingPot__RoundHasNotStarted(); + } - /// @notice Validates timing and cap constraints, adjusts amount if needed - /// @param roundId_ ID of the round - /// @param accessId_ ID of the access criteria - /// @param amount_ Requested contribution amount - /// @param round_ Round storage object - /// @return adjustedAmount The potentially adjusted contribution amount - /// @return canOverrideTimeAndCap Whether the user can override time and cap constraints - function _validateTimingAndCaps( - uint64 roundId_, - uint8 accessId_, - uint amount_, - Round storage round_ - ) internal view returns (uint adjustedAmount, bool canOverrideTimeAndCap) { - adjustedAmount = amount_; + _validateAccessCriteria(roundId_, accessCriteriaId_, merkleProof_); - // Get access criteria privileges AccessCriteriaPrivileges storage privileges = - accessCriteriaPrivileges[roundId_][accessId_]; + accessCriteriaPrivileges[roundId_][accessCriteriaId_]; - canOverrideTimeAndCap = privileges.overrideCap; + bool canOverrideContributionSpan = privileges.overrideContributionSpan; - _validateTiming(round_, privileges, canOverrideTimeAndCap); + // Allow contributions after the round end if the user can override the contribution span + if ( + round.roundEnd > 0 && currentTime > round.roundEnd + && !canOverrideContributionSpan + ) { + revert Module__LM_PC_FundingPot__RoundHasEnded(); + } - // Handle cap validation and amount adjustment adjustedAmount = _validateAndAdjustCaps( - roundId_, amount_, round_, privileges, canOverrideTimeAndCap + roundId_, + amount_, + round, + accessCriteriaId_, + canOverrideContributionSpan ); - return (adjustedAmount, canOverrideTimeAndCap); - } - - /// @notice Validates timing constraints based on privileges - /// @param round_ Round storage object - /// @param privileges_ Access criteria privileges - /// @param canOverrideTimeAndCap_ Whether the user can override time constraints - function _validateTiming( - Round storage round_, - AccessCriteriaPrivileges storage privileges_, - bool canOverrideTimeAndCap_ - ) internal view { - if (canOverrideTimeAndCap_) { - return; - } - - uint currentTime = block.timestamp; - - // Check custom timing for this access level if defined - uint effectiveStart = - privileges_.start > 0 ? privileges_.start : round_.roundStart; - uint effectiveEnd = - privileges_.end > 0 ? privileges_.end : round_.roundEnd; - - if (currentTime < effectiveStart) { - revert Module__LM_PC_FundingPot__RoundHasNotStarted(); - } - if (effectiveEnd > 0 && currentTime > effectiveEnd) { - revert Module__LM_PC_FundingPot__RoundHasEnded(); - } + return adjustedAmount; } /// @notice Validates cap constraints and adjusts amount if needed /// @param roundId_ ID of the round /// @param amount_ Requested contribution amount /// @param round_ Round storage object - /// @param privileges_ Access criteria privileges - /// @param canOverrideTimeAndCap_ Whether the user can override cap constraints + /// @param canOverrideContributionSpan_ Whether the user can override cap constraints /// @return adjustedAmount The potentially adjusted contribution amount function _validateAndAdjustCaps( uint64 roundId_, uint amount_, Round storage round_, - AccessCriteriaPrivileges storage privileges_, - bool canOverrideTimeAndCap_ + uint8 accessId_, + bool canOverrideContributionSpan_ ) internal view returns (uint adjustedAmount) { adjustedAmount = amount_; - if (!canOverrideTimeAndCap_ && round_.roundCap > 0) { + if (!canOverrideContributionSpan_ && round_.roundCap > 0) { uint totalRoundContribution = _getTotalRoundContribution(roundId_); uint effectiveRoundCap = round_.roundCap; - // If global accumulative caps are enabled, add unused capacity from previous rounds + // If global accumulative caps are enabled, + // adjust the round cap to acommodate unused capacity from previous rounds if (round_.globalAccumulativeCaps) { uint unusedCapacityFromPrevious = 0; for (uint64 i = 1; i < roundId_; ++i) { @@ -640,7 +635,7 @@ contract LM_PC_FundingPot_v1 is revert Module__LM_PC_FundingPot__RoundCapReached(); } - // Adjust for remaining round cap + // Allow the user to contribute up to the remaining round cap uint remainingRoundCap = effectiveRoundCap - totalRoundContribution; if (adjustedAmount > remainingRoundCap) { adjustedAmount = remainingRoundCap; @@ -649,10 +644,9 @@ contract LM_PC_FundingPot_v1 is // Check and adjust for personal cap uint userPreviousContribution = - _getUserContribution(roundId_, msg.sender); - uint userPersonalCap = privileges_.personalCap > 0 - ? privileges_.personalCap - : _getUserPersonalCap(roundId_, msg.sender); + _getUserContributionToRound(roundId_, msg.sender); + uint userPersonalCap = + _getUserPersonalCapForRound(roundId_, accessId_, msg.sender); if (userPreviousContribution + adjustedAmount > userPersonalCap) { if (userPreviousContribution < userPersonalCap) { @@ -665,37 +659,6 @@ contract LM_PC_FundingPot_v1 is return adjustedAmount; } - /// @notice Helper function to check if a user meets specific access criteria - /// @param accessCriteria_ The access criteria to check against - /// @param user_ The user address to validate - /// @param merkleProof_ Optional merkle proof for merkle-based validation - /// @return (bool, AccessCriteriaId) Returns success and the access criteria type used - function _checkAccessCriteriaEligibility( - AccessCriteria storage accessCriteria_, - address user_, - bytes32[] memory merkleProof_ - ) internal view returns (bool, AccessCriteriaId) { - AccessCriteriaId criteriaId = accessCriteria_.accessCriteriaId; - - if (criteriaId == AccessCriteriaId.OPEN) { - return (true, criteriaId); - } - bool isValid; - if (criteriaId == AccessCriteriaId.NFT) { - isValid = _checkNftOwnership(accessCriteria_.nftContract, user_); - } else if (criteriaId == AccessCriteriaId.MERKLE) { - isValid = _validateMerkleProof( - accessCriteria_.merkleRoot, user_, merkleProof_ - ); - } else if (criteriaId == AccessCriteriaId.LIST) { - isValid = _checkAllowedAddressList( - accessCriteria_.allowedAddresses, user_ - ); - } - - return (isValid, criteriaId); - } - /// @notice Validates access criteria for a specific round and access type /// @dev Checks if a user meets the access requirements based on the round's access criteria /// @param roundId_ The ID of the round being validated @@ -713,19 +676,18 @@ contract LM_PC_FundingPot_v1 is return; } - (bool isValid, AccessCriteriaId criteriaId) = - _checkAccessCriteriaEligibility( - accessCriteria, msg.sender, merkleProof_ - ); - - if (!isValid) { - if (criteriaId == AccessCriteriaId.NFT) { - revert Module__LM_PC_FundingPot__AccessCriteriaNftFailed(); - } else if (criteriaId == AccessCriteriaId.MERKLE) { - revert Module__LM_PC_FundingPot__AccessCriteriaMerkleFailed(); - } else if (criteriaId == AccessCriteriaId.LIST) { - revert Module__LM_PC_FundingPot__AccessCriteriaListFailed(); - } + bool accessGranted = false; + if (accessCriteria.accessCriteriaId == AccessCriteriaId.NFT) { + accessGranted = + _checkNftOwnership(accessCriteria.nftContract, msg.sender); + } else if (accessCriteria.accessCriteriaId == AccessCriteriaId.MERKLE) { + accessGranted = _validateMerkleProof( + accessCriteria.merkleRoot, merkleProof_, msg.sender, roundId_ + ); + } else if (accessCriteria.accessCriteriaId == AccessCriteriaId.LIST) { + accessGranted = _checkAllowedAddressList( + accessCriteria.allowedAddresses, msg.sender + ); } } @@ -746,7 +708,7 @@ contract LM_PC_FundingPot_v1 is /// @param roundId_ The ID of the round to check contributions for /// @param user_ The address of the user /// @return The user's contribution amount for the specified round - function _getUserContribution(uint64 roundId_, address user_) + function _getUserContributionToRound(uint64 roundId_, address user_) internal view returns (uint) @@ -759,19 +721,23 @@ contract LM_PC_FundingPot_v1 is /// @param roundId_ The ID of the current round /// @param user_ The address of the user /// @return The personal contribution cap for the user - function _getUserPersonalCap(uint64 roundId_, address user_) - internal - view - returns (uint) - { - Round storage round = rounds[roundId_]; + function _getUserPersonalCapForRound( + uint64 roundId_, + uint8 accessId_, + address user_ + ) internal view returns (uint) { + AccessCriteriaPrivileges storage privileges = + accessCriteriaPrivileges[roundId_][accessId_]; + + uint personalCap = privileges.personalCap; + Round storage round = rounds[roundId_]; if (round.globalAccumulativeCaps) { uint unusedCapacity = - _getUnusedCapacityFromPreviousRounds(user_, roundId_); - return BASE_PERSONAL_CAP + unusedCapacity; + _getUserUnusedCapacityFromPreviousRounds(user_, roundId_); + return personalCap + unusedCapacity; } - return BASE_PERSONAL_CAP; + return personalCap; } /// @notice Calculates unused contribution capacity from previous rounds @@ -779,37 +745,32 @@ contract LM_PC_FundingPot_v1 is /// @param user_ The address of the user /// @param currentRoundId_ The ID of the current round /// @return Total unused contribution capacity from previous rounds - function _getUnusedCapacityFromPreviousRounds( + function _getUserUnusedCapacityFromPreviousRounds( address user_, uint64 currentRoundId_ ) internal view returns (uint) { uint totalUnusedCapacity = 0; - bytes32[] memory emptyProof = new bytes32[](0); for (uint64 i = 1; i < currentRoundId_; ++i) { Round storage prevRound = rounds[i]; if (!prevRound.globalAccumulativeCaps) continue; - uint personalCap = BASE_PERSONAL_CAP; + uint personalCap = 0; - // Check if there were specific privileges for this user in previous rounds for (uint8 j = 0; j < 4; ++j) { AccessCriteria storage accessCriteria = prevRound.accessCriterias[j]; - (bool isValid,) = _checkAccessCriteriaEligibility( - accessCriteria, user_, emptyProof - ); - - if (isValid) { - AccessCriteriaPrivileges storage privileges = - accessCriteriaPrivileges[i][j]; - if (privileges.personalCap > personalCap) { - personalCap = privileges.personalCap; - } + + AccessCriteriaPrivileges storage privileges = + accessCriteriaPrivileges[i][j]; + + // return only the highest personal cap from the selected access criteria + if (privileges.personalCap > personalCap) { + personalCap = privileges.personalCap; } } - uint userContribution = _getUserContribution(i, user_); + uint userContribution = _getUserContributionToRound(i, user_); if (userContribution < personalCap) { totalUnusedCapacity += (personalCap - userContribution); } @@ -863,14 +824,16 @@ contract LM_PC_FundingPot_v1 is /// @dev Validates that the user's address is part of the Merkle tree /// @param root_ The Merkle root to validate against /// @param user_ The address of the user to check + /// @param roundId_ The ID of the round to check /// @param merkleProof_ The Merkle proof to verify /// @return Boolean indicating whether the proof is valid function _validateMerkleProof( bytes32 root_, + bytes32[] memory merkleProof_, address user_, - bytes32[] memory merkleProof_ + uint64 roundId_ ) internal pure returns (bool) { - bytes32 leaf = keccak256(abi.encodePacked(user_)); + bytes32 leaf = keccak256(abi.encodePacked(user_, roundId_)); if (!MerkleProof.verify(merkleProof_, root_, leaf)) { revert Module__LM_PC_FundingPot__AccessCriteriaMerkleFailed(); diff --git a/src/modules/logicModule/interfaces/ILM_PC_FundingPot_v1.sol b/src/modules/logicModule/interfaces/ILM_PC_FundingPot_v1.sol index 31f691ef7..0f52c4d6c 100644 --- a/src/modules/logicModule/interfaces/ILM_PC_FundingPot_v1.sol +++ b/src/modules/logicModule/interfaces/ILM_PC_FundingPot_v1.sol @@ -43,7 +43,7 @@ interface ILM_PC_FundingPot_v1 is IERC20PaymentClientBase_v2 { struct AccessCriteriaPrivileges { uint personalCap; - bool overrideCap; + bool overrideContributionSpan; uint start; uint cliff; uint end; @@ -262,7 +262,7 @@ interface ILM_PC_FundingPot_v1 is IERC20PaymentClientBase_v2 { /// @param accessId_ The identifier of the access criteria. /// @return isRoundOpen_ Whether the round is open /// @return personalCap_ The personal cap for the access criteria - /// @return overrideCap_ Whether to override the global cap + /// @return overrideContributionSpan_ Whether to override the round contribution span /// @return start_ The start timestamp for the access criteria /// @return cliff_ The cliff timestamp for the access criteria /// @return end_ The end timestamp for the access criteria @@ -272,7 +272,7 @@ interface ILM_PC_FundingPot_v1 is IERC20PaymentClientBase_v2 { returns ( bool isRoundOpen_, uint personalCap_, - bool overrideCap_, + bool overrideContributionSpan_, uint start_, uint cliff_, uint end_ @@ -359,7 +359,10 @@ interface ILM_PC_FundingPot_v1 is IERC20PaymentClientBase_v2 { /// @param roundId_ ID of the round /// @param accessId_ ID of the access criteria /// @param personalCap_ Personal cap for the access criteria - /// @param overrideCap_ Whether to override the global cap + /// @param capByNFT_ Cap by for the NFT access criteria + /// @param capByMerkle_ Cap for the Merkle root access criteria + /// @param capByList_ Cap by for the List access criteria + /// @param overrideContributionSpan_ Whether to override the round contribution span /// @param start_ Start timestamp for the access criteria /// @param cliff_ Cliff timestamp for the access criteria /// @param end_ End timestamp for the access criteria @@ -367,7 +370,10 @@ interface ILM_PC_FundingPot_v1 is IERC20PaymentClientBase_v2 { uint64 roundId_, uint8 accessId_, uint personalCap_, - bool overrideCap_, + uint capByNFT_, + uint capByMerkle_, + uint capByList_, + bool overrideContributionSpan_, uint start_, uint cliff_, uint end_ diff --git a/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol b/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol index bd42c0527..fe0bd1a6c 100644 --- a/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol +++ b/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol @@ -1004,7 +1004,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { ├── Then it should not revert └── Then the access criteria should be updated */ - function testFuzzContributeToRound_revertsGivenRoundContributionCapReached( + function testContributeToRound_revertsGivenRoundContributionCapReached( uint roundCap_ ) public { testCreateRound(10); @@ -1112,8 +1112,9 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { ); } - function testFuzzContributeToRound_revertsGivenContributionIsBeforeRoundStart( - ) public { + function testContributeToRound_revertsGivenContributionIsBeforeRoundStart() + public + { testCreateRound(1000); uint64 roundId = fundingPot.getRoundCount(); @@ -1121,9 +1122,12 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { uint amount = 250; ILM_PC_FundingPot_v1.AccessCriteria memory accessCriteria = - _helper_createAccessCriteria(0); + _helper_createAccessCriteria(accessId); fundingPot.setAccessCriteriaForRound(roundId, accessId, accessCriteria); + _helper_callSetAccessCriteriaPrivileges( + roundId, accessId, 500, 0, 0, 0, false, 0, 0, 0 + ); (uint roundStart,,,,,,) = fundingPot.getRoundGenericParameters(roundId); @@ -1145,19 +1149,22 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { ); } - function testFuzzContributeToRound_revertsGivenContributionIsAfterRoundEnd() + function testContributeToRound_revertsGivenContributionIsAfterRoundEnd() public { testCreateRound(1000); uint64 roundId = fundingPot.getRoundCount(); - uint8 accessId = 1; + uint8 accessId = 0; uint amount = 250; ILM_PC_FundingPot_v1.AccessCriteria memory accessCriteria = - _helper_createAccessCriteria(0); + _helper_createAccessCriteria(accessId); fundingPot.setAccessCriteriaForRound(roundId, accessId, accessCriteria); + _helper_callSetAccessCriteriaPrivileges( + roundId, accessId, 500, 0, 0, 0, false, 0, 0, 0 + ); (uint roundStart,,,,,,) = fundingPot.getRoundGenericParameters(roundId); vm.warp(roundStart + 10 days); @@ -1180,7 +1187,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { ); } - function testFuzzContributeToRound_revertsGivenNFTAccessCriteriaIsNotMet() + function testContributeToRound_revertsGivenNFTAccessCriteriaIsNotMet() public { testCreateRound(1000); @@ -1190,9 +1197,12 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { uint amount = 250; ILM_PC_FundingPot_v1.AccessCriteria memory accessCriteria = - _helper_createAccessCriteria(1); + _helper_createAccessCriteria(accessId); fundingPot.setAccessCriteriaForRound(roundId, accessId, accessCriteria); + _helper_callSetAccessCriteriaPrivileges( + roundId, accessId, 500, 10, 0, 0, false, 0, 0, 0 + ); (uint roundStart,,,,,,) = fundingPot.getRoundGenericParameters(roundId); vm.warp(roundStart + 1); @@ -1215,18 +1225,21 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { ); } - function testFuzzContributeToRound_revertsGivenMerkleRootAccessCriteriaIsNotMet( + function testContributeToRound_revertsGivenMerkleRootAccessCriteriaIsNotMet( ) public { testCreateRound(1000); uint64 roundId = fundingPot.getRoundCount(); - uint8 accessId = 1; + uint8 accessId = 2; uint amount = 250; ILM_PC_FundingPot_v1.AccessCriteria memory accessCriteria = - _helper_createAccessCriteria(2); + _helper_createAccessCriteria(accessId); fundingPot.setAccessCriteriaForRound(roundId, accessId, accessCriteria); + _helper_callSetAccessCriteriaPrivileges( + roundId, accessId, 500, 0, 0, 0, false, 0, 0, 0 + ); (uint roundStart,,,,,,) = fundingPot.getRoundGenericParameters(roundId); vm.warp(roundStart + 1); @@ -1247,18 +1260,21 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { fundingPot.contributeToRound(roundId, amount, accessId, PROOF); } - function testFuzzContributeToRound_revertsGivenAllowedListAccessCriteriaIsNotMet( + function testontributeToRound_revertsGivenAllowedListAccessCriteriaIsNotMet( ) public { testCreateRound(1000); uint64 roundId = fundingPot.getRoundCount(); - uint8 accessId = 1; + uint8 accessId = 3; uint amount = 250; ILM_PC_FundingPot_v1.AccessCriteria memory accessCriteria = - _helper_createAccessCriteria(3); + _helper_createAccessCriteria(accessId); fundingPot.setAccessCriteriaForRound(roundId, accessId, accessCriteria); + _helper_callSetAccessCriteriaPrivileges( + roundId, accessId, 500, 0, 0, 0, false, 0, 0, 0 + ); (uint roundStart,,,,,,) = fundingPot.getRoundGenericParameters(roundId); vm.warp(roundStart + 1); @@ -1281,7 +1297,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { ); } - function testFuzzContributeToRound_revertsGivenPreviousContributionExceedsPersonalCap( + function testContributeToRound_revertsGivenPreviousContributionExceedsPersonalCap( ) public { testCreateRound(1000); @@ -1290,9 +1306,12 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { uint amount = 500; ILM_PC_FundingPot_v1.AccessCriteria memory accessCriteria = - _helper_createAccessCriteria(1); + _helper_createAccessCriteria(accessId); fundingPot.setAccessCriteriaForRound(roundId, accessId, accessCriteria); + _helper_callSetAccessCriteriaPrivileges( + roundId, accessId, 500, 0, 0, 0, false, 0, 0, 0 + ); mockNFTContract.mint(contributor1_); @@ -1309,8 +1328,9 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { ); // Get the base personal cap from the contract - uint personalCap = - fundingPot.exposed_getUserPersonalCap(roundId, contributor1_); + uint personalCap = fundingPot.exposed_getUserPersonalCapForRound( + roundId, accessId, contributor1_ + ); // Attempt to contribute beyond personal cap vm.expectRevert( @@ -1369,7 +1389,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { │ And the funds are transferred into the funding pot │ */ - function testFuzzContributeToRound_worksGivenAllConditionsMet() public { + function testContributeToRound_worksGivenAllConditionsMet() public { testCreateRound(1000); uint64 roundId = fundingPot.getRoundCount(); @@ -1377,10 +1397,12 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { uint amount = 250; ILM_PC_FundingPot_v1.AccessCriteria memory accessCriteria = - _helper_createAccessCriteria(1); + _helper_createAccessCriteria(accessId); fundingPot.setAccessCriteriaForRound(roundId, accessId, accessCriteria); - + _helper_callSetAccessCriteriaPrivileges( + roundId, accessId, 500, 100, 0, 0, false, 0, 0, 0 + ); mockNFTContract.mint(contributor1_); (uint roundStart,,,,,,) = fundingPot.getRoundGenericParameters(roundId); @@ -1400,12 +1422,12 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { assertEq(totalContributions, amount); - uint personalContributions = - fundingPot.exposed_getUserContribution(roundId, contributor1_); + uint personalContributions = fundingPot + .exposed_getUserContributionToRound(roundId, contributor1_); assertEq(personalContributions, amount); } - function testFuzzContributeToRound_worksGivenUserCurrentContributionExceedsTheRoundCap( + function testContributeToRound_worksGivenUserCurrentContributionExceedsTheRoundCap( uint roundCap_ ) public { vm.assume(accessCriteriaEnumOld >= 0 && accessCriteriaEnumOld <= 4); @@ -1416,13 +1438,16 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { _helper_setupRoundWithAccessCriteria(accessCriteriaEnumOld); uint64 roundId = fundingPot.getRoundCount(); - uint8 accessCriteriaId = 0; + uint8 accessId = 0; + uint amount = 201; - // Create and apply new access criteria - ILM_PC_FundingPot_v1.AccessCriteria memory newAccessCriteria = - _helper_createAccessCriteria(accessCriteriaEnumNew); + ILM_PC_FundingPot_v1.AccessCriteria memory accessCriteria = + _helper_createAccessCriteria(accessId); fundingPot.setAccessCriteriaForRound(roundId, accessId, accessCriteria); + _helper_callSetAccessCriteriaPrivileges( + roundId, accessId, 500, 0, 0, 0, false, 0, 0, 0 + ); (uint roundStart,, uint roundCap,,,,) = fundingPot.getRoundGenericParameters(roundId); @@ -1439,11 +1464,14 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { // only the amount that does not exceed the roundcap is contributed assertEq( - fundingPot.exposed_getUserContribution(roundId, contributor1_), 200 + fundingPot.exposed_getUserContributionToRound( + roundId, contributor1_ + ), + 200 ); } - function testFuzzContributeToRound_worksGivenContributionPartiallyExceedingPersonalCap( + function testContributeToRound_worksGivenContributionPartiallyExceedingPersonalCap( ) public { testCreateRound(1000); @@ -1453,9 +1481,12 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { uint firstAmount = 400; ILM_PC_FundingPot_v1.AccessCriteria memory accessCriteria = - _helper_createAccessCriteria(1); + _helper_createAccessCriteria(accessId); fundingPot.setAccessCriteriaForRound(roundId, accessId, accessCriteria); + _helper_callSetAccessCriteriaPrivileges( + roundId, accessId, 500, 0, 0, 0, false, 0, 0, 0 + ); mockNFTContract.mint(contributor1_); @@ -1473,8 +1504,9 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { ); // Get the personal cap - uint personalCap = - fundingPot.exposed_getUserPersonalCap(roundId, contributor1_); + uint personalCap = fundingPot.exposed_getUserPersonalCapForRound( + roundId, accessId, contributor1_ + ); uint secondAmount = 200; @@ -1483,13 +1515,15 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { roundId, secondAmount, accessId, new bytes32[](0) ); - uint totalContribution = - fundingPot.exposed_getUserContribution(roundId, contributor1_); + uint totalContribution = fundingPot.exposed_getUserContributionToRound( + roundId, contributor1_ + ); assertEq(totalContribution, personalCap); } - function testFuzzContributeToRound_worksGivenUserCanOverrideTimeConstraints( - ) public { + function testContributeToRound_worksGivenUserCanOverrideTimeConstraints() + public + { testCreateRound(1000); uint64 roundId = fundingPot.getRoundCount(); @@ -1497,19 +1531,13 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { uint amount = 250; ILM_PC_FundingPot_v1.AccessCriteria memory accessCriteria = - _helper_createAccessCriteria(1); + _helper_createAccessCriteria(accessId); fundingPot.setAccessCriteriaForRound(roundId, accessId, accessCriteria); // Set privileges with override capability - fundingPot.setAccessCriteriaPrivileges( - roundId, - accessId, - 500, // personalCap - true, // overrideCap (can override time constraints) - 0, // start - 0, // cliff - 0 // end + _helper_callSetAccessCriteriaPrivileges( + roundId, accessId, 500, 200, 0, 0, true, 0, 0, 0 ); mockNFTContract.mint(contributor1_); @@ -1562,6 +1590,9 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { ILM_PC_FundingPot_v1.AccessCriteria memory accessCriteria = _helper_createAccessCriteria(1); fundingPot.setAccessCriteriaForRound(round1Id, accessId, accessCriteria); + _helper_callSetAccessCriteriaPrivileges( + round1Id, accessId, 500, 0, 0, 0, false, 0, 0, 0 + ); mockNFTContract.mint(contributor1_); // Create Round 2 @@ -1576,6 +1607,9 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { ); uint64 round2Id = fundingPot.getRoundCount(); fundingPot.setAccessCriteriaForRound(round2Id, accessId, accessCriteria); + _helper_callSetAccessCriteriaPrivileges( + round2Id, accessId, 500, 0, 0, 0, false, 0, 0, 0 + ); vm.startPrank(contributor1_); fundingPotToken.approve(address(fundingPot), 1500); @@ -1591,23 +1625,26 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { vm.warp(roundStart + 3 days + 1); // Calculate unused capacity from Round 1 - uint basePersonalCap = - fundingPot.exposed_getUserPersonalCap(round1Id, contributor1_); - uint unusedCapacityFromRound1 = basePersonalCap - round1Contribution; + uint personalCap = fundingPot.exposed_getUserPersonalCapForRound( + round1Id, accessId, contributor1_ + ); - uint round2MaxContribution = basePersonalCap + unusedCapacityFromRound1; fundingPot.contributeToRound( - round2Id, round2MaxContribution, accessId, new bytes32[](0) + round2Id, personalCap, accessId, new bytes32[](0) ); vm.stopPrank(); assertEq( - fundingPot.exposed_getUserContribution(round1Id, contributor1_), + fundingPot.exposed_getUserContributionToRound( + round1Id, contributor1_ + ), round1Contribution ); assertEq( - fundingPot.exposed_getUserContribution(round2Id, contributor1_), - round2MaxContribution + fundingPot.exposed_getUserContributionToRound( + round2Id, contributor1_ + ), + personalCap ); } @@ -1639,8 +1676,11 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { uint8 accessId = 0; ILM_PC_FundingPot_v1.AccessCriteria memory accessCriteria = - _helper_createAccessCriteria(0); + _helper_createAccessCriteria(accessId); fundingPot.setAccessCriteriaForRound(round1Id, accessId, accessCriteria); + _helper_callSetAccessCriteriaPrivileges( + round1Id, accessId, 500, 0, 0, 0, false, 0, 0, 0 + ); // Round 2 with a different cap uint round2Cap = 500; @@ -1655,6 +1695,10 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { ); uint64 round2Id = fundingPot.getRoundCount(); fundingPot.setAccessCriteriaForRound(round2Id, accessId, accessCriteria); + _helper_callSetAccessCriteriaPrivileges( + round2Id, accessId, 500, 0, 0, 0, false, 0, 0, 0 + ); + // Round 1: Multiple users contribute, but don't reach the cap vm.warp(roundStart + 1); @@ -1690,19 +1734,31 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { // Verify Round 1 contributions assertEq(fundingPot.exposed_getTotalRoundContributions(round1Id), 500); assertEq( - fundingPot.exposed_getUserContribution(round1Id, contributor1_), 300 + fundingPot.exposed_getUserContributionToRound( + round1Id, contributor1_ + ), + 300 ); assertEq( - fundingPot.exposed_getUserContribution(round1Id, contributor2_), 200 + fundingPot.exposed_getUserContributionToRound( + round1Id, contributor2_ + ), + 200 ); // Verify Round 2 contributions assertEq(fundingPot.exposed_getTotalRoundContributions(round2Id), 1000); assertEq( - fundingPot.exposed_getUserContribution(round2Id, contributor2_), 400 + fundingPot.exposed_getUserContributionToRound( + round2Id, contributor2_ + ), + 400 ); assertEq( - fundingPot.exposed_getUserContribution(round2Id, contributor3_), 600 + fundingPot.exposed_getUserContributionToRound( + round2Id, contributor3_ + ), + 600 ); assertEq( diff --git a/test/unit/modules/logicModule/LM_PC_FundingPot_v1_Exposed.sol b/test/unit/modules/logicModule/LM_PC_FundingPot_v1_Exposed.sol index 4b939ab46..af58a2252 100644 --- a/test/unit/modules/logicModule/LM_PC_FundingPot_v1_Exposed.sol +++ b/test/unit/modules/logicModule/LM_PC_FundingPot_v1_Exposed.sol @@ -18,32 +18,32 @@ contract LM_PC_FundingPot_v1_Exposed is LM_PC_FundingPot_v1 { return _getTotalRoundContribution(roundId_); } - function exposed_getUserContribution(uint64 roundId_, address user_) + function exposed_getUserContributionToRound(uint64 roundId_, address user_) external view returns (uint) { - return _getUserContribution(roundId_, user_); + return _getUserContributionToRound(roundId_, user_); } /** * @notice Exposes the internal _getUserPersonalCap function for testing */ - function exposed_getUserPersonalCap(uint64 roundId_, address user_) - external - view - returns (uint) - { - return _getUserPersonalCap(roundId_, user_); + function exposed_getUserPersonalCapForRound( + uint64 roundId_, + uint8 accessId_, + address user_ + ) external view returns (uint) { + return _getUserPersonalCapForRound(roundId_, accessId_, user_); } /** * @notice Exposes the internal _getUnusedCapacityFromPreviousRounds function for testing */ - function exposed_getUnusedCapacityFromPreviousRounds( + function exposed_getUserUnusedCapacityFromPreviousRounds( address user_, uint64 currentRoundId_ ) external view returns (uint) { - return _getUnusedCapacityFromPreviousRounds(user_, currentRoundId_); + return _getUserUnusedCapacityFromPreviousRounds(user_, currentRoundId_); } } From e772013b26ad9c2f50c7a65181fbfe3035968bfe Mon Sep 17 00:00:00 2001 From: JeffreyJoel Date: Fri, 4 Apr 2025 12:17:48 +0100 Subject: [PATCH 043/130] add exposed functions --- .../logicModule/LM_PC_FundingPot_v1.t.sol | 3 + .../LM_PC_FundingPot_v1_Exposed.sol | 119 +++++++++++++++++- 2 files changed, 120 insertions(+), 2 deletions(-) diff --git a/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol b/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol index fe0bd1a6c..ef7e77be7 100644 --- a/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol +++ b/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol @@ -1767,6 +1767,9 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { ); } + // ------------------------------------------------------------------------- + // Internal Functions + // ------------------------------------------------------------------------- // Helper Functions diff --git a/test/unit/modules/logicModule/LM_PC_FundingPot_v1_Exposed.sol b/test/unit/modules/logicModule/LM_PC_FundingPot_v1_Exposed.sol index af58a2252..a708a70e6 100644 --- a/test/unit/modules/logicModule/LM_PC_FundingPot_v1_Exposed.sol +++ b/test/unit/modules/logicModule/LM_PC_FundingPot_v1_Exposed.sol @@ -27,7 +27,7 @@ contract LM_PC_FundingPot_v1_Exposed is LM_PC_FundingPot_v1 { } /** - * @notice Exposes the internal _getUserPersonalCap function for testing + * @notice Exposes the internal _getUserPersonalCapForRound function for testing */ function exposed_getUserPersonalCapForRound( uint64 roundId_, @@ -38,7 +38,7 @@ contract LM_PC_FundingPot_v1_Exposed is LM_PC_FundingPot_v1 { } /** - * @notice Exposes the internal _getUnusedCapacityFromPreviousRounds function for testing + * @notice Exposes the internal _getUserUnusedCapacityFromPreviousRounds function for testing */ function exposed_getUserUnusedCapacityFromPreviousRounds( address user_, @@ -46,4 +46,119 @@ contract LM_PC_FundingPot_v1_Exposed is LM_PC_FundingPot_v1 { ) external view returns (uint) { return _getUserUnusedCapacityFromPreviousRounds(user_, currentRoundId_); } + + /** + * @notice Exposes the internal _validateRoundParameters function for testing + */ + function exposed_validateRoundParameters(Round storage round_) + external + view + { + _validateRoundParameters(round_); + } + + /** + * @notice Exposes the internal _validateEditRoundParameters function for testing + */ + function exposed_validateEditRoundParameters(Round storage round_) + external + view + { + _validateEditRoundParameters(round_); + } + + /** + * @notice Exposes the internal _validTimes function for testing + */ + function exposed_validTimes(uint start_, uint cliff_, uint end_) + external + pure + returns (bool) + { + return _validTimes(start_, cliff_, end_); + } + + /** + * @notice Exposes the internal _validateRoundContribution function for testing + */ + function exposed_validateRoundContribution( + uint64 roundId_, + uint8 accessCriteriaId_, + bytes32[] calldata merkleProof_, + uint amount_ + ) external view returns (uint) { + return _validateRoundContribution( + roundId_, accessCriteriaId_, merkleProof_, amount_ + ); + } + + /** + * @notice Exposes the internal _validateAndAdjustCaps function for testing + */ + function exposed_validateAndAdjustCaps( + uint64 roundId_, + uint amount_, + Round storage round_, + uint8 accessId_, + bool canOverrideContributionSpan_ + ) external view returns (uint) { + return _validateAndAdjustCaps( + roundId_, amount_, round_, accessId_, canOverrideContributionSpan_ + ); + } + + /** + * @notice Exposes the internal _validateAccessCriteria function for testing + */ + function exposed_validateAccessCriteria( + uint64 roundId_, + uint8 accessId_, + bytes32[] calldata merkleProof_ + ) external view { + _validateAccessCriteria(roundId_, accessId_, merkleProof_); + } + + /** + * @notice Exposes the internal _checkAllowedAddressList function for testing + */ + function exposed_checkAllowedAddressList( + address[] memory allowedAddresses_, + address sender_ + ) external pure returns (bool) { + return _checkAllowedAddressList(allowedAddresses_, sender_); + } + + /** + * @notice Exposes the internal _checkNftOwnership function for testing + */ + function exposed_checkNftOwnership(address nftContract_, address user_) + external + view + returns (bool) + { + return _checkNftOwnership(nftContract_, user_); + } + + /** + * @notice Exposes the internal _validateMerkleProof function for testing + */ + function exposed_validateMerkleProof( + bytes32 root_, + bytes32[] memory merkleProof_, + address user_, + uint64 roundId_ + ) external pure returns (bool) { + return _validateMerkleProof(root_, merkleProof_, user_, roundId_); + } + + /** + * @notice Exposes the internal _recordContribution function for testing + */ + function exposed_recordContribution( + uint64 roundId_, + address user_, + uint amount_ + ) external { + _recordContribution(roundId_, user_, amount_); + } } From 9269186282cba635bdca2233731c3e3b6f6103fe Mon Sep 17 00:00:00 2001 From: JeffreyJoel Date: Fri, 4 Apr 2025 15:51:36 +0100 Subject: [PATCH 044/130] test: add fuzz tests for internal contribution functions --- .../logicModule/LM_PC_FundingPot_v1.sol | 16 ++--- .../logicModule/LM_PC_FundingPot_v1.t.sol | 71 +++++++++++++++++++ .../LM_PC_FundingPot_v1_Exposed.sol | 27 ++++--- 3 files changed, 90 insertions(+), 24 deletions(-) diff --git a/src/modules/logicModule/LM_PC_FundingPot_v1.sol b/src/modules/logicModule/LM_PC_FundingPot_v1.sol index 0dc7bbd40..079fc84d0 100644 --- a/src/modules/logicModule/LM_PC_FundingPot_v1.sol +++ b/src/modules/logicModule/LM_PC_FundingPot_v1.sol @@ -585,11 +585,7 @@ contract LM_PC_FundingPot_v1 is } adjustedAmount = _validateAndAdjustCaps( - roundId_, - amount_, - round, - accessCriteriaId_, - canOverrideContributionSpan + roundId_, amount_, accessCriteriaId_, canOverrideContributionSpan ); return adjustedAmount; @@ -598,25 +594,25 @@ contract LM_PC_FundingPot_v1 is /// @notice Validates cap constraints and adjusts amount if needed /// @param roundId_ ID of the round /// @param amount_ Requested contribution amount - /// @param round_ Round storage object /// @param canOverrideContributionSpan_ Whether the user can override cap constraints /// @return adjustedAmount The potentially adjusted contribution amount function _validateAndAdjustCaps( uint64 roundId_, uint amount_, - Round storage round_, uint8 accessId_, bool canOverrideContributionSpan_ ) internal view returns (uint adjustedAmount) { adjustedAmount = amount_; - if (!canOverrideContributionSpan_ && round_.roundCap > 0) { + Round storage round = rounds[roundId_]; + + if (!canOverrideContributionSpan_ && round.roundCap > 0) { uint totalRoundContribution = _getTotalRoundContribution(roundId_); - uint effectiveRoundCap = round_.roundCap; + uint effectiveRoundCap = round.roundCap; // If global accumulative caps are enabled, // adjust the round cap to acommodate unused capacity from previous rounds - if (round_.globalAccumulativeCaps) { + if (round.globalAccumulativeCaps) { uint unusedCapacityFromPrevious = 0; for (uint64 i = 1; i < roundId_; ++i) { Round storage prevRound = rounds[i]; diff --git a/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol b/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol index ef7e77be7..e268fbb74 100644 --- a/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol +++ b/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol @@ -1769,6 +1769,77 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { // ------------------------------------------------------------------------- // Internal Functions + function testFuzz_validateAccessCriteria( + uint64 roundId_, + uint8 accessId_, + bytes32[] calldata merkleProof_ + ) external { + vm.assume(roundId_ <= fundingPot.getRoundCount() + 1); + vm.assume(accessId_ <= 4); + + try fundingPot.exposed_validateAccessCriteria( + roundId_, accessId_, merkleProof_ + ) { + assert(true); + } catch (bytes memory) { + assert(false); + } + } + + function testFuzz_validateAndAdjustCaps( + uint64 roundId_, + uint amount_, + uint8 accessId_, + bool canOverrideContributionSpan_ + ) external { + vm.assume(roundId_ > 0 && roundId_ >= fundingPot.getRoundCount()); + vm.assume(amount_ <= 1000); + vm.assume(accessId_ <= 4); + + uint initialTotalContribution = + fundingPot.exposed_getTotalRoundContributions(roundId_); + uint initialUserContribution = + fundingPot.exposed_getUserContributionToRound(roundId_, msg.sender); + + try fundingPot.exposed_validateAndAdjustCaps( + roundId_, amount_, accessId_, canOverrideContributionSpan_ + ) returns (uint adjustedAmount) { + assertLe( + adjustedAmount, amount_, "Adjusted amount should be <= amount_" + ); + assertGe(adjustedAmount, 0, "Adjusted amount should be >= 0"); + } catch (bytes memory reason) { + // Compare using keccak256 hash rather than direct string comparison + bytes32 roundCapReachedSelector = keccak256( + abi.encodeWithSignature( + "Module__LM_PC_FundingPot__RoundCapReached()" + ) + ); + bytes32 personalCapReachedSelector = keccak256( + abi.encodeWithSignature( + "Module__LM_PC_FundingPot__PersonalCapReached()" + ) + ); + + if (keccak256(reason) == roundCapReachedSelector) { + assertTrue( + !canOverrideContributionSpan_, + "Should not revert RoundCapReached when canOverrideContributionSpan is true" + ); + // Additional assertions commented out for now + // assert(roundCap > 0, "Round cap should be > 0"); + // assert( + // initialTotalContribution >= effectiveRoundCap, + // "Total contribution should be >= effectiveRoundCap" + // ); + } else if (keccak256(reason) == personalCapReachedSelector) { + // We expect this sometimes + assertTrue(true, "Personal cap reached as expected"); + } else { + assertTrue(false, "Unexpected revert reason"); + } + } + } // ------------------------------------------------------------------------- // Helper Functions diff --git a/test/unit/modules/logicModule/LM_PC_FundingPot_v1_Exposed.sol b/test/unit/modules/logicModule/LM_PC_FundingPot_v1_Exposed.sol index a708a70e6..3321ff921 100644 --- a/test/unit/modules/logicModule/LM_PC_FundingPot_v1_Exposed.sol +++ b/test/unit/modules/logicModule/LM_PC_FundingPot_v1_Exposed.sol @@ -50,22 +50,22 @@ contract LM_PC_FundingPot_v1_Exposed is LM_PC_FundingPot_v1 { /** * @notice Exposes the internal _validateRoundParameters function for testing */ - function exposed_validateRoundParameters(Round storage round_) - external - view - { - _validateRoundParameters(round_); - } + // function exposed_validateRoundParameters(Round memory round_) + // external + // view + // { + // _validateRoundParameters(round_); + // } /** * @notice Exposes the internal _validateEditRoundParameters function for testing */ - function exposed_validateEditRoundParameters(Round storage round_) - external - view - { - _validateEditRoundParameters(round_); - } + // function exposed_validateEditRoundParameters(Round storage round_) + // external + // view + // { + // _validateEditRoundParameters(round_); + // } /** * @notice Exposes the internal _validTimes function for testing @@ -98,12 +98,11 @@ contract LM_PC_FundingPot_v1_Exposed is LM_PC_FundingPot_v1 { function exposed_validateAndAdjustCaps( uint64 roundId_, uint amount_, - Round storage round_, uint8 accessId_, bool canOverrideContributionSpan_ ) external view returns (uint) { return _validateAndAdjustCaps( - roundId_, amount_, round_, accessId_, canOverrideContributionSpan_ + roundId_, amount_, accessId_, canOverrideContributionSpan_ ); } From a53efd1f3138f49a49bd9a1314959dc35f34d0bb Mon Sep 17 00:00:00 2001 From: JeffreyJoel Date: Sun, 6 Apr 2025 17:24:20 +0100 Subject: [PATCH 045/130] rebase changes --- .../logicModule/LM_PC_FundingPot_v1.sol | 32 +- .../interfaces/ILM_PC_FundingPot_v1.sol | 2 +- .../logicModule/LM_PC_FundingPot_v1.t.sol | 289 +++++++++--------- 3 files changed, 164 insertions(+), 159 deletions(-) diff --git a/src/modules/logicModule/LM_PC_FundingPot_v1.sol b/src/modules/logicModule/LM_PC_FundingPot_v1.sol index 079fc84d0..b8c9f3094 100644 --- a/src/modules/logicModule/LM_PC_FundingPot_v1.sol +++ b/src/modules/logicModule/LM_PC_FundingPot_v1.sol @@ -186,7 +186,8 @@ contract LM_PC_FundingPot_v1 is Round storage round = rounds[roundId_]; AccessCriteria storage accessCriteria = round.accessCriterias[id_]; - isOpen = (accessCriteria.accessCriteriaType == AccessCriteriaType.OPEN); + bool isOpen = + (accessCriteria.accessCriteriaType == AccessCriteriaType.OPEN); return ( isOpen, accessCriteria.nftContract, @@ -211,7 +212,7 @@ contract LM_PC_FundingPot_v1 is Round storage round = rounds[roundId_]; AccessCriteria storage accessCriteria = round.accessCriterias[accessId_]; - if (accessCriteria.accessCriteriaId == AccessCriteriaId.OPEN) { + if (accessCriteria.accessCriteriaType == AccessCriteriaType.OPEN) { return (true, 0, false, 0, 0, 0); } @@ -393,15 +394,15 @@ contract LM_PC_FundingPot_v1 is _validateEditRoundParameters(round); if ( - round.accessCriterias[accessId_].accessCriteriaId - == AccessCriteriaId.OPEN + round.accessCriterias[accessId_].accessCriteriaType + == AccessCriteriaType.OPEN ) { highestCap = personalCap_; } if ( - round.accessCriterias[accessId_].accessCriteriaId - == AccessCriteriaId.NFT && capByNFT_ > 0 + round.accessCriterias[accessId_].accessCriteriaType + == AccessCriteriaType.NFT && capByNFT_ > 0 ) { uint nftCap = personalCap_ + capByNFT_; if (nftCap > highestCap) { @@ -410,8 +411,8 @@ contract LM_PC_FundingPot_v1 is } if ( - round.accessCriterias[accessId_].accessCriteriaId - == AccessCriteriaId.MERKLE && capByMerkle_ > 0 + round.accessCriterias[accessId_].accessCriteriaType + == AccessCriteriaType.MERKLE && capByMerkle_ > 0 ) { uint merkleCap = personalCap_ + capByMerkle_; if (merkleCap > highestCap) { @@ -420,8 +421,8 @@ contract LM_PC_FundingPot_v1 is } if ( - round.accessCriterias[accessId_].accessCriteriaId - == AccessCriteriaId.LIST && capByList_ > 0 + round.accessCriterias[accessId_].accessCriteriaType + == AccessCriteriaType.LIST && capByList_ > 0 ) { uint listCap = personalCap_ + capByList_; if (listCap > highestCap) { @@ -668,19 +669,22 @@ contract LM_PC_FundingPot_v1 is Round storage round = rounds[roundId_]; AccessCriteria storage accessCriteria = round.accessCriterias[accessId_]; - if (accessCriteria.accessCriteriaId == AccessCriteriaId.OPEN) { + if (accessCriteria.accessCriteriaType == AccessCriteriaType.OPEN) { return; } bool accessGranted = false; - if (accessCriteria.accessCriteriaId == AccessCriteriaId.NFT) { + if (accessCriteria.accessCriteriaType == AccessCriteriaType.NFT) { accessGranted = _checkNftOwnership(accessCriteria.nftContract, msg.sender); - } else if (accessCriteria.accessCriteriaId == AccessCriteriaId.MERKLE) { + } else if ( + accessCriteria.accessCriteriaType == AccessCriteriaType.MERKLE + ) { accessGranted = _validateMerkleProof( accessCriteria.merkleRoot, merkleProof_, msg.sender, roundId_ ); - } else if (accessCriteria.accessCriteriaId == AccessCriteriaId.LIST) { + } else if (accessCriteria.accessCriteriaType == AccessCriteriaType.LIST) + { accessGranted = _checkAllowedAddressList( accessCriteria.allowedAddresses, msg.sender ); diff --git a/src/modules/logicModule/interfaces/ILM_PC_FundingPot_v1.sol b/src/modules/logicModule/interfaces/ILM_PC_FundingPot_v1.sol index 0f52c4d6c..5e550b89a 100644 --- a/src/modules/logicModule/interfaces/ILM_PC_FundingPot_v1.sol +++ b/src/modules/logicModule/interfaces/ILM_PC_FundingPot_v1.sol @@ -243,7 +243,7 @@ interface ILM_PC_FundingPot_v1 is IERC20PaymentClientBase_v2 { /// @notice Retrieves the access criteria for a specific funding round. /// @param roundId_ The unique identifier of the round to retrieve. /// @param accessCriteriaId_ The identifier of the access criteria to retrieve. - /// @return isOpen_ Whether the access criteria is open. + /// @return isRoundOpen_ Whether the access criteria is open. /// @return nftContract_ The address of the NFT contract used for access control. /// @return merkleRoot_ The merkle root used for access verification. /// @return allowedAddresses_ The list of explicitly allowed addresses. diff --git a/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol b/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol index e268fbb74..a571f6008 100644 --- a/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol +++ b/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol @@ -67,7 +67,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { uint roundCap; address hookContract; bytes hookFunction; - bool closureMechanism; + bool autoClosure; bool globalAccumulativeCaps; } // SuT @@ -106,17 +106,15 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { contributor2_ = address(0xDEAD); contributor3_ = address(0xCAFE); - fundingPotToken.mint(contributor1_, 10_000); - fundingPotToken.mint(contributor2_, 10_000); - fundingPotToken.mint(contributor3_, 10_000); + _token.mint(contributor1_, 10_000); + _token.mint(contributor2_, 10_000); + _token.mint(contributor3_, 10_000); // Setup the module to test _setUpOrchestrator(fundingPot); // Initiate the Logic Module with the metadata and config data - fundingPot.init( - _orchestrator, _METADATA, abi.encode(address(fundingPotToken)) - ); + fundingPot.init(_orchestrator, _METADATA, abi.encode(address(_token))); _authorizer.setIsAuthorized(address(this), true); @@ -214,7 +212,8 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { IModule_v1.Module__CallerNotAuthorized.selector, roleId, user_ ) ); - RoundParams memory params = _helper_createDefaultFundingRound(); + RoundParams memory params = _defaultRoundParams; + fundingPot.createRound( params.roundStart, params.roundEnd, @@ -231,7 +230,8 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { public { vm.assume(roundStart_ < block.timestamp); - RoundParams memory params = _helper_createDefaultFundingRound(); + RoundParams memory params = _defaultRoundParams; + params.roundStart = roundStart_; vm.expectRevert( abi.encodeWithSelector( @@ -254,7 +254,8 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { function testCreateRound_revertsGivenRoundEndTimeAndCapAreBothZero() public { - RoundParams memory params = _helper_createDefaultFundingRound(); + RoundParams memory params = _defaultRoundParams; + params.roundEnd = 0; params.roundCap = 0; vm.expectRevert( @@ -278,7 +279,8 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { function testCreateRound_revertsGivenRoundEndTimeIsBeforeRoundStart( uint roundEnd_ ) public { - RoundParams memory params = _helper_createDefaultFundingRound(); + RoundParams memory params = _defaultRoundParams; + vm.assume(roundEnd_ != 0 && roundEnd_ < params.roundStart); params.roundEnd = roundEnd_; vm.expectRevert( @@ -301,7 +303,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { function testCreateRound_revertsGivenHookContractIsSetButHookFunctionIsEmpty( ) public { - RoundParams memory params = _helper_createDefaultFundingRound(); + RoundParams memory params = _defaultRoundParams; params.hookContract = address(1); params.hookFunction = bytes(""); vm.expectRevert( @@ -324,7 +326,8 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { function testCreateRound_revertsGivenHookFunctionIsSetButHookContractIsEmpty( ) public { - RoundParams memory params = _helper_createDefaultFundingRound(); + RoundParams memory params = _defaultRoundParams; + params.hookContract = address(0); params.hookFunction = bytes("test"); vm.expectRevert( @@ -429,7 +432,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { function testEditRound_revertsGivenUserIsNotFundingPotAdmin(address user_) public { - testCreateRound(1000); + testCreateRound(); uint64 roundId = fundingPot.getRoundCount(); RoundParams memory params = RoundParams({ @@ -465,9 +468,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { } function testEditRound_revertsGivenRoundIsNotCreated() public { - testCreateRound(1000); - uint64 roundId = fundingPot.getRoundCount() + 1; - + testCreateRound(); uint64 roundId = fundingPot.getRoundCount(); RoundParams memory params = RoundParams({ @@ -500,7 +501,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { } function testEditRound_revertsGivenRoundIsActive() public { - testCreateRound(1000); + testCreateRound(); uint64 roundId = fundingPot.getRoundCount(); RoundParams memory params; @@ -547,20 +548,10 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { function testEditRound_revertsGivenRoundStartIsInThePast(uint roundStartP_) public { - testCreateRound(1000); + testCreateRound(); uint64 roundId = fundingPot.getRoundCount(); - - RoundParameters memory editedParams = _helper_createEditedRoundParams(); + _editedRoundParams; vm.assume(roundStartP_ < block.timestamp); - RoundParams memory params = RoundParams({ - roundStart: roundStartP_, - roundEnd: block.timestamp + 4 days, - roundCap: 2000, - hookContract: address(0x1), - hookFunction: bytes("test"), - autoClosure: true, - globalAccumulativeCaps: true - }); vm.expectRevert( abi.encodeWithSelector( @@ -572,18 +563,18 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { fundingPot.editRound( roundId, - params.roundStart, - params.roundEnd, - params.roundCap, - params.hookContract, - params.hookFunction, - params.autoClosure, - params.globalAccumulativeCaps + _editedRoundParams.roundStart, + _editedRoundParams.roundEnd, + _editedRoundParams.roundCap, + _editedRoundParams.hookContract, + _editedRoundParams.hookFunction, + _editedRoundParams.autoClosure, + _editedRoundParams.globalAccumulativeCaps ); } function testEditRound_revertsGivenRoundEndTimeAndCapAreBothZero() public { - testCreateRound(1000); + testCreateRound(); uint64 roundId = fundingPot.getRoundCount(); RoundParams memory params = RoundParams({ @@ -624,7 +615,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { roundEnd_ != 0 && roundStart_ > block.timestamp && roundEnd_ < roundStart_ ); - testCreateRound(1000); + testCreateRound(); uint64 roundId = fundingPot.getRoundCount(); // Get the current round start time @@ -668,7 +659,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { function testEditRound_revertsGivenHookContractIsSetButHookFunctionIsEmpty() public { - testCreateRound(1000); + testCreateRound(); uint64 roundId = fundingPot.getRoundCount(); RoundParams memory params = _helper_createEditRoundParams( @@ -704,7 +695,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { function testEditRound_revertsGivenHookFunctionIsSetButHookContractIsEmpty() public { - testCreateRound(1000); + testCreateRound(); uint64 roundId = fundingPot.getRoundCount(); RoundParams memory params = _helper_createEditRoundParams( @@ -752,7 +743,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { */ function testEditRound() public { - testCreateRound(1000); + testCreateRound(); uint64 lastRoundId = fundingPot.getRoundCount(); RoundParams memory params = _editedRoundParams; @@ -827,7 +818,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { vm.assume(accessCriteriaEnum_ >= 0 && accessCriteriaEnum_ <= 4); vm.assume(user_ != address(0) && user_ != address(this)); - testCreateRound(1000); + testCreateRound(); uint64 roundId = fundingPot.getRoundCount(); ILM_PC_FundingPot_v1.AccessCriteria memory accessCriteria = @@ -1004,10 +995,6 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { ├── Then it should not revert └── Then the access criteria should be updated */ - function testContributeToRound_revertsGivenRoundContributionCapReached( - uint roundCap_ - ) public { - testCreateRound(10); function testFuzzEditAccessCriteriaForRound_revertsGivenUserDoesNotHaveFundingPotAdminRole( uint8 accessCriteriaEnum, @@ -1086,6 +1073,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { _helper_setupRoundWithAccessCriteria(accessCriteriaEnum); uint64 roundId = fundingPot.getRoundCount(); uint8 accessCriteriaId = 0; + uint amount = 250; // Warp to make the round active (uint roundStart,,,,,,) = fundingPot.getRoundGenericParameters(roundId); @@ -1093,10 +1081,12 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { // Approve vm.prank(contributor1_); - fundingPotToken.approve(address(fundingPot), 110); + _token.approve(address(fundingPot), 110); vm.prank(contributor1_); - fundingPot.contributeToRound(roundId, 10, accessId, new bytes32[](0)); + fundingPot.contributeToRound( + roundId, 10, accessCriteriaId, new bytes32[](0) + ); vm.expectRevert( abi.encodeWithSelector( @@ -1108,14 +1098,14 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { vm.prank(contributor1_); fundingPot.contributeToRound( - roundId, amount, accessId, new bytes32[](0) + roundId, amount, accessCriteriaId, new bytes32[](0) ); } function testContributeToRound_revertsGivenContributionIsBeforeRoundStart() public { - testCreateRound(1000); + testCreateRound(); uint64 roundId = fundingPot.getRoundCount(); uint8 accessId = 1; @@ -1124,7 +1114,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { ILM_PC_FundingPot_v1.AccessCriteria memory accessCriteria = _helper_createAccessCriteria(accessId); - fundingPot.setAccessCriteriaForRound(roundId, accessId, accessCriteria); + fundingPot.setAccessCriteriaForRound(roundId, accessCriteria); _helper_callSetAccessCriteriaPrivileges( roundId, accessId, 500, 0, 0, 0, false, 0, 0, 0 ); @@ -1133,7 +1123,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { // Approve vm.prank(contributor1_); - fundingPotToken.approve(address(fundingPot), 500); + _token.approve(address(fundingPot), 500); vm.expectRevert( abi.encodeWithSelector( @@ -1152,7 +1142,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { function testContributeToRound_revertsGivenContributionIsAfterRoundEnd() public { - testCreateRound(1000); + testCreateRound(); uint64 roundId = fundingPot.getRoundCount(); uint8 accessId = 0; @@ -1161,7 +1151,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { ILM_PC_FundingPot_v1.AccessCriteria memory accessCriteria = _helper_createAccessCriteria(accessId); - fundingPot.setAccessCriteriaForRound(roundId, accessId, accessCriteria); + fundingPot.setAccessCriteriaForRound(roundId, accessCriteria); _helper_callSetAccessCriteriaPrivileges( roundId, accessId, 500, 0, 0, 0, false, 0, 0, 0 ); @@ -1171,7 +1161,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { // Approve vm.prank(contributor1_); - fundingPotToken.approve(address(fundingPot), 500); + _token.approve(address(fundingPot), 500); vm.expectRevert( abi.encodeWithSelector( @@ -1190,7 +1180,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { function testContributeToRound_revertsGivenNFTAccessCriteriaIsNotMet() public { - testCreateRound(1000); + testCreateRound(); uint64 roundId = fundingPot.getRoundCount(); uint8 accessId = 1; @@ -1199,7 +1189,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { ILM_PC_FundingPot_v1.AccessCriteria memory accessCriteria = _helper_createAccessCriteria(accessId); - fundingPot.setAccessCriteriaForRound(roundId, accessId, accessCriteria); + fundingPot.setAccessCriteriaForRound(roundId, accessCriteria); _helper_callSetAccessCriteriaPrivileges( roundId, accessId, 500, 10, 0, 0, false, 0, 0, 0 ); @@ -1209,7 +1199,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { // Approve vm.prank(contributor1_); - fundingPotToken.approve(address(fundingPot), amount); + _token.approve(address(fundingPot), amount); vm.expectRevert( abi.encodeWithSelector( @@ -1227,7 +1217,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { function testContributeToRound_revertsGivenMerkleRootAccessCriteriaIsNotMet( ) public { - testCreateRound(1000); + testCreateRound(); uint64 roundId = fundingPot.getRoundCount(); uint8 accessId = 2; @@ -1236,7 +1226,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { ILM_PC_FundingPot_v1.AccessCriteria memory accessCriteria = _helper_createAccessCriteria(accessId); - fundingPot.setAccessCriteriaForRound(roundId, accessId, accessCriteria); + fundingPot.setAccessCriteriaForRound(roundId, accessCriteria); _helper_callSetAccessCriteriaPrivileges( roundId, accessId, 500, 0, 0, 0, false, 0, 0, 0 ); @@ -1246,7 +1236,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { // Approve vm.prank(contributor1_); - fundingPotToken.approve(address(fundingPot), amount); + _token.approve(address(fundingPot), amount); vm.expectRevert( abi.encodeWithSelector( @@ -1262,7 +1252,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { function testontributeToRound_revertsGivenAllowedListAccessCriteriaIsNotMet( ) public { - testCreateRound(1000); + testCreateRound(); uint64 roundId = fundingPot.getRoundCount(); uint8 accessId = 3; @@ -1271,7 +1261,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { ILM_PC_FundingPot_v1.AccessCriteria memory accessCriteria = _helper_createAccessCriteria(accessId); - fundingPot.setAccessCriteriaForRound(roundId, accessId, accessCriteria); + fundingPot.setAccessCriteriaForRound(roundId, accessCriteria); _helper_callSetAccessCriteriaPrivileges( roundId, accessId, 500, 0, 0, 0, false, 0, 0, 0 ); @@ -1281,7 +1271,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { // Approve vm.prank(contributor1_); - fundingPotToken.approve(address(fundingPot), amount); + _token.approve(address(fundingPot), amount); vm.expectRevert( abi.encodeWithSelector( @@ -1299,7 +1289,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { function testContributeToRound_revertsGivenPreviousContributionExceedsPersonalCap( ) public { - testCreateRound(1000); + testCreateRound(); uint64 roundId = fundingPot.getRoundCount(); uint8 accessId = 1; @@ -1308,7 +1298,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { ILM_PC_FundingPot_v1.AccessCriteria memory accessCriteria = _helper_createAccessCriteria(accessId); - fundingPot.setAccessCriteriaForRound(roundId, accessId, accessCriteria); + fundingPot.setAccessCriteriaForRound(roundId, accessCriteria); _helper_callSetAccessCriteriaPrivileges( roundId, accessId, 500, 0, 0, 0, false, 0, 0, 0 ); @@ -1320,7 +1310,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { // Approve vm.prank(contributor1_); - fundingPotToken.approve(address(fundingPot), 1000); + _token.approve(address(fundingPot), 1000); vm.prank(contributor1_); fundingPot.contributeToRound( @@ -1390,7 +1380,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { │ */ function testContributeToRound_worksGivenAllConditionsMet() public { - testCreateRound(1000); + testCreateRound(); uint64 roundId = fundingPot.getRoundCount(); uint8 accessId = 1; @@ -1399,7 +1389,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { ILM_PC_FundingPot_v1.AccessCriteria memory accessCriteria = _helper_createAccessCriteria(accessId); - fundingPot.setAccessCriteriaForRound(roundId, accessId, accessCriteria); + fundingPot.setAccessCriteriaForRound(roundId, accessCriteria); _helper_callSetAccessCriteriaPrivileges( roundId, accessId, 500, 100, 0, 0, false, 0, 0, 0 ); @@ -1410,7 +1400,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { // Approve vm.prank(contributor1_); - fundingPotToken.approve(address(fundingPot), 500); + _token.approve(address(fundingPot), 500); vm.prank(contributor1_); fundingPot.contributeToRound( @@ -1428,7 +1418,9 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { } function testContributeToRound_worksGivenUserCurrentContributionExceedsTheRoundCap( - uint roundCap_ + uint roundCap_, + uint8 accessCriteriaEnumOld, + uint8 accessCriteriaEnumNew ) public { vm.assume(accessCriteriaEnumOld >= 0 && accessCriteriaEnumOld <= 4); vm.assume( @@ -1444,7 +1436,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { ILM_PC_FundingPot_v1.AccessCriteria memory accessCriteria = _helper_createAccessCriteria(accessId); - fundingPot.setAccessCriteriaForRound(roundId, accessId, accessCriteria); + fundingPot.setAccessCriteriaForRound(roundId, accessCriteria); _helper_callSetAccessCriteriaPrivileges( roundId, accessId, 500, 0, 0, 0, false, 0, 0, 0 ); @@ -1455,7 +1447,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { // Approve vm.prank(contributor1_); - fundingPotToken.approve(address(fundingPot), amount); + _token.approve(address(fundingPot), amount); vm.prank(contributor1_); fundingPot.contributeToRound( @@ -1473,7 +1465,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { function testContributeToRound_worksGivenContributionPartiallyExceedingPersonalCap( ) public { - testCreateRound(1000); + testCreateRound(); uint64 roundId = fundingPot.getRoundCount(); uint8 accessId = 1; @@ -1483,7 +1475,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { ILM_PC_FundingPot_v1.AccessCriteria memory accessCriteria = _helper_createAccessCriteria(accessId); - fundingPot.setAccessCriteriaForRound(roundId, accessId, accessCriteria); + fundingPot.setAccessCriteriaForRound(roundId, accessCriteria); _helper_callSetAccessCriteriaPrivileges( roundId, accessId, 500, 0, 0, 0, false, 0, 0, 0 ); @@ -1495,7 +1487,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { // Approve vm.prank(contributor1_); - fundingPotToken.approve(address(fundingPot), 1000 ether); + _token.approve(address(fundingPot), 1000 ether); // First contribution vm.prank(contributor1_); @@ -1524,7 +1516,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { function testContributeToRound_worksGivenUserCanOverrideTimeConstraints() public { - testCreateRound(1000); + testCreateRound(); uint64 roundId = fundingPot.getRoundCount(); uint8 accessId = 1; @@ -1533,7 +1525,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { ILM_PC_FundingPot_v1.AccessCriteria memory accessCriteria = _helper_createAccessCriteria(accessId); - fundingPot.setAccessCriteriaForRound(roundId, accessId, accessCriteria); + fundingPot.setAccessCriteriaForRound(roundId, accessCriteria); // Set privileges with override capability _helper_callSetAccessCriteriaPrivileges( @@ -1548,7 +1540,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { // Approve vm.prank(contributor1_); - fundingPotToken.approve(address(fundingPot), amount); + _token.approve(address(fundingPot), amount); // This should succeed despite being after round end, due to override privilege vm.prank(contributor1_); @@ -1563,33 +1555,25 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { } function testContributeToRound_worksGivenPersonalCapAccumulation() public { - ( - uint roundStart, - uint roundEnd, - uint roundCap, - address hookContract, - bytes memory hookFunction, - bool closureMechanism, - bool globalAccumulativeCaps - ) = _helper_createDefaultFundingRound(1000); - globalAccumulativeCaps = true; // global accumulative caps enabled - - // Round 1 + _defaultRoundParams.globalAccumulativeCaps = true; // global accumulative caps enabled + + // Create Round 1 fundingPot.createRound( - roundStart, - roundEnd, - roundCap, - hookContract, - hookFunction, - closureMechanism, - globalAccumulativeCaps + _defaultRoundParams.roundStart, + _defaultRoundParams.roundEnd, + _defaultRoundParams.roundCap, + _defaultRoundParams.hookContract, + _defaultRoundParams.hookFunction, + _defaultRoundParams.autoClosure, + _defaultRoundParams.globalAccumulativeCaps ); + uint64 round1Id = fundingPot.getRoundCount(); uint8 accessId = 1; ILM_PC_FundingPot_v1.AccessCriteria memory accessCriteria = _helper_createAccessCriteria(1); - fundingPot.setAccessCriteriaForRound(round1Id, accessId, accessCriteria); + fundingPot.setAccessCriteriaForRound(round1Id, accessCriteria); _helper_callSetAccessCriteriaPrivileges( round1Id, accessId, 500, 0, 0, 0, false, 0, 0, 0 ); @@ -1597,32 +1581,32 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { // Create Round 2 fundingPot.createRound( - roundStart + 3 days, - roundEnd + 3 days, - roundCap, - hookContract, - hookFunction, - closureMechanism, - globalAccumulativeCaps + _defaultRoundParams.roundStart + 3 days, + _defaultRoundParams.roundEnd + 3 days, + _defaultRoundParams.roundCap, + _defaultRoundParams.hookContract, + _defaultRoundParams.hookFunction, + _defaultRoundParams.autoClosure, + _defaultRoundParams.globalAccumulativeCaps ); uint64 round2Id = fundingPot.getRoundCount(); - fundingPot.setAccessCriteriaForRound(round2Id, accessId, accessCriteria); + fundingPot.setAccessCriteriaForRound(round2Id, accessCriteria); _helper_callSetAccessCriteriaPrivileges( round2Id, accessId, 500, 0, 0, 0, false, 0, 0, 0 ); vm.startPrank(contributor1_); - fundingPotToken.approve(address(fundingPot), 1500); + _token.approve(address(fundingPot), 1500); // Contribute to Round 1 - vm.warp(roundStart + 1); + vm.warp(_defaultRoundParams.roundStart + 1); uint round1Contribution = 200; fundingPot.contributeToRound( round1Id, round1Contribution, accessId, new bytes32[](0) ); // Move to Round 2 - vm.warp(roundStart + 3 days + 1); + vm.warp(_defaultRoundParams.roundStart + 3 days + 1); // Calculate unused capacity from Round 1 uint personalCap = fundingPot.exposed_getUserPersonalCapForRound( @@ -1651,33 +1635,24 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { function testContributeToRound_worksGivenTotalRoundCapAccumulation() public { - ( - uint roundStart, - uint roundEnd, - uint roundCap, - address hookContract, - bytes memory hookFunction, - bool closureMechanism, - bool globalAccumulativeCaps - ) = _helper_createDefaultFundingRound(1000); - globalAccumulativeCaps = true; + _defaultRoundParams.globalAccumulativeCaps = true; // global accumulative caps enabled // Create Round 1 fundingPot.createRound( - roundStart, - roundEnd, - roundCap, - hookContract, - hookFunction, - closureMechanism, - globalAccumulativeCaps + _defaultRoundParams.roundStart, + _defaultRoundParams.roundEnd, + _defaultRoundParams.roundCap, + _defaultRoundParams.hookContract, + _defaultRoundParams.hookFunction, + _defaultRoundParams.autoClosure, + _defaultRoundParams.globalAccumulativeCaps ); uint64 round1Id = fundingPot.getRoundCount(); uint8 accessId = 0; ILM_PC_FundingPot_v1.AccessCriteria memory accessCriteria = _helper_createAccessCriteria(accessId); - fundingPot.setAccessCriteriaForRound(round1Id, accessId, accessCriteria); + fundingPot.setAccessCriteriaForRound(round1Id, accessCriteria); _helper_callSetAccessCriteriaPrivileges( round1Id, accessId, 500, 0, 0, 0, false, 0, 0, 0 ); @@ -1685,49 +1660,49 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { // Round 2 with a different cap uint round2Cap = 500; fundingPot.createRound( - roundStart + 3 days, - roundEnd + 3 days, + _defaultRoundParams.roundStart, + _defaultRoundParams.roundEnd, round2Cap, - hookContract, - hookFunction, - closureMechanism, - globalAccumulativeCaps + _defaultRoundParams.hookContract, + _defaultRoundParams.hookFunction, + _defaultRoundParams.autoClosure, + _defaultRoundParams.globalAccumulativeCaps ); uint64 round2Id = fundingPot.getRoundCount(); - fundingPot.setAccessCriteriaForRound(round2Id, accessId, accessCriteria); + fundingPot.setAccessCriteriaForRound(round2Id, accessCriteria); _helper_callSetAccessCriteriaPrivileges( round2Id, accessId, 500, 0, 0, 0, false, 0, 0, 0 ); // Round 1: Multiple users contribute, but don't reach the cap - vm.warp(roundStart + 1); + vm.warp(_defaultRoundParams.roundStart + 1); vm.startPrank(contributor1_); - fundingPotToken.approve(address(fundingPot), 300); + _token.approve(address(fundingPot), 300); fundingPot.contributeToRound(round1Id, 300, accessId, new bytes32[](0)); vm.stopPrank(); vm.startPrank(contributor2_); - fundingPotToken.approve(address(fundingPot), 200); + _token.approve(address(fundingPot), 200); fundingPot.contributeToRound(round1Id, 200, accessId, new bytes32[](0)); vm.stopPrank(); // Move to Round 2 - vm.warp(roundStart + 3 days + 1); + vm.warp(_defaultRoundParams.roundStart + 3 days + 1); - uint unusedCapacityFromRound1 = - roundCap - fundingPot.exposed_getTotalRoundContributions(round1Id); + uint unusedCapacityFromRound1 = _defaultRoundParams.roundCap + - fundingPot.exposed_getTotalRoundContributions(round1Id); uint totalAvailableCapacityRound2 = round2Cap + unusedCapacityFromRound1; // Round 2: Contributors try to use the accumulated capacity vm.startPrank(contributor2_); - fundingPotToken.approve(address(fundingPot), 400); + _token.approve(address(fundingPot), 400); fundingPot.contributeToRound(round2Id, 400, accessId, new bytes32[](0)); vm.stopPrank(); vm.startPrank(contributor3_); - fundingPotToken.approve(address(fundingPot), 600); + _token.approve(address(fundingPot), 600); fundingPot.contributeToRound(round2Id, 600, accessId, new bytes32[](0)); vm.stopPrank(); @@ -1943,4 +1918,30 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { fundingPot.setAccessCriteriaForRound(roundId, accessCriteria); } + + function _helper_callSetAccessCriteriaPrivileges( + uint64 roundId, + uint8 accessId, + uint personalCap, + uint capByNFT, + uint capByMerkle, + uint capByList, + bool canOverrideTimeConstraints, + uint start, + uint cliff, + uint end + ) internal { + fundingPot.setAccessCriteriaPrivileges( + roundId, + accessId, + personalCap, + capByNFT, + capByMerkle, + capByList, + canOverrideTimeConstraints, + start, + cliff, + end + ); + } } From 83335acc1d2c707d9351005391674bd1d542f745 Mon Sep 17 00:00:00 2001 From: JeffreyJoel Date: Mon, 7 Apr 2025 14:12:19 +0100 Subject: [PATCH 046/130] add closure logic --- .../logicModule/LM_PC_FundingPot_v1.sol | 134 ++++++++++++++---- .../interfaces/ILM_PC_FundingPot_v1.sol | 23 +++ .../logicModule/LM_PC_FundingPot_v1.t.sol | 99 +++++++------ .../LM_PC_FundingPot_v1_Exposed.sol | 21 +-- 4 files changed, 186 insertions(+), 91 deletions(-) diff --git a/src/modules/logicModule/LM_PC_FundingPot_v1.sol b/src/modules/logicModule/LM_PC_FundingPot_v1.sol index b8c9f3094..805b25e00 100644 --- a/src/modules/logicModule/LM_PC_FundingPot_v1.sol +++ b/src/modules/logicModule/LM_PC_FundingPot_v1.sol @@ -103,6 +103,9 @@ contract LM_PC_FundingPot_v1 is /// @notice Maps round IDs to total contributions mapping(uint64 => uint) private roundTotalContributions; + /// @notice Maps round IDs to closed status + mapping(uint64 => bool) private roundClosed; + /// @notice The current round count. uint64 private roundCount; @@ -186,14 +189,21 @@ contract LM_PC_FundingPot_v1 is Round storage round = rounds[roundId_]; AccessCriteria storage accessCriteria = round.accessCriterias[id_]; - bool isOpen = - (accessCriteria.accessCriteriaType == AccessCriteriaType.OPEN); - return ( - isOpen, - accessCriteria.nftContract, - accessCriteria.merkleRoot, - accessCriteria.allowedAddresses - ); + if (accessCriteria.accessCriteriaType == AccessCriteriaType.OPEN) { + return ( + true, + accessCriteria.nftContract, + accessCriteria.merkleRoot, + accessCriteria.allowedAddresses + ); + } else { + return ( + false, + accessCriteria.nftContract, + accessCriteria.merkleRoot, + accessCriteria.allowedAddresses + ); + } } /// @inheritdoc ILM_PC_FundingPot_v1 @@ -244,6 +254,11 @@ contract LM_PC_FundingPot_v1 is return roundIdtoAccessId[roundId_]; } + /// @inheritdoc ILM_PC_FundingPot_v1 + function isRoundClosed(uint64 roundId_) external view returns (bool) { + return roundClosed[roundId_]; + } + // ------------------------------------------------------------------------- // Public - Mutating @@ -463,16 +478,52 @@ contract LM_PC_FundingPot_v1 is bytes32[] calldata merkleProof_ ) external { uint adjustedAmount = _validateRoundContribution( - roundId_, accessCriteriaId_, merkleProof_, amount_ + roundId_, accessCriteriaId_, merkleProof_, amount_, msg.sender ); - _recordContribution(roundId_, msg.sender, adjustedAmount); + Round storage round = rounds[roundId_]; + + //Record contribution + userContributions[roundId_][msg.sender] += adjustedAmount; + roundTotalContributions[roundId_] += adjustedAmount; IERC20(contributionToken).safeTransferFrom( msg.sender, address(this), adjustedAmount ); emit ContributionMade(roundId_, msg.sender, adjustedAmount); + + // contribution triggers automatic closure + if (!roundClosed[roundId_] && round.autoClosure) { + bool readyToClose = _checkRoundClosureConditions(roundId_); + if (readyToClose) { + _closeRound(roundId_); + } else { + revert Module__LM_PC_FundingPot__ClosureConditionsNotMet(); + } + } + } + + /// @inheritdoc ILM_PC_FundingPot_v1 + function closeRound(uint64 roundId_) external { + Round storage round = rounds[roundId_]; + + // Validate round exists + if (round.roundEnd == 0 && round.roundCap == 0) { + revert Module__LM_PC_FundingPot__RoundNotCreated(); + } + + // Check if round is already closed + if (roundClosed[roundId_]) { + revert Module__LM_PC_FundingPot__RoundHasEnded(); + } + + bool readyToClose = _checkRoundClosureConditions(roundId_); + if (readyToClose) { + _closeRound(roundId_); + } else { + revert Module__LM_PC_FundingPot__ClosureConditionsNotMet(); + } } // ------------------------------------------------------------------------- @@ -546,12 +597,14 @@ contract LM_PC_FundingPot_v1 is /// @param accessCriteriaId_ ID of the access criteria to check /// @param merkleProof_ Merkle proof for validation if needed /// @param amount_ The amount sent by the user + /// @param user_ The address of the user /// @return adjustedAmount The potentially adjusted contribution amount based on personal and round caps function _validateRoundContribution( uint64 roundId_, uint8 accessCriteriaId_, bytes32[] calldata merkleProof_, - uint amount_ + uint amount_, + address user_ ) internal view returns (uint adjustedAmount) { Round storage round = rounds[roundId_]; uint currentTime = block.timestamp; @@ -570,7 +623,9 @@ contract LM_PC_FundingPot_v1 is revert Module__LM_PC_FundingPot__RoundHasNotStarted(); } - _validateAccessCriteria(roundId_, accessCriteriaId_, merkleProof_); + _validateAccessCriteria( + roundId_, accessCriteriaId_, merkleProof_, user_ + ); AccessCriteriaPrivileges storage privileges = accessCriteriaPrivileges[roundId_][accessCriteriaId_]; @@ -664,7 +719,8 @@ contract LM_PC_FundingPot_v1 is function _validateAccessCriteria( uint64 roundId_, uint8 accessId_, - bytes32[] calldata merkleProof_ + bytes32[] calldata merkleProof_, + address user_ ) internal view { Round storage round = rounds[roundId_]; AccessCriteria storage accessCriteria = round.accessCriterias[accessId_]; @@ -676,18 +732,17 @@ contract LM_PC_FundingPot_v1 is bool accessGranted = false; if (accessCriteria.accessCriteriaType == AccessCriteriaType.NFT) { accessGranted = - _checkNftOwnership(accessCriteria.nftContract, msg.sender); + _checkNftOwnership(accessCriteria.nftContract, user_); } else if ( accessCriteria.accessCriteriaType == AccessCriteriaType.MERKLE ) { accessGranted = _validateMerkleProof( - accessCriteria.merkleRoot, merkleProof_, msg.sender, roundId_ + accessCriteria.merkleRoot, merkleProof_, user_, roundId_ ); } else if (accessCriteria.accessCriteriaType == AccessCriteriaType.LIST) { - accessGranted = _checkAllowedAddressList( - accessCriteria.allowedAddresses, msg.sender - ); + accessGranted = + _checkAllowedAddressList(accessCriteria.allowedAddresses, user_); } } @@ -842,15 +897,42 @@ contract LM_PC_FundingPot_v1 is return true; } - /// @notice Records a contribution for a user in a specific round - /// @dev Updates the user's contribution and the total round contribution - /// @param roundId_ The ID of the round - /// @param user_ The address of the user making the contribution - /// @param amount_ The amount of the contribution - function _recordContribution(uint64 roundId_, address user_, uint amount_) + /// @notice Handles round closure logic + /// @dev Updates round status and executes hook if needed + /// @param roundId_ The ID of the round to close + function _closeRound(uint64 roundId_) internal { + Round storage round = rounds[roundId_]; + + // Mark round as closed + roundClosed[roundId_] = true; + + // Execute hook if configured + if (round.hookContract != address(0) && round.hookFunction.length > 0) { + (bool success,) = round.hookContract.call(round.hookFunction); + if (!success) { + revert Module__LM_PC_FundingPot__HookExecutionFailed(); + } + } + + // Emit event for round closure + emit RoundClosed( + roundId_, block.timestamp, roundTotalContributions[roundId_] + ); + } + + /// @notice Checks if a round has reached its cap or time limit + /// @param roundId_ The ID of the round to check + /// @return Boolean indicating if the round has reached its cap or time limit + function _checkRoundClosureConditions(uint64 roundId_) internal + view + returns (bool) { - userContributions[roundId_][user_] += amount_; - roundTotalContributions[roundId_] += amount_; + Round storage round = rounds[roundId_]; + uint totalContribution = roundTotalContributions[roundId_]; + bool capReached = + round.roundCap > 0 && totalContribution == round.roundCap; + bool timeEnded = round.roundEnd > 0 && block.timestamp >= round.roundEnd; + return capReached || timeEnded; } } diff --git a/src/modules/logicModule/interfaces/ILM_PC_FundingPot_v1.sol b/src/modules/logicModule/interfaces/ILM_PC_FundingPot_v1.sol index 5e550b89a..19561cf10 100644 --- a/src/modules/logicModule/interfaces/ILM_PC_FundingPot_v1.sol +++ b/src/modules/logicModule/interfaces/ILM_PC_FundingPot_v1.sol @@ -147,6 +147,14 @@ interface ILM_PC_FundingPot_v1 is IERC20PaymentClientBase_v2 { /// @param amount_ The amount contributed event ContributionMade(uint64 roundId_, address contributor_, uint amount_); + /// @notice Emitted when a round is closed + /// @param roundId_ The ID of the round + /// @param timestamp_ The timestamp when the round was closed + /// @param totalContributions_ The total contributions collected in the round + event RoundClosed( + uint64 roundId_, uint timestamp_, uint totalContributions_ + ); + // ------------------------------------------------------------------------- // Errors @@ -215,6 +223,12 @@ interface ILM_PC_FundingPot_v1 is IERC20PaymentClientBase_v2 { /// @notice Round contribution cap has been reached error Module__LM_PC_FundingPot__RoundCapReached(); + /// @notice Round Closure conditions are not met + error Module__LM_PC_FundingPot__ClosureConditionsNotMet(); + + /// @notice Hook execution failed + error Module__LM_PC_FundingPot__HookExecutionFailed(); + // ------------------------------------------------------------------------- // Public - Getters @@ -290,6 +304,11 @@ interface ILM_PC_FundingPot_v1 is IERC20PaymentClientBase_v2 { view returns (uint8 accessCriteriaCount_); + /// @notice Retrieves the closed status of a round + /// @param roundId_ The ID of the round + /// @return The closed status of the round + function isRoundClosed(uint64 roundId_) external view returns (bool); + // ------------------------------------------------------------------------- // Public - Mutating @@ -391,4 +410,8 @@ interface ILM_PC_FundingPot_v1 is IERC20PaymentClientBase_v2 { uint8 accessCriteriaId_, bytes32[] calldata merkleProof_ ) external; + + /// @notice Closes a round + /// @param roundId_ The ID of the round to close + function closeRound(uint64 roundId_) external; } diff --git a/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol b/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol index a571f6008..39bcd6b26 100644 --- a/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol +++ b/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol @@ -175,12 +175,12 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { │ └── When user attempts to create a round │ └── Then it should revert │ - ├── And round end time == 0 + ├── And round end time == 0 ├── And round cap == 0 │ └── When user attempts to create a round │ └── Then it should revert │ - ├── And round end time is set + ├── And round end time is set ├── And round end != 0 ├── And round end < round start │ └── When user attempts to create a round @@ -352,7 +352,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { ├── Given all the valid parameters are provided │ └── When user attempts to create a round │ └── Then it should not be active and should return the round id - */ + */ function testCreateRound() public { RoundParams memory params = _defaultRoundParams; @@ -396,29 +396,29 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { │ └── Then it should revert │ └── Given user has FUNDING_POT_ADMIN_ROLE - ├── Given round does not exist - │ └── When user attempts to edit the round - │ └── Then it should revert + ├── Given round does not exist + │ └── When user attempts to edit the round + │ └── Then it should revert │ - ├── Given round is active - │ └── When user attempts to edit the round - │ └── Then it should revert + ├── Given round is active + │ └── When user attempts to edit the round + │ └── Then it should revert │ - ├── Given round start time is in the past + ├── Given round start time is in the past │ └── When user attempts to edit a round with this parameter - │ └── Then it should revert + │ └── Then it should revert │ ├── Given round end time == 0 and round cap == 0 │ └── When user attempts to edit a round with these parameters - │ └── Then it should revert + │ └── Then it should revert │ ├── Given round end time is set and round end < round start - │ └── When user attempts to edit the round - │ └── Then it should revert + │ └── When user attempts to edit the round + │ └── Then it should revert │ ├── Given hook contract is set but hook function is empty - │ └── When user attempts to edit the round - │ └── Then it should revert + │ └── When user attempts to edit the round + │ └── Then it should revert │ ├── Given hook function is set but hook contract is empty │ └── When user attempts to edit the round @@ -786,29 +786,29 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { │ └── Then it should revert │ └── Given user has FUNDING_POT_ADMIN_ROLE - ├── Given round does not exist - │ └── When user attempts to set access criteria - │ └── Then it should revert + ├── Given round does not exist + │ └── When user attempts to set access criteria + │ └── Then it should revert │ - ├── Given round is active - │ └── When user attempts to set access criteria - │ └── Then it should revert + ├── Given round is active + │ └── When user attempts to set access criteria + │ └── Then it should revert │ - ├── Given AccessCriteriaId is NFT and nftContract is 0x0 - │ └── When user attempts to set access criteria - │ └── Then it should revert + ├── Given AccessCriteriaId is NFT and nftContract is 0x0 + │ └── When user attempts to set access criteria + │ └── Then it should revert │ - ├── Given AccessCriteriaId is MERKLE and merkleRoot is 0x0 - │ └── When user attempts to set access criteria - │ └── Then it should revert + ├── Given AccessCriteriaId is MERKLE and merkleRoot is 0x0 + │ └── When user attempts to set access criteria + │ └── Then it should revert │ - ├── Given AccessCriteriaId is LIST and allowedAddresses is empty - │ └── When user attempts to set access criteria - │ └── Then it should revert + ├── Given AccessCriteriaId is LIST and allowedAddresses is empty + │ └── When user attempts to set access criteria + │ └── Then it should revert │ - └── Given all the valid parameters are provided - └── When user attempts to set access criteria - └── Then it should not revert + └── Given all the valid parameters are provided + └── When user attempts to set access criteria + └── Then it should not revert */ function testFuzzSetAccessCriteria_revertsGivenUserDoesNotHaveFundingPotAdminRole( @@ -1072,7 +1072,6 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { _helper_setupRoundWithAccessCriteria(accessCriteriaEnum); uint64 roundId = fundingPot.getRoundCount(); - uint8 accessCriteriaId = 0; uint amount = 250; // Warp to make the round active @@ -1085,7 +1084,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { vm.prank(contributor1_); fundingPot.contributeToRound( - roundId, 10, accessCriteriaId, new bytes32[](0) + roundId, 10, accessCriteriaEnum, new bytes32[](0) ); vm.expectRevert( @@ -1098,7 +1097,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { vm.prank(contributor1_); fundingPot.contributeToRound( - roundId, amount, accessCriteriaId, new bytes32[](0) + roundId, amount, accessCriteriaEnum, new bytes32[](0) ); } @@ -1180,19 +1179,16 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { function testContributeToRound_revertsGivenNFTAccessCriteriaIsNotMet() public { - testCreateRound(); + uint8 accessId = 2; + _helper_setupRoundWithAccessCriteria(accessId); uint64 roundId = fundingPot.getRoundCount(); - uint8 accessId = 1; + uint amount = 250; - ILM_PC_FundingPot_v1.AccessCriteria memory accessCriteria = - _helper_createAccessCriteria(accessId); - - fundingPot.setAccessCriteriaForRound(roundId, accessCriteria); - _helper_callSetAccessCriteriaPrivileges( - roundId, accessId, 500, 10, 0, 0, false, 0, 0, 0 - ); + // _helper_callSetAccessCriteriaPrivileges( + // roundId, accessId, 500, 10, 0, 0, false, 0, 0, 0 + // ); (uint roundStart,,,,,,) = fundingPot.getRoundGenericParameters(roundId); vm.warp(roundStart + 1); @@ -1201,6 +1197,8 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { vm.prank(contributor1_); _token.approve(address(fundingPot), amount); + mockNFTContract.balanceOf(contributor1_); + vm.expectRevert( abi.encodeWithSelector( ILM_PC_FundingPot_v1 @@ -1220,7 +1218,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { testCreateRound(); uint64 roundId = fundingPot.getRoundCount(); - uint8 accessId = 2; + uint8 accessId = 3; uint amount = 250; ILM_PC_FundingPot_v1.AccessCriteria memory accessCriteria = @@ -1255,7 +1253,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { testCreateRound(); uint64 roundId = fundingPot.getRoundCount(); - uint8 accessId = 3; + uint8 accessId = 4; uint amount = 250; ILM_PC_FundingPot_v1.AccessCriteria memory accessCriteria = @@ -1747,13 +1745,14 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { function testFuzz_validateAccessCriteria( uint64 roundId_, uint8 accessId_, - bytes32[] calldata merkleProof_ + bytes32[] calldata merkleProof_, + address user_ ) external { vm.assume(roundId_ <= fundingPot.getRoundCount() + 1); vm.assume(accessId_ <= 4); try fundingPot.exposed_validateAccessCriteria( - roundId_, accessId_, merkleProof_ + roundId_, accessId_, merkleProof_, msg.sender ) { assert(true); } catch (bytes memory) { diff --git a/test/unit/modules/logicModule/LM_PC_FundingPot_v1_Exposed.sol b/test/unit/modules/logicModule/LM_PC_FundingPot_v1_Exposed.sol index 3321ff921..7dd2d2e05 100644 --- a/test/unit/modules/logicModule/LM_PC_FundingPot_v1_Exposed.sol +++ b/test/unit/modules/logicModule/LM_PC_FundingPot_v1_Exposed.sol @@ -85,10 +85,11 @@ contract LM_PC_FundingPot_v1_Exposed is LM_PC_FundingPot_v1 { uint64 roundId_, uint8 accessCriteriaId_, bytes32[] calldata merkleProof_, - uint amount_ + uint amount_, + address user_ ) external view returns (uint) { return _validateRoundContribution( - roundId_, accessCriteriaId_, merkleProof_, amount_ + roundId_, accessCriteriaId_, merkleProof_, amount_, user_ ); } @@ -112,9 +113,10 @@ contract LM_PC_FundingPot_v1_Exposed is LM_PC_FundingPot_v1 { function exposed_validateAccessCriteria( uint64 roundId_, uint8 accessId_, - bytes32[] calldata merkleProof_ + bytes32[] calldata merkleProof_, + address user_ ) external view { - _validateAccessCriteria(roundId_, accessId_, merkleProof_); + _validateAccessCriteria(roundId_, accessId_, merkleProof_, user_); } /** @@ -149,15 +151,4 @@ contract LM_PC_FundingPot_v1_Exposed is LM_PC_FundingPot_v1 { ) external pure returns (bool) { return _validateMerkleProof(root_, merkleProof_, user_, roundId_); } - - /** - * @notice Exposes the internal _recordContribution function for testing - */ - function exposed_recordContribution( - uint64 roundId_, - address user_, - uint amount_ - ) external { - _recordContribution(roundId_, user_, amount_); - } } From 2ca7684e2de56e04dcc482934ea015538102bd1c Mon Sep 17 00:00:00 2001 From: Leandro Faria Date: Mon, 7 Apr 2025 10:26:00 -0500 Subject: [PATCH 047/130] fix: fix broken tests --- .../logicModule/LM_PC_FundingPot_v1.sol | 34 ++++++------------- .../interfaces/ILM_PC_FundingPot_v1.sol | 8 ----- .../logicModule/LM_PC_FundingPot_v1.t.sol | 28 ++++++--------- 3 files changed, 22 insertions(+), 48 deletions(-) diff --git a/src/modules/logicModule/LM_PC_FundingPot_v1.sol b/src/modules/logicModule/LM_PC_FundingPot_v1.sol index 805b25e00..577ba876a 100644 --- a/src/modules/logicModule/LM_PC_FundingPot_v1.sol +++ b/src/modules/logicModule/LM_PC_FundingPot_v1.sol @@ -90,8 +90,6 @@ contract LM_PC_FundingPot_v1 is /// @notice Stores all funding rounds by their unique ID. mapping(uint64 => Round) private rounds; - /// @notice Stores the access criteria ID for each round. - mapping(uint64 => uint8) private roundIdtoAccessId; /// @notice Stores all access criteria privilages by their unique ID. mapping( uint64 roundId => mapping(uint8 accessId => AccessCriteriaPrivileges) @@ -245,15 +243,6 @@ contract LM_PC_FundingPot_v1 is return roundCount; } - /// @inheritdoc ILM_PC_FundingPot_v1 - function getRoundAccessCriteriaCount(uint64 roundId_) - public - view - returns (uint8 accessCriteriaCount_) - { - return roundIdtoAccessId[roundId_]; - } - /// @inheritdoc ILM_PC_FundingPot_v1 function isRoundClosed(uint64 roundId_) external view returns (bool) { return roundClosed[roundId_]; @@ -363,12 +352,10 @@ contract LM_PC_FundingPot_v1 is ) { revert Module__LM_PC_FundingPot__MissingRequiredAccessCriteriaData(); } - uint8 accessCriteriaId = roundIdtoAccessId[roundId_]; - round.accessCriterias[accessCriteriaId] = accessCriteria_; - - emit AccessCriteriaSet(roundId_, accessCriteriaId, accessCriteria_); + uint8 accessID = uint8(accessCriteria_.accessCriteriaType); + round.accessCriterias[accessID] = accessCriteria_; - roundIdtoAccessId[roundId_] += 1; + emit AccessCriteriaSet(roundId_, accessID, accessCriteria_); } /// @inheritdoc ILM_PC_FundingPot_v1 @@ -377,10 +364,10 @@ contract LM_PC_FundingPot_v1 is uint8 accessCriteriaId_, AccessCriteria memory accessCriteria_ ) external onlyModuleRole(FUNDING_POT_ADMIN_ROLE) { - if (accessCriteriaId_ >= roundIdtoAccessId[roundId_]) { + Round storage round = rounds[roundId_]; + if (accessCriteriaId_ > 4) { revert Module__LM_PC_FundingPot__InvalidAccessCriteriaId(); } - Round storage round = rounds[roundId_]; _validateEditRoundParameters(round); @@ -714,19 +701,20 @@ contract LM_PC_FundingPot_v1 is /// @notice Validates access criteria for a specific round and access type /// @dev Checks if a user meets the access requirements based on the round's access criteria /// @param roundId_ The ID of the round being validated - /// @param accessId_ The ID of the specific access criteria + /// @param accessCriteriaId_ The ID of the specific access criteria /// @param merkleProof_ Merkle proof for Merkle tree-based access (optional) function _validateAccessCriteria( uint64 roundId_, - uint8 accessId_, + uint8 accessCriteriaId_, bytes32[] calldata merkleProof_, address user_ ) internal view { Round storage round = rounds[roundId_]; - AccessCriteria storage accessCriteria = round.accessCriterias[accessId_]; + AccessCriteria storage accessCriteria = + round.accessCriterias[accessCriteriaId_]; - if (accessCriteria.accessCriteriaType == AccessCriteriaType.OPEN) { - return; + if (accessCriteriaId_ > 4) { + revert Module__LM_PC_FundingPot__InvalidAccessCriteriaId(); } bool accessGranted = false; diff --git a/src/modules/logicModule/interfaces/ILM_PC_FundingPot_v1.sol b/src/modules/logicModule/interfaces/ILM_PC_FundingPot_v1.sol index 19561cf10..ae446cbcd 100644 --- a/src/modules/logicModule/interfaces/ILM_PC_FundingPot_v1.sol +++ b/src/modules/logicModule/interfaces/ILM_PC_FundingPot_v1.sol @@ -296,14 +296,6 @@ interface ILM_PC_FundingPot_v1 is IERC20PaymentClientBase_v2 { /// @return roundCount_ The total number of funding rounds. function getRoundCount() external view returns (uint64 roundCount_); - /// @notice Retrieves the total number of access criteria for a specific round. - /// @param roundId_ The unique identifier of the round. - /// @return accessCriteriaCount_ The total number of access criteria for the round. - function getRoundAccessCriteriaCount(uint64 roundId_) - external - view - returns (uint8 accessCriteriaCount_); - /// @notice Retrieves the closed status of a round /// @param roundId_ The ID of the round /// @return The closed status of the round diff --git a/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol b/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol index 39bcd6b26..82277f172 100644 --- a/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol +++ b/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol @@ -545,13 +545,14 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { ); } - function testEditRound_revertsGivenRoundStartIsInThePast(uint roundStartP_) + function testEditRound_revertsGivenRoundStartIsInThePast(uint roundStart_) public { testCreateRound(); uint64 roundId = fundingPot.getRoundCount(); _editedRoundParams; - vm.assume(roundStartP_ < block.timestamp); + vm.assume(roundStart_ < block.timestamp); + _editedRoundParams.roundStart = roundStart_; vm.expectRevert( abi.encodeWithSelector( @@ -1436,7 +1437,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { fundingPot.setAccessCriteriaForRound(roundId, accessCriteria); _helper_callSetAccessCriteriaPrivileges( - roundId, accessId, 500, 0, 0, 0, false, 0, 0, 0 + roundId, accessId, 200, 0, 0, 0, false, 0, 0, 0 ); (uint roundStart,, uint roundCap,,,,) = @@ -1658,8 +1659,8 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { // Round 2 with a different cap uint round2Cap = 500; fundingPot.createRound( - _defaultRoundParams.roundStart, - _defaultRoundParams.roundEnd, + _defaultRoundParams.roundStart + 3 days, + _defaultRoundParams.roundEnd + 3 days, round2Cap, _defaultRoundParams.hookContract, _defaultRoundParams.hookFunction, @@ -1688,10 +1689,6 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { // Move to Round 2 vm.warp(_defaultRoundParams.roundStart + 3 days + 1); - uint unusedCapacityFromRound1 = _defaultRoundParams.roundCap - - fundingPot.exposed_getTotalRoundContributions(round1Id); - uint totalAvailableCapacityRound2 = round2Cap + unusedCapacityFromRound1; - // Round 2: Contributors try to use the accumulated capacity vm.startPrank(contributor2_); @@ -1700,8 +1697,8 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { vm.stopPrank(); vm.startPrank(contributor3_); - _token.approve(address(fundingPot), 600); - fundingPot.contributeToRound(round2Id, 600, accessId, new bytes32[](0)); + _token.approve(address(fundingPot), 300); + fundingPot.contributeToRound(round2Id, 300, accessId, new bytes32[](0)); vm.stopPrank(); // Verify Round 1 contributions @@ -1720,7 +1717,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { ); // Verify Round 2 contributions - assertEq(fundingPot.exposed_getTotalRoundContributions(round2Id), 1000); + assertEq(fundingPot.exposed_getTotalRoundContributions(round2Id), 700); assertEq( fundingPot.exposed_getUserContributionToRound( round2Id, contributor2_ @@ -1731,13 +1728,10 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { fundingPot.exposed_getUserContributionToRound( round2Id, contributor3_ ), - 600 + 300 ); - assertEq( - fundingPot.exposed_getTotalRoundContributions(round2Id), - totalAvailableCapacityRound2 - ); + assertEq(fundingPot.exposed_getTotalRoundContributions(round2Id), 700); } // ------------------------------------------------------------------------- From c9840ba6d3b211b573851a8be08be1210eca8d81 Mon Sep 17 00:00:00 2001 From: Leandro Faria Date: Mon, 7 Apr 2025 10:38:13 -0500 Subject: [PATCH 048/130] fix:fix all failing test --- .../logicModule/LM_PC_FundingPot_v1.t.sol | 25 +++++++------------ 1 file changed, 9 insertions(+), 16 deletions(-) diff --git a/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol b/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol index 82277f172..aec1bc553 100644 --- a/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol +++ b/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol @@ -953,7 +953,6 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { testCreateRound(); uint64 roundId = fundingPot.getRoundCount(); - uint8 accessCriteriaId = 0; ILM_PC_FundingPot_v1.AccessCriteria memory accessCriteria = _helper_createAccessCriteria(accessCriteriaEnum); @@ -965,7 +964,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { address nftContract, bytes32 merkleRoot, address[] memory allowedAddresses - ) = fundingPot.getRoundAccessCriteria(roundId, accessCriteriaId); + ) = fundingPot.getRoundAccessCriteria(roundId, accessCriteriaEnum); assertEq(isOpen, accessCriteriaEnum == 1); assertEq(nftContract, accessCriteria.nftContract); @@ -1071,35 +1070,29 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { ) public { vm.assume(accessCriteriaEnum >= 0 && accessCriteriaEnum <= 4); + // Set up a round with access criteria _helper_setupRoundWithAccessCriteria(accessCriteriaEnum); uint64 roundId = fundingPot.getRoundCount(); - uint amount = 250; // Warp to make the round active (uint roundStart,,,,,,) = fundingPot.getRoundGenericParameters(roundId); vm.warp(roundStart + 1); - // Approve - vm.prank(contributor1_); - _token.approve(address(fundingPot), 110); - - vm.prank(contributor1_); - fundingPot.contributeToRound( - roundId, 10, accessCriteriaEnum, new bytes32[](0) - ); + // Create a new access criteria to try to edit with + ILM_PC_FundingPot_v1.AccessCriteria memory newAccessCriteria = + _helper_createAccessCriteria((accessCriteriaEnum + 1) % 5); // Use a different access criteria type + // Expect revert when trying to edit access criteria for an active round vm.expectRevert( abi.encodeWithSelector( ILM_PC_FundingPot_v1 - .Module__LM_PC_FundingPot__PersonalCapReached + .Module__LM_PC_FundingPot__RoundAlreadyStarted .selector ) ); - vm.prank(contributor1_); - fundingPot.contributeToRound( - roundId, amount, accessCriteriaEnum, new bytes32[](0) - ); + // Attempt to edit the access criteria for the active round + fundingPot.editAccessCriteriaForRound(roundId, 0, newAccessCriteria); } function testContributeToRound_revertsGivenContributionIsBeforeRoundStart() From dfaf4d39f6561219dbd0110c42d5cb8947e1d718 Mon Sep 17 00:00:00 2001 From: Leandro Faria Date: Mon, 7 Apr 2025 11:41:39 -0500 Subject: [PATCH 049/130] fix:rename accessId to accessCriteriaId --- .../logicModule/LM_PC_FundingPot_v1.sol | 44 +++++++++++-------- 1 file changed, 25 insertions(+), 19 deletions(-) diff --git a/src/modules/logicModule/LM_PC_FundingPot_v1.sol b/src/modules/logicModule/LM_PC_FundingPot_v1.sol index 577ba876a..17d89c654 100644 --- a/src/modules/logicModule/LM_PC_FundingPot_v1.sol +++ b/src/modules/logicModule/LM_PC_FundingPot_v1.sol @@ -92,7 +92,8 @@ contract LM_PC_FundingPot_v1 is /// @notice Stores all access criteria privilages by their unique ID. mapping( - uint64 roundId => mapping(uint8 accessId => AccessCriteriaPrivileges) + uint64 roundId + => mapping(uint8 accessCriteriaId_ => AccessCriteriaPrivileges) ) private accessCriteriaPrivileges; /// @notice Maps round IDs to user addresses to contribution amounts @@ -205,7 +206,10 @@ contract LM_PC_FundingPot_v1 is } /// @inheritdoc ILM_PC_FundingPot_v1 - function getRoundAccessCriteriaPrivileges(uint64 roundId_, uint8 accessId_) + function getRoundAccessCriteriaPrivileges( + uint64 roundId_, + uint8 accessCriteriaId__ + ) external view returns ( @@ -218,7 +222,8 @@ contract LM_PC_FundingPot_v1 is ) { Round storage round = rounds[roundId_]; - AccessCriteria storage accessCriteria = round.accessCriterias[accessId_]; + AccessCriteria storage accessCriteria = + round.accessCriterias[accessCriteriaId__]; if (accessCriteria.accessCriteriaType == AccessCriteriaType.OPEN) { return (true, 0, false, 0, 0, 0); @@ -226,7 +231,7 @@ contract LM_PC_FundingPot_v1 is // Store the privileges in a local variable to reduce stack usage. AccessCriteriaPrivileges storage privs = - accessCriteriaPrivileges[roundId_][accessId_]; + accessCriteriaPrivileges[roundId_][accessCriteriaId__]; return ( false, @@ -352,10 +357,10 @@ contract LM_PC_FundingPot_v1 is ) { revert Module__LM_PC_FundingPot__MissingRequiredAccessCriteriaData(); } - uint8 accessID = uint8(accessCriteria_.accessCriteriaType); - round.accessCriterias[accessID] = accessCriteria_; + uint8 accessCriteriaId_ = uint8(accessCriteria_.accessCriteriaType); + round.accessCriterias[accessCriteriaId_] = accessCriteria_; - emit AccessCriteriaSet(roundId_, accessID, accessCriteria_); + emit AccessCriteriaSet(roundId_, accessCriteriaId_, accessCriteria_); } /// @inheritdoc ILM_PC_FundingPot_v1 @@ -379,7 +384,7 @@ contract LM_PC_FundingPot_v1 is /// @inheritdoc ILM_PC_FundingPot_v1 function setAccessCriteriaPrivileges( uint64 roundId_, - uint8 accessId_, + uint8 accessCriteriaId__, uint personalCap_, uint capByNFT_, uint capByMerkle_, @@ -396,14 +401,14 @@ contract LM_PC_FundingPot_v1 is _validateEditRoundParameters(round); if ( - round.accessCriterias[accessId_].accessCriteriaType + round.accessCriterias[accessCriteriaId__].accessCriteriaType == AccessCriteriaType.OPEN ) { highestCap = personalCap_; } if ( - round.accessCriterias[accessId_].accessCriteriaType + round.accessCriterias[accessCriteriaId__].accessCriteriaType == AccessCriteriaType.NFT && capByNFT_ > 0 ) { uint nftCap = personalCap_ + capByNFT_; @@ -413,7 +418,7 @@ contract LM_PC_FundingPot_v1 is } if ( - round.accessCriterias[accessId_].accessCriteriaType + round.accessCriterias[accessCriteriaId__].accessCriteriaType == AccessCriteriaType.MERKLE && capByMerkle_ > 0 ) { uint merkleCap = personalCap_ + capByMerkle_; @@ -423,7 +428,7 @@ contract LM_PC_FundingPot_v1 is } if ( - round.accessCriterias[accessId_].accessCriteriaType + round.accessCriterias[accessCriteriaId__].accessCriteriaType == AccessCriteriaType.LIST && capByList_ > 0 ) { uint listCap = personalCap_ + capByList_; @@ -437,7 +442,7 @@ contract LM_PC_FundingPot_v1 is } AccessCriteriaPrivileges storage accessCriteriaPrivileges = - accessCriteriaPrivileges[roundId_][accessId_]; + accessCriteriaPrivileges[roundId_][accessCriteriaId__]; accessCriteriaPrivileges.personalCap = personalCap_; accessCriteriaPrivileges.overrideContributionSpan = @@ -448,7 +453,7 @@ contract LM_PC_FundingPot_v1 is emit AccessCriteriaPrivilegesSet( roundId_, - accessId_, + accessCriteriaId__, personalCap_, overrideContributionSpan_, start_, @@ -642,7 +647,7 @@ contract LM_PC_FundingPot_v1 is function _validateAndAdjustCaps( uint64 roundId_, uint amount_, - uint8 accessId_, + uint8 accessCriteriaId__, bool canOverrideContributionSpan_ ) internal view returns (uint adjustedAmount) { adjustedAmount = amount_; @@ -684,8 +689,9 @@ contract LM_PC_FundingPot_v1 is // Check and adjust for personal cap uint userPreviousContribution = _getUserContributionToRound(roundId_, msg.sender); - uint userPersonalCap = - _getUserPersonalCapForRound(roundId_, accessId_, msg.sender); + uint userPersonalCap = _getUserPersonalCapForRound( + roundId_, accessCriteriaId__, msg.sender + ); if (userPreviousContribution + adjustedAmount > userPersonalCap) { if (userPreviousContribution < userPersonalCap) { @@ -766,11 +772,11 @@ contract LM_PC_FundingPot_v1 is /// @return The personal contribution cap for the user function _getUserPersonalCapForRound( uint64 roundId_, - uint8 accessId_, + uint8 accessCriteriaId__, address user_ ) internal view returns (uint) { AccessCriteriaPrivileges storage privileges = - accessCriteriaPrivileges[roundId_][accessId_]; + accessCriteriaPrivileges[roundId_][accessCriteriaId__]; uint personalCap = privileges.personalCap; From bb6dc44d18fdf0c64852c8f0fe1bbeccc56d75dc Mon Sep 17 00:00:00 2001 From: Leandro Faria Date: Mon, 7 Apr 2025 16:03:58 -0500 Subject: [PATCH 050/130] fix: fix warning --- .../logicModule/LM_PC_FundingPot_v1.sol | 80 ++++++++++++------- .../logicModule/LM_PC_FundingPot_v1.t.sol | 49 +++--------- .../LM_PC_FundingPot_v1_Exposed.sol | 20 ----- 3 files changed, 59 insertions(+), 90 deletions(-) diff --git a/src/modules/logicModule/LM_PC_FundingPot_v1.sol b/src/modules/logicModule/LM_PC_FundingPot_v1.sol index 17d89c654..6e231a509 100644 --- a/src/modules/logicModule/LM_PC_FundingPot_v1.sol +++ b/src/modules/logicModule/LM_PC_FundingPot_v1.sol @@ -84,6 +84,9 @@ contract LM_PC_FundingPot_v1 is /// @notice The payment processor flag for the end timestamp. uint8 internal constant FLAG_END = 3; + /// @notice The maximum valid access criteria ID. + uint8 internal constant MAX_ACCESS_CRITERIA_ID = 4; + // ------------------------------------------------------------------------- // State @@ -230,16 +233,16 @@ contract LM_PC_FundingPot_v1 is } // Store the privileges in a local variable to reduce stack usage. - AccessCriteriaPrivileges storage privs = + AccessCriteriaPrivileges storage priviledges = accessCriteriaPrivileges[roundId_][accessCriteriaId__]; return ( false, - privs.personalCap, - privs.overrideContributionSpan, - privs.start, - privs.cliff, - privs.end + priviledges.personalCap, + priviledges.overrideContributionSpan, + priviledges.start, + priviledges.cliff, + priviledges.end ); } @@ -370,7 +373,7 @@ contract LM_PC_FundingPot_v1 is AccessCriteria memory accessCriteria_ ) external onlyModuleRole(FUNDING_POT_ADMIN_ROLE) { Round storage round = rounds[roundId_]; - if (accessCriteriaId_ > 4) { + if (accessCriteriaId_ > MAX_ACCESS_CRITERIA_ID) { revert Module__LM_PC_FundingPot__InvalidAccessCriteriaId(); } @@ -396,7 +399,7 @@ contract LM_PC_FundingPot_v1 is ) external onlyModuleRole(FUNDING_POT_ADMIN_ROLE) { Round storage round = rounds[roundId_]; - uint highestCap = 0; + uint highestCap; _validateEditRoundParameters(round); @@ -600,7 +603,7 @@ contract LM_PC_FundingPot_v1 is ) internal view returns (uint adjustedAmount) { Round storage round = rounds[roundId_]; uint currentTime = block.timestamp; - uint adjustedAmount = amount_; + adjustedAmount = amount_; if (amount_ == 0) { revert Module__LM_PC_FundingPot__InvalidDepositAmount(); @@ -661,17 +664,8 @@ contract LM_PC_FundingPot_v1 is // If global accumulative caps are enabled, // adjust the round cap to acommodate unused capacity from previous rounds if (round.globalAccumulativeCaps) { - uint unusedCapacityFromPrevious = 0; - for (uint64 i = 1; i < roundId_; ++i) { - Round storage prevRound = rounds[i]; - if (!prevRound.globalAccumulativeCaps) continue; - - uint prevRoundTotal = _getTotalRoundContribution(i); - if (prevRoundTotal < prevRound.roundCap) { - unusedCapacityFromPrevious += - (prevRound.roundCap - prevRoundTotal); - } - } + uint unusedCapacityFromPrevious = + _calculateUnusedCapacityFromPreviousRounds(roundId_); effectiveRoundCap += unusedCapacityFromPrevious; } @@ -704,40 +698,66 @@ contract LM_PC_FundingPot_v1 is return adjustedAmount; } + /// @notice Calculates unused capacity from previous rounds + /// @param roundId_ The ID of the current round + /// @return unusedCapacityFromPrevious The total unused capacity from previous rounds + function _calculateUnusedCapacityFromPreviousRounds(uint64 roundId_) + internal + view + returns (uint unusedCapacityFromPrevious) + { + unusedCapacityFromPrevious = 0; + // Iterate through all previous rounds (1 to roundId_-1) + for (uint64 i = 1; i < roundId_; ++i) { + Round storage prevRound = rounds[i]; + if (!prevRound.globalAccumulativeCaps) continue; + + uint prevRoundTotal = _getTotalRoundContribution(i); + if (prevRoundTotal < prevRound.roundCap) { + unusedCapacityFromPrevious += + (prevRound.roundCap - prevRoundTotal); + } + } + return unusedCapacityFromPrevious; + } + /// @notice Validates access criteria for a specific round and access type /// @dev Checks if a user meets the access requirements based on the round's access criteria /// @param roundId_ The ID of the round being validated /// @param accessCriteriaId_ The ID of the specific access criteria /// @param merkleProof_ Merkle proof for Merkle tree-based access (optional) + /// @param user_ The address of the user to validate + /// @return True if the user meets the access criteria, reverts otherwise function _validateAccessCriteria( uint64 roundId_, uint8 accessCriteriaId_, bytes32[] calldata merkleProof_, address user_ - ) internal view { + ) internal view returns (bool) { Round storage round = rounds[roundId_]; AccessCriteria storage accessCriteria = round.accessCriterias[accessCriteriaId_]; - if (accessCriteriaId_ > 4) { + if (accessCriteriaId_ > MAX_ACCESS_CRITERIA_ID) { revert Module__LM_PC_FundingPot__InvalidAccessCriteriaId(); } - bool accessGranted = false; if (accessCriteria.accessCriteriaType == AccessCriteriaType.NFT) { - accessGranted = - _checkNftOwnership(accessCriteria.nftContract, user_); + return _checkNftOwnership(accessCriteria.nftContract, user_); } else if ( accessCriteria.accessCriteriaType == AccessCriteriaType.MERKLE ) { - accessGranted = _validateMerkleProof( + return _validateMerkleProof( accessCriteria.merkleRoot, merkleProof_, user_, roundId_ ); } else if (accessCriteria.accessCriteriaType == AccessCriteriaType.LIST) { - accessGranted = + return _checkAllowedAddressList(accessCriteria.allowedAddresses, user_); } + + // For OPEN access criteria type, no validation needed + return true; } /// @notice Retrieves the total contribution for a specific round @@ -806,10 +826,8 @@ contract LM_PC_FundingPot_v1 is uint personalCap = 0; - for (uint8 j = 0; j < 4; ++j) { - AccessCriteria storage accessCriteria = - prevRound.accessCriterias[j]; - + // Iterate through all possible access criteria IDs (0 to MAX_ACCESS_CRITERIA_ID) + for (uint8 j = 0; j <= MAX_ACCESS_CRITERIA_ID; ++j) { AccessCriteriaPrivileges storage privileges = accessCriteriaPrivileges[i][j]; diff --git a/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol b/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol index aec1bc553..bf6ed3e93 100644 --- a/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol +++ b/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol @@ -1112,8 +1112,6 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { roundId, accessId, 500, 0, 0, 0, false, 0, 0, 0 ); - (uint roundStart,,,,,,) = fundingPot.getRoundGenericParameters(roundId); - // Approve vm.prank(contributor1_); _token.approve(address(fundingPot), 500); @@ -1309,11 +1307,6 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { roundId, amount, accessId, new bytes32[](0) ); - // Get the base personal cap from the contract - uint personalCap = fundingPot.exposed_getUserPersonalCapForRound( - roundId, accessId, contributor1_ - ); - // Attempt to contribute beyond personal cap vm.expectRevert( abi.encodeWithSelector( @@ -1324,7 +1317,6 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { ); vm.prank(contributor1_); - uint remainingCap = personalCap - amount; fundingPot.contributeToRound(roundId, 251, accessId, new bytes32[](0)); } @@ -1410,7 +1402,6 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { } function testContributeToRound_worksGivenUserCurrentContributionExceedsTheRoundCap( - uint roundCap_, uint8 accessCriteriaEnumOld, uint8 accessCriteriaEnumNew ) public { @@ -1433,8 +1424,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { roundId, accessId, 200, 0, 0, 0, false, 0, 0, 0 ); - (uint roundStart,, uint roundCap,,,,) = - fundingPot.getRoundGenericParameters(roundId); + (uint roundStart,,,,,,) = fundingPot.getRoundGenericParameters(roundId); vm.warp(roundStart + 1); // Approve @@ -1732,9 +1722,8 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { function testFuzz_validateAccessCriteria( uint64 roundId_, uint8 accessId_, - bytes32[] calldata merkleProof_, - address user_ - ) external { + bytes32[] calldata merkleProof_ + ) external view { vm.assume(roundId_ <= fundingPot.getRoundCount() + 1); vm.assume(accessId_ <= 4); @@ -1757,11 +1746,6 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { vm.assume(amount_ <= 1000); vm.assume(accessId_ <= 4); - uint initialTotalContribution = - fundingPot.exposed_getTotalRoundContributions(roundId_); - uint initialUserContribution = - fundingPot.exposed_getUserContributionToRound(roundId_, msg.sender); - try fundingPot.exposed_validateAndAdjustCaps( roundId_, amount_, accessId_, canOverrideContributionSpan_ ) returns (uint adjustedAmount) { @@ -1787,12 +1771,6 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { !canOverrideContributionSpan_, "Should not revert RoundCapReached when canOverrideContributionSpan is true" ); - // Additional assertions commented out for now - // assert(roundCap > 0, "Round cap should be > 0"); - // assert( - // initialTotalContribution >= effectiveRoundCap, - // "Total contribution should be >= effectiveRoundCap" - // ); } else if (keccak256(reason) == personalCapReachedSelector) { // We expect this sometimes assertTrue(true, "Personal cap reached as expected"); @@ -1805,14 +1783,6 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { // ------------------------------------------------------------------------- // Helper Functions - // @notice Creates a default funding round - function _helper_createDefaultFundingRound(uint roundCap_) - internal - returns (RoundParams memory) - { - return _defaultRoundParams; - } - // @notice Creates edit round parameters with customizable values function _helper_createEditRoundParams( uint roundStart_, @@ -1822,7 +1792,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { bytes memory hookFunction_, bool autoClosure_, bool globalAccumulativeCaps_ - ) internal returns (RoundParams memory) { + ) internal pure returns (RoundParams memory) { return RoundParams({ roundStart: roundStart_, roundEnd: roundEnd_, @@ -1836,14 +1806,15 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { function _helper_createAccessCriteria(uint8 accessCriteriaEnum) internal - returns (ILM_PC_FundingPot_v1.AccessCriteria memory) + view + returns (ILM_PC_FundingPot_v1.AccessCriteria memory accessCriteria) { { if ( accessCriteriaEnum == uint8(ILM_PC_FundingPot_v1.AccessCriteriaType.OPEN) ) { - return ILM_PC_FundingPot_v1.AccessCriteria( + accessCriteria = ILM_PC_FundingPot_v1.AccessCriteria( ILM_PC_FundingPot_v1.AccessCriteriaType.OPEN, address(0x0), bytes32(uint(0x0)), @@ -1855,7 +1826,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { ) { address nftContract = address(mockNFTContract); - return ILM_PC_FundingPot_v1.AccessCriteria( + accessCriteria = ILM_PC_FundingPot_v1.AccessCriteria( ILM_PC_FundingPot_v1.AccessCriteriaType.NFT, nftContract, bytes32(uint(0x0)), @@ -1867,7 +1838,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { ) { bytes32 merkleRoot = ROOT; - return ILM_PC_FundingPot_v1.AccessCriteria( + accessCriteria = ILM_PC_FundingPot_v1.AccessCriteria( ILM_PC_FundingPot_v1.AccessCriteriaType.MERKLE, address(0x0), merkleRoot, @@ -1882,7 +1853,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { allowedAddresses[1] = address(0x2); allowedAddresses[2] = address(0x3); - return ILM_PC_FundingPot_v1.AccessCriteria( + accessCriteria = ILM_PC_FundingPot_v1.AccessCriteria( ILM_PC_FundingPot_v1.AccessCriteriaType.LIST, address(0x0), bytes32(uint(0x0)), diff --git a/test/unit/modules/logicModule/LM_PC_FundingPot_v1_Exposed.sol b/test/unit/modules/logicModule/LM_PC_FundingPot_v1_Exposed.sol index 7dd2d2e05..176d5c9aa 100644 --- a/test/unit/modules/logicModule/LM_PC_FundingPot_v1_Exposed.sol +++ b/test/unit/modules/logicModule/LM_PC_FundingPot_v1_Exposed.sol @@ -47,26 +47,6 @@ contract LM_PC_FundingPot_v1_Exposed is LM_PC_FundingPot_v1 { return _getUserUnusedCapacityFromPreviousRounds(user_, currentRoundId_); } - /** - * @notice Exposes the internal _validateRoundParameters function for testing - */ - // function exposed_validateRoundParameters(Round memory round_) - // external - // view - // { - // _validateRoundParameters(round_); - // } - - /** - * @notice Exposes the internal _validateEditRoundParameters function for testing - */ - // function exposed_validateEditRoundParameters(Round storage round_) - // external - // view - // { - // _validateEditRoundParameters(round_); - // } - /** * @notice Exposes the internal _validTimes function for testing */ From 4f94b42e5f8fbeae1106ddbd2375d996e0c85876 Mon Sep 17 00:00:00 2001 From: Leandro Faria Date: Mon, 7 Apr 2025 17:35:58 -0500 Subject: [PATCH 051/130] fix:remove unecessary helper function --- .../interfaces/ILM_PC_FundingPot_v1.sol | 27 +++++---- .../logicModule/LM_PC_FundingPot_v1.t.sol | 56 +++++-------------- 2 files changed, 30 insertions(+), 53 deletions(-) diff --git a/src/modules/logicModule/interfaces/ILM_PC_FundingPot_v1.sol b/src/modules/logicModule/interfaces/ILM_PC_FundingPot_v1.sol index ae446cbcd..8d1543efc 100644 --- a/src/modules/logicModule/interfaces/ILM_PC_FundingPot_v1.sol +++ b/src/modules/logicModule/interfaces/ILM_PC_FundingPot_v1.sol @@ -109,23 +109,27 @@ interface ILM_PC_FundingPot_v1 is IERC20PaymentClientBase_v2 { /// @notice Emitted when access criteria is set for a round. /// @param roundId_ The unique identifier of the round. - /// @param accessId_ The identifier of the access criteria. + /// @param AccessCriteriaId The identifier of the access criteria. /// @param accessCriteria_ The access criteria. event AccessCriteriaSet( - uint64 indexed roundId_, uint8 accessId_, AccessCriteria accessCriteria_ + uint64 indexed roundId_, + uint8 AccessCriteriaId, + AccessCriteria accessCriteria_ ); /// @notice Emitted when access criteria is edited for a round. /// @param roundId_ The unique identifier of the round. - /// @param accessId_ The identifier of the access criteria. + /// @param AccessCriteriaId The identifier of the access criteria. /// @param accessCriteria_ The access criteria. event AccessCriteriaEdited( - uint64 indexed roundId_, uint8 accessId_, AccessCriteria accessCriteria_ + uint64 indexed roundId_, + uint8 AccessCriteriaId, + AccessCriteria accessCriteria_ ); /// @notice Emitted when access criteria Privileges are set for a round. /// @param roundId_ The unique identifier of the round. - /// @param accessId_ The identifier of the access criteria. + /// @param AccessCriteriaId The identifier of the access criteria. /// @param personalCap_ The personal cap for the access criteria. /// @param overrideCap_ Whether to override the global cap. /// @param start_ The start timestamp for the access criteria. @@ -133,7 +137,7 @@ interface ILM_PC_FundingPot_v1 is IERC20PaymentClientBase_v2 { /// @param end_ The end timestamp for the access criteria. event AccessCriteriaPrivilegesSet( uint64 indexed roundId_, - uint8 accessId_, + uint8 AccessCriteriaId, uint personalCap_, bool overrideCap_, uint start_, @@ -273,14 +277,17 @@ interface ILM_PC_FundingPot_v1 is IERC20PaymentClientBase_v2 { /// @notice Retrieves the access criteria Privileges for a specific funding round. /// @param roundId_ The unique identifier of the round. - /// @param accessId_ The identifier of the access criteria. + /// @param AccessCriteriaId The identifier of the access criteria. /// @return isRoundOpen_ Whether the round is open /// @return personalCap_ The personal cap for the access criteria /// @return overrideContributionSpan_ Whether to override the round contribution span /// @return start_ The start timestamp for the access criteria /// @return cliff_ The cliff timestamp for the access criteria /// @return end_ The end timestamp for the access criteria - function getRoundAccessCriteriaPrivileges(uint64 roundId_, uint8 accessId_) + function getRoundAccessCriteriaPrivileges( + uint64 roundId_, + uint8 AccessCriteriaId + ) external view returns ( @@ -368,7 +375,7 @@ interface ILM_PC_FundingPot_v1 is IERC20PaymentClientBase_v2 { /// @notice Set Access Criteria Privileges /// @dev Only callable by funding pot admin and only before the round has started /// @param roundId_ ID of the round - /// @param accessId_ ID of the access criteria + /// @param AccessCriteriaId ID of the access criteria /// @param personalCap_ Personal cap for the access criteria /// @param capByNFT_ Cap by for the NFT access criteria /// @param capByMerkle_ Cap for the Merkle root access criteria @@ -379,7 +386,7 @@ interface ILM_PC_FundingPot_v1 is IERC20PaymentClientBase_v2 { /// @param end_ End timestamp for the access criteria function setAccessCriteriaPrivileges( uint64 roundId_, - uint8 accessId_, + uint8 AccessCriteriaId, uint personalCap_, uint capByNFT_, uint capByMerkle_, diff --git a/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol b/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol index bf6ed3e93..74286492f 100644 --- a/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol +++ b/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol @@ -1108,7 +1108,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { _helper_createAccessCriteria(accessId); fundingPot.setAccessCriteriaForRound(roundId, accessCriteria); - _helper_callSetAccessCriteriaPrivileges( + fundingPot.setAccessCriteriaPrivileges( roundId, accessId, 500, 0, 0, 0, false, 0, 0, 0 ); @@ -1143,7 +1143,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { _helper_createAccessCriteria(accessId); fundingPot.setAccessCriteriaForRound(roundId, accessCriteria); - _helper_callSetAccessCriteriaPrivileges( + fundingPot.setAccessCriteriaPrivileges( roundId, accessId, 500, 0, 0, 0, false, 0, 0, 0 ); @@ -1178,10 +1178,6 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { uint amount = 250; - // _helper_callSetAccessCriteriaPrivileges( - // roundId, accessId, 500, 10, 0, 0, false, 0, 0, 0 - // ); - (uint roundStart,,,,,,) = fundingPot.getRoundGenericParameters(roundId); vm.warp(roundStart + 1); @@ -1217,7 +1213,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { _helper_createAccessCriteria(accessId); fundingPot.setAccessCriteriaForRound(roundId, accessCriteria); - _helper_callSetAccessCriteriaPrivileges( + fundingPot.setAccessCriteriaPrivileges( roundId, accessId, 500, 0, 0, 0, false, 0, 0, 0 ); @@ -1252,7 +1248,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { _helper_createAccessCriteria(accessId); fundingPot.setAccessCriteriaForRound(roundId, accessCriteria); - _helper_callSetAccessCriteriaPrivileges( + fundingPot.setAccessCriteriaPrivileges( roundId, accessId, 500, 0, 0, 0, false, 0, 0, 0 ); @@ -1289,7 +1285,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { _helper_createAccessCriteria(accessId); fundingPot.setAccessCriteriaForRound(roundId, accessCriteria); - _helper_callSetAccessCriteriaPrivileges( + fundingPot.setAccessCriteriaPrivileges( roundId, accessId, 500, 0, 0, 0, false, 0, 0, 0 ); @@ -1374,7 +1370,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { _helper_createAccessCriteria(accessId); fundingPot.setAccessCriteriaForRound(roundId, accessCriteria); - _helper_callSetAccessCriteriaPrivileges( + fundingPot.setAccessCriteriaPrivileges( roundId, accessId, 500, 100, 0, 0, false, 0, 0, 0 ); mockNFTContract.mint(contributor1_); @@ -1420,7 +1416,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { _helper_createAccessCriteria(accessId); fundingPot.setAccessCriteriaForRound(roundId, accessCriteria); - _helper_callSetAccessCriteriaPrivileges( + fundingPot.setAccessCriteriaPrivileges( roundId, accessId, 200, 0, 0, 0, false, 0, 0, 0 ); @@ -1458,7 +1454,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { _helper_createAccessCriteria(accessId); fundingPot.setAccessCriteriaForRound(roundId, accessCriteria); - _helper_callSetAccessCriteriaPrivileges( + fundingPot.setAccessCriteriaPrivileges( roundId, accessId, 500, 0, 0, 0, false, 0, 0, 0 ); @@ -1510,7 +1506,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { fundingPot.setAccessCriteriaForRound(roundId, accessCriteria); // Set privileges with override capability - _helper_callSetAccessCriteriaPrivileges( + fundingPot.setAccessCriteriaPrivileges( roundId, accessId, 500, 200, 0, 0, true, 0, 0, 0 ); @@ -1556,7 +1552,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { ILM_PC_FundingPot_v1.AccessCriteria memory accessCriteria = _helper_createAccessCriteria(1); fundingPot.setAccessCriteriaForRound(round1Id, accessCriteria); - _helper_callSetAccessCriteriaPrivileges( + fundingPot.setAccessCriteriaPrivileges( round1Id, accessId, 500, 0, 0, 0, false, 0, 0, 0 ); mockNFTContract.mint(contributor1_); @@ -1573,7 +1569,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { ); uint64 round2Id = fundingPot.getRoundCount(); fundingPot.setAccessCriteriaForRound(round2Id, accessCriteria); - _helper_callSetAccessCriteriaPrivileges( + fundingPot.setAccessCriteriaPrivileges( round2Id, accessId, 500, 0, 0, 0, false, 0, 0, 0 ); @@ -1635,7 +1631,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { ILM_PC_FundingPot_v1.AccessCriteria memory accessCriteria = _helper_createAccessCriteria(accessId); fundingPot.setAccessCriteriaForRound(round1Id, accessCriteria); - _helper_callSetAccessCriteriaPrivileges( + fundingPot.setAccessCriteriaPrivileges( round1Id, accessId, 500, 0, 0, 0, false, 0, 0, 0 ); @@ -1652,7 +1648,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { ); uint64 round2Id = fundingPot.getRoundCount(); fundingPot.setAccessCriteriaForRound(round2Id, accessCriteria); - _helper_callSetAccessCriteriaPrivileges( + fundingPot.setAccessCriteriaPrivileges( round2Id, accessId, 500, 0, 0, 0, false, 0, 0, 0 ); @@ -1875,30 +1871,4 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { fundingPot.setAccessCriteriaForRound(roundId, accessCriteria); } - - function _helper_callSetAccessCriteriaPrivileges( - uint64 roundId, - uint8 accessId, - uint personalCap, - uint capByNFT, - uint capByMerkle, - uint capByList, - bool canOverrideTimeConstraints, - uint start, - uint cliff, - uint end - ) internal { - fundingPot.setAccessCriteriaPrivileges( - roundId, - accessId, - personalCap, - capByNFT, - capByMerkle, - capByList, - canOverrideTimeConstraints, - start, - cliff, - end - ); - } } From 7f6d04aee139fd0f80a4c1dc743bd9bcfc60d39e Mon Sep 17 00:00:00 2001 From: Leandro Faria Date: Mon, 7 Apr 2025 19:00:20 -0500 Subject: [PATCH 052/130] fix:add tests for exposed functions --- .../logicModule/LM_PC_FundingPot_v1.sol | 38 +- .../logicModule/LM_PC_FundingPot_v1.t.sol | 379 +++++++++++++++++- .../LM_PC_FundingPot_v1_Exposed.sol | 29 ++ 3 files changed, 436 insertions(+), 10 deletions(-) diff --git a/src/modules/logicModule/LM_PC_FundingPot_v1.sol b/src/modules/logicModule/LM_PC_FundingPot_v1.sol index 6e231a509..749dda2f3 100644 --- a/src/modules/logicModule/LM_PC_FundingPot_v1.sol +++ b/src/modules/logicModule/LM_PC_FundingPot_v1.sol @@ -25,15 +25,39 @@ import {ERC165Upgradeable} from import "@oz/utils/cryptography/MerkleProof.sol"; /** - * @title Inverter Funding Pot Module + * @title Inverter Funding Pot Logic Module * - * @notice The module allows project supporters to contribute during funding rounds. + * @notice Manages contribution rounds for fundraising within the Inverter Network, enabling + * configurable access control, contribution limits, and automated distribution. + * Supports multiple concurrent access criteria per round with customizable privileges. * - * @dev Extends {ERC20PaymentClientBase_v2} and implements {ILM_PC_FundingPot_v1}. - * This contract manages funding rounds with configurable parameters including - * start/end times, funding caps, and hook contracts for custom logic. - * Uses timestamps as flags for payment processing via FLAG_START, FLAG_CLIFF, - * and FLAG_END constants. + * @dev Implements a sophisticated round-based funding system with features including: + * - Configurable round parameters (start/end times, caps, hooks) + * - Multiple access criteria types (NFT holding, allowlist, Merkle proof) + * - Customizable privileges per access criteria + * - Global accumulative caps across rounds + * - Automatic and manual round closure mechanisms + * - Hook system for post-round actions + * + * DISCLAIMER: Known Limitations + * 1. Storage Considerations: + * The contract stores significant data per round (access criteria, privileges, + * contributions). While this enables flexible round configuration, it may lead + * to higher gas costs as the number of rounds and contributors increases. + * + * 2. Round Management: + * Rounds cannot be modified once started. This is a security feature but + * requires careful initial configuration. Additionally, rounds must be created + * sequentially and cannot run concurrently. + * + * 3. Access Criteria: + * The contract supports multiple access criteria per round, but each address + * can only contribute under one access criteria type per round. This is to + * prevent double-counting of privileges and caps. + * + * CAUTION: Administrators should carefully consider round configurations, + * particularly when using global accumulative caps and multiple access criteria, + * as these features interact in complex ways that affect contribution limits. * * @custom:security-contact security@inverter.network * In case of any concerns or findings, please refer to our Security Policy diff --git a/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol b/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol index 74286492f..9cc150928 100644 --- a/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol +++ b/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol @@ -1236,7 +1236,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { fundingPot.contributeToRound(roundId, amount, accessId, PROOF); } - function testontributeToRound_revertsGivenAllowedListAccessCriteriaIsNotMet( + function testContributeToRound_revertsGivenAllowedListAccessCriteriaIsNotMet( ) public { testCreateRound(); @@ -1776,6 +1776,372 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { } } + // ------------------------------------------------------------------------- + // Test: _calculateUnusedCapacityFromPreviousRounds + + function test_calculateUnusedCapacityFromPreviousRounds_returnsZeroForFirstRound( + ) public { + // First round should have no unused capacity from previous rounds + uint64 roundId = 1; + uint unusedCapacity = fundingPot + .exposed_calculateUnusedCapacityFromPreviousRounds(roundId); + assertEq(unusedCapacity, 0); + } + + function test_calculateUnusedCapacityFromPreviousRounds_withPartiallyUsedCap( + ) public { + // Create first round with cap 1000 but only use 600 + RoundParams memory params = _defaultRoundParams; + params.roundStart = block.timestamp + 1 days; + params.roundEnd = block.timestamp + 2 days; + params.roundCap = 1000; + params.globalAccumulativeCaps = true; + + uint64 roundId1 = fundingPot.createRound( + params.roundStart, + params.roundEnd, + params.roundCap, + params.hookContract, + params.hookFunction, + params.autoClosure, + params.globalAccumulativeCaps + ); + + // Set access criteria and privileges + uint8 accessId = 0; + ILM_PC_FundingPot_v1.AccessCriteria memory accessCriteria = + _helper_createAccessCriteria(accessId); + fundingPot.setAccessCriteriaForRound(roundId1, accessCriteria); + fundingPot.setAccessCriteriaPrivileges( + roundId1, + accessId, + 1000, // personal cap high enough for test + 0, // no NFT cap + 0, // no merkle cap + 0, // no list cap + false, + 0, // no start + 0, // no cliff + 0 // no end + ); + + // Contribute 600 to first round + vm.warp(params.roundStart + 1); + vm.startPrank(contributor1_); + _token.approve(address(fundingPot), 600); + fundingPot.contributeToRound(roundId1, 600, accessId, new bytes32[](0)); + vm.stopPrank(); + + // Check unused capacity for next round (should be 400) + uint64 roundId2 = 2; + uint unusedCapacity = fundingPot + .exposed_calculateUnusedCapacityFromPreviousRounds(roundId2); + assertEq(unusedCapacity, 400); + } + + function test_calculateUnusedCapacityFromPreviousRounds_withMultipleRounds() + public + { + // Create first round with cap 1000, use 600 + RoundParams memory params = _defaultRoundParams; + params.roundStart = block.timestamp + 1 days; + params.roundEnd = block.timestamp + 2 days; + params.roundCap = 1000; + params.globalAccumulativeCaps = true; + + uint64 roundId1 = fundingPot.createRound( + params.roundStart, + params.roundEnd, + params.roundCap, + params.hookContract, + params.hookFunction, + params.autoClosure, + params.globalAccumulativeCaps + ); + + // Set access criteria for round 1 + uint8 accessId = 0; + ILM_PC_FundingPot_v1.AccessCriteria memory accessCriteria = + _helper_createAccessCriteria(accessId); + fundingPot.setAccessCriteriaForRound(roundId1, accessCriteria); + fundingPot.setAccessCriteriaPrivileges( + roundId1, + accessId, + 1000, // personal cap high enough for the test + 0, // no NFT cap + 0, // no merkle cap + 0, // no list cap + false, + 0, // no start + 0, // no cliff + 0 // no end + ); + + // Warp to round 1 start time and contribute + vm.warp(params.roundStart + 1); + vm.startPrank(contributor1_); + _token.approve(address(fundingPot), 600); + fundingPot.contributeToRound(roundId1, 600, accessId, new bytes32[](0)); + vm.stopPrank(); + + // Create second round with cap 2000 + uint64 roundId2 = fundingPot.createRound( + params.roundStart + 2 days, // Start when first round ends + params.roundStart + 4 days, // End 2 days after start + 2000, // Cap of 2000 + params.hookContract, + params.hookFunction, + params.autoClosure, + params.globalAccumulativeCaps + ); + + // Set access criteria for round 2 + fundingPot.setAccessCriteriaForRound(roundId2, accessCriteria); + fundingPot.setAccessCriteriaPrivileges( + roundId2, + accessId, + 2000, // personal cap high enough for the test + 0, // no NFT cap + 0, // no merkle cap + 0, // no list cap + false, + 0, // no start + 0, // no cliff + 0 // no end + ); + + // Warp to round 2 start time and contribute + vm.warp(params.roundStart + 2 days + 1); + vm.startPrank(contributor2_); + _token.approve(address(fundingPot), 1500); + fundingPot.contributeToRound(roundId2, 1500, accessId, new bytes32[](0)); + vm.stopPrank(); + + // Check unused capacity for third round + // Round 1: 400 unused (1000-600) + // Round 2: 500 unused (2000-1500) + // Total: 900 unused + uint64 roundId3 = 3; + uint unusedCapacity = fundingPot + .exposed_calculateUnusedCapacityFromPreviousRounds(roundId3); + assertEq(unusedCapacity, 900); + } + + function test_calculateUnusedCapacityFromPreviousRounds_withoutGlobalAccumulativeCaps( + ) public { + // Create first round with cap 1000, use 600, but no global accumulative caps + RoundParams memory params = _defaultRoundParams; + params.roundStart = block.timestamp + 1 days; + params.roundEnd = block.timestamp + 2 days; + params.roundCap = 1000; + params.globalAccumulativeCaps = false; + + uint64 roundId1 = fundingPot.createRound( + params.roundStart, + params.roundEnd, + params.roundCap, + params.hookContract, + params.hookFunction, + params.autoClosure, + params.globalAccumulativeCaps + ); + + // Set access criteria and privileges + uint8 accessId = 0; + ILM_PC_FundingPot_v1.AccessCriteria memory accessCriteria = + _helper_createAccessCriteria(accessId); + fundingPot.setAccessCriteriaForRound(roundId1, accessCriteria); + fundingPot.setAccessCriteriaPrivileges( + roundId1, + accessId, + 1000, // personal cap high enough for test + 0, // no NFT cap + 0, // no merkle cap + 0, // no list cap + false, + 0, // no start + 0, // no cliff + 0 // no end + ); + + vm.warp(params.roundStart + 1); + vm.startPrank(contributor1_); + _token.approve(address(fundingPot), 600); + fundingPot.contributeToRound(roundId1, 600, accessId, new bytes32[](0)); + vm.stopPrank(); + + // Should return 0 since global accumulative caps is disabled + uint64 roundId2 = 2; + uint unusedCapacity = fundingPot + .exposed_calculateUnusedCapacityFromPreviousRounds(roundId2); + assertEq(unusedCapacity, 0); + } + + // ------------------------------------------------------------------------- + // Test: _checkRoundClosureConditions + + function test_checkRoundClosureConditions_whenCapReached() public { + RoundParams memory params = _defaultRoundParams; + params.roundStart = block.timestamp + 1 days; + params.roundEnd = block.timestamp + 2 days; + params.roundCap = 1000; + + uint64 roundId = fundingPot.createRound( + params.roundStart, + params.roundEnd, + params.roundCap, + params.hookContract, + params.hookFunction, + params.autoClosure, + params.globalAccumulativeCaps + ); + + // Set access criteria and privileges + uint8 accessId = 0; + ILM_PC_FundingPot_v1.AccessCriteria memory accessCriteria = + _helper_createAccessCriteria(accessId); + fundingPot.setAccessCriteriaForRound(roundId, accessCriteria); + fundingPot.setAccessCriteriaPrivileges( + roundId, + accessId, + 1000, // personal cap equal to round cap + 0, // no NFT cap + 0, // no merkle cap + 0, // no list cap + false, + 0, // no start + 0, // no cliff + 0 // no end + ); + + // Contribute up to the cap + vm.warp(params.roundStart + 1); + vm.startPrank(contributor1_); + _token.approve(address(fundingPot), params.roundCap); + fundingPot.contributeToRound( + roundId, params.roundCap, accessId, new bytes32[](0) + ); + vm.stopPrank(); + + assertTrue(fundingPot.exposed_checkRoundClosureConditions(roundId)); + } + + function test_checkRoundClosureConditions_whenEndTimeReached() public { + RoundParams memory params = _defaultRoundParams; + params.roundStart = block.timestamp + 1 days; + params.roundEnd = block.timestamp + 2 days; + params.roundCap = 1000; + + uint64 roundId = fundingPot.createRound( + params.roundStart, + params.roundEnd, + params.roundCap, + params.hookContract, + params.hookFunction, + params.autoClosure, + params.globalAccumulativeCaps + ); + + // Move time past end time + vm.warp(params.roundEnd + 1); + assertTrue(fundingPot.exposed_checkRoundClosureConditions(roundId)); + } + + function test_checkRoundClosureConditions_whenNeitherConditionMet() + public + { + RoundParams memory params = _defaultRoundParams; + params.roundStart = block.timestamp + 1 days; + params.roundEnd = block.timestamp + 2 days; + params.roundCap = 1000; + + uint64 roundId = fundingPot.createRound( + params.roundStart, + params.roundEnd, + params.roundCap, + params.hookContract, + params.hookFunction, + params.autoClosure, + params.globalAccumulativeCaps + ); + + // Time is before end and no contributions + assertFalse(fundingPot.exposed_checkRoundClosureConditions(roundId)); + } + + function test_checkRoundClosureConditions_withNoEndTime() public { + RoundParams memory params = _defaultRoundParams; + params.roundStart = block.timestamp + 1 days; + params.roundEnd = 0; // No end time + params.roundCap = 1000; + + uint64 roundId = fundingPot.createRound( + params.roundStart, + params.roundEnd, + params.roundCap, + params.hookContract, + params.hookFunction, + params.autoClosure, + params.globalAccumulativeCaps + ); + + // Set access criteria and privileges + uint8 accessId = 0; + ILM_PC_FundingPot_v1.AccessCriteria memory accessCriteria = + _helper_createAccessCriteria(accessId); + fundingPot.setAccessCriteriaForRound(roundId, accessCriteria); + fundingPot.setAccessCriteriaPrivileges( + roundId, + accessId, + 1000, // personal cap equal to round cap + 0, // no NFT cap + 0, // no merkle cap + 0, // no list cap + false, + 0, // no start + 0, // no cliff + 0 // no end + ); + + // Should be false initially + assertFalse(fundingPot.exposed_checkRoundClosureConditions(roundId)); + + // Should be true when cap is reached + vm.warp(params.roundStart + 1); + vm.startPrank(contributor1_); + _token.approve(address(fundingPot), params.roundCap); + fundingPot.contributeToRound( + roundId, params.roundCap, accessId, new bytes32[](0) + ); + vm.stopPrank(); + + assertTrue(fundingPot.exposed_checkRoundClosureConditions(roundId)); + } + + function test_checkRoundClosureConditions_withNoCap() public { + RoundParams memory params = _defaultRoundParams; + params.roundStart = block.timestamp + 1 days; + params.roundEnd = block.timestamp + 2 days; + params.roundCap = 0; // No cap + + uint64 roundId = fundingPot.createRound( + params.roundStart, + params.roundEnd, + params.roundCap, + params.hookContract, + params.hookFunction, + params.autoClosure, + params.globalAccumulativeCaps + ); + + // Should be false before end time + assertFalse(fundingPot.exposed_checkRoundClosureConditions(roundId)); + + // Should be true after end time + vm.warp(params.roundEnd + 1); + assertTrue(fundingPot.exposed_checkRoundClosureConditions(roundId)); + } + // ------------------------------------------------------------------------- // Helper Functions @@ -1863,8 +2229,15 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { function _helper_setupRoundWithAccessCriteria(uint8 accessCriteriaEnum) internal { - testCreateRound(); - uint64 roundId = fundingPot.getRoundCount(); + uint64 roundId = fundingPot.createRound( + _defaultRoundParams.roundStart, + _defaultRoundParams.roundEnd, + _defaultRoundParams.roundCap, + _defaultRoundParams.hookContract, + _defaultRoundParams.hookFunction, + _defaultRoundParams.autoClosure, + _defaultRoundParams.globalAccumulativeCaps + ); ILM_PC_FundingPot_v1.AccessCriteria memory accessCriteria = _helper_createAccessCriteria(accessCriteriaEnum); diff --git a/test/unit/modules/logicModule/LM_PC_FundingPot_v1_Exposed.sol b/test/unit/modules/logicModule/LM_PC_FundingPot_v1_Exposed.sol index 176d5c9aa..9f11775a4 100644 --- a/test/unit/modules/logicModule/LM_PC_FundingPot_v1_Exposed.sol +++ b/test/unit/modules/logicModule/LM_PC_FundingPot_v1_Exposed.sol @@ -131,4 +131,33 @@ contract LM_PC_FundingPot_v1_Exposed is LM_PC_FundingPot_v1 { ) external pure returns (bool) { return _validateMerkleProof(root_, merkleProof_, user_, roundId_); } + + /** + * @notice Exposes the internal _calculateUnusedCapacityFromPreviousRounds function for testing + */ + function exposed_calculateUnusedCapacityFromPreviousRounds(uint64 roundId_) + external + view + returns (uint) + { + return _calculateUnusedCapacityFromPreviousRounds(roundId_); + } + + /** + * @notice Exposes the internal _closeRound function for testing + */ + function exposed_closeRound(uint64 roundId_) external { + _closeRound(roundId_); + } + + /** + * @notice Exposes the internal _checkRoundClosureConditions function for testing + */ + function exposed_checkRoundClosureConditions(uint64 roundId_) + external + view + returns (bool) + { + return _checkRoundClosureConditions(roundId_); + } } From 2c8d2a1f84a637a51c43aa55f1ddcdef6e5af58e Mon Sep 17 00:00:00 2001 From: JeffreyJoel Date: Tue, 8 Apr 2025 11:00:41 +0100 Subject: [PATCH 053/130] test: add gherkin comments to contribution tests --- .../logicModule/LM_PC_FundingPot_v1.t.sol | 36 ++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) diff --git a/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol b/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol index 9cc150928..e54867086 100644 --- a/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol +++ b/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol @@ -1095,6 +1095,40 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { fundingPot.editAccessCriteriaForRound(roundId, 0, newAccessCriteria); } + /* Test: contributeToRound() unhappy paths + ├── Given the round has not started yet + │ └── When the user contributes to the round + │ └── Then the transaction should revert + │ + ├── Given the round has ended + │ └── When the user contributes to the round + │ └── Then the transaction should revert + │ + ├── Given a round has been configured with generic round configuration and access criteria + │ And the round has started + │ And the round has not ended + │ And the user has approved their contribution + │ And the total contribution cap is not yet reached + │ ├── Given the access criteria is an NFT + │ │ └── And the user does not fulfill the access criteria + │ │ └── When the user contributes to the round + │ │ └── Then the transaction should revert + │ │ + │ ├── Given the access criteria is a Merkle Root + │ │ └── And the user does not fulfill the access criteria + │ │ └── When the user contributes to the round + │ │ └── Then the transaction should revert + │ │ + │ ├── Given the access criteria is a List + │ │ └── And the user does not fulfill the access criteria + │ │ └── When the user contributes to the round + │ │ └── Then the transaction should revert + │ │ + │ └── Given a user has already contributed up to their personal cap + │ └── When the user attempts to contribute again + │ └── Then the transaction should revert + */ + function testContributeToRound_revertsGivenContributionIsBeforeRoundStart() public { @@ -1316,7 +1350,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { fundingPot.contributeToRound(roundId, 251, accessId, new bytes32[](0)); } - /* + /* Test: contributeToRound() happy paths ├── Given a round has been configured with generic round configuration and access criteria │ And the round has started │ And the user fulfills the access criteria From 92e1ce15798cf0c3a2b2be7a8597b80741184b5c Mon Sep 17 00:00:00 2001 From: Leandro Faria Date: Wed, 9 Apr 2025 16:53:58 -0500 Subject: [PATCH 054/130] fix:add todos for jeffrey --- .../logicModule/LM_PC_FundingPot_v1.sol | 19 ++++++++++++++----- .../interfaces/ILM_PC_FundingPot_v1.sol | 6 ++++++ .../logicModule/LM_PC_FundingPot_v1.t.sol | 2 ++ 3 files changed, 22 insertions(+), 5 deletions(-) diff --git a/src/modules/logicModule/LM_PC_FundingPot_v1.sol b/src/modules/logicModule/LM_PC_FundingPot_v1.sol index 749dda2f3..66fa7c6b6 100644 --- a/src/modules/logicModule/LM_PC_FundingPot_v1.sol +++ b/src/modules/logicModule/LM_PC_FundingPot_v1.sol @@ -200,16 +200,20 @@ contract LM_PC_FundingPot_v1 is round.globalAccumulativeCaps ); } - + /// TODO should either have a param for a specifc address + /// and return whether they have access or not + /// or should not return anything related to allowed addresses + /// Though I think the first option is better /// @inheritdoc ILM_PC_FundingPot_v1 - function getRoundAccessCriteria(uint64 roundId_, uint8 id_) + + function getRoundAccessCriteria(uint64 roundId_, uint8 id_, address user_) external view returns ( bool isRoundOpen_, address nftContract_, bytes32 merkleRoot_, - address[] memory allowedAddresses_ + bool hasAccess_ ) { Round storage round = rounds[roundId_]; @@ -220,7 +224,8 @@ contract LM_PC_FundingPot_v1 is true, accessCriteria.nftContract, accessCriteria.merkleRoot, - accessCriteria.allowedAddresses + //// This is wrong but gives you an idea of what you need to do + accessCriteria.allowedAddresses[user_] ); } else { return ( @@ -358,8 +363,10 @@ contract LM_PC_FundingPot_v1 is globalAccumulativeCaps_ ); } - + // TODO should take in nft merkle root and list of allowed addresses + // SHould loop through the array and set the access criteria for each address to true in the mapping /// @inheritdoc ILM_PC_FundingPot_v1 + function setAccessCriteriaForRound( uint64 roundId_, AccessCriteria memory accessCriteria_ @@ -369,6 +376,7 @@ contract LM_PC_FundingPot_v1 is _validateEditRoundParameters(round); if ( + /// TODO this would just check that the array being passed in is empty ( accessCriteria_.accessCriteriaType == AccessCriteriaType.NFT && accessCriteria_.nftContract == address(0) @@ -874,6 +882,7 @@ contract LM_PC_FundingPot_v1 is /// @param allowedAddresses_ Array of addresses permitted to participate /// @param sender_ Address to check for permission /// @return Boolean indicating whether the sender is in the allowed list + /// TODO should check for address in mapping instead of looping through array function _checkAllowedAddressList( address[] memory allowedAddresses_, address sender_ diff --git a/src/modules/logicModule/interfaces/ILM_PC_FundingPot_v1.sol b/src/modules/logicModule/interfaces/ILM_PC_FundingPot_v1.sol index 8d1543efc..50f8ba258 100644 --- a/src/modules/logicModule/interfaces/ILM_PC_FundingPot_v1.sol +++ b/src/modules/logicModule/interfaces/ILM_PC_FundingPot_v1.sol @@ -34,6 +34,7 @@ interface ILM_PC_FundingPot_v1 is IERC20PaymentClientBase_v2 { /// @param nftContract Address of the NFT contract. /// @param merkleRoot Merkle root for the access criteria. /// @param allowedAddresses Mapping of addresses to their access status. + // TODO change array to mapping struct AccessCriteria { AccessCriteriaType accessCriteriaType; address nftContract; // NFT contract address (0x0 if unused) @@ -111,6 +112,8 @@ interface ILM_PC_FundingPot_v1 is IERC20PaymentClientBase_v2 { /// @param roundId_ The unique identifier of the round. /// @param AccessCriteriaId The identifier of the access criteria. /// @param accessCriteria_ The access criteria. + + // TODO remove last parameter event AccessCriteriaSet( uint64 indexed roundId_, uint8 AccessCriteriaId, @@ -121,6 +124,7 @@ interface ILM_PC_FundingPot_v1 is IERC20PaymentClientBase_v2 { /// @param roundId_ The unique identifier of the round. /// @param AccessCriteriaId The identifier of the access criteria. /// @param accessCriteria_ The access criteria. + // TODO remove last parameter event AccessCriteriaEdited( uint64 indexed roundId_, uint8 AccessCriteriaId, @@ -341,6 +345,7 @@ interface ILM_PC_FundingPot_v1 is IERC20PaymentClientBase_v2 { /// @param hookFunction_ New encoded function call. /// @param autoClosure_ New closure mechanism setting. /// @param globalAccumulativeCaps_ New global accumulative caps setting. + /// TODO allow admin to pass nft merkle root and list of allowed addresses function editRound( uint64 roundId_, uint roundStart_, @@ -356,6 +361,7 @@ interface ILM_PC_FundingPot_v1 is IERC20PaymentClientBase_v2 { /// @dev Only callable by funding pot admin and only before the round has started. /// @param roundId_ ID of the round. /// @param accessCriteria_ Access criteria to set. + /// TODO allow admin to pass nft merkle root and list of allowed addresses function setAccessCriteriaForRound( uint64 roundId_, AccessCriteria memory accessCriteria_ diff --git a/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol b/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol index e54867086..4dd68b1b4 100644 --- a/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol +++ b/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol @@ -142,6 +142,8 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { true, true ); + + ///TODO : add array here for allowed addresses } // ------------------------------------------------------------------------- From f29e3126999e99afe25b6fcd09d9133970b29748 Mon Sep 17 00:00:00 2001 From: JeffreyJoel Date: Thu, 10 Apr 2025 12:20:31 +0100 Subject: [PATCH 055/130] feat: change allowed addresses array to mapping in AccessCriteria struct --- .../logicModule/LM_PC_FundingPot_v1.sol | 94 ++-- .../interfaces/ILM_PC_FundingPot_v1.sol | 51 +-- .../logicModule/LM_PC_FundingPot_v1.t.sol | 430 ++++++++++++------ .../LM_PC_FundingPot_v1_Exposed.sol | 10 - 4 files changed, 374 insertions(+), 211 deletions(-) diff --git a/src/modules/logicModule/LM_PC_FundingPot_v1.sol b/src/modules/logicModule/LM_PC_FundingPot_v1.sol index 66fa7c6b6..f605663a0 100644 --- a/src/modules/logicModule/LM_PC_FundingPot_v1.sol +++ b/src/modules/logicModule/LM_PC_FundingPot_v1.sol @@ -200,12 +200,8 @@ contract LM_PC_FundingPot_v1 is round.globalAccumulativeCaps ); } - /// TODO should either have a param for a specifc address - /// and return whether they have access or not - /// or should not return anything related to allowed addresses - /// Though I think the first option is better - /// @inheritdoc ILM_PC_FundingPot_v1 + /// @inheritdoc ILM_PC_FundingPot_v1 function getRoundAccessCriteria(uint64 roundId_, uint8 id_, address user_) external view @@ -224,15 +220,14 @@ contract LM_PC_FundingPot_v1 is true, accessCriteria.nftContract, accessCriteria.merkleRoot, - //// This is wrong but gives you an idea of what you need to do - accessCriteria.allowedAddresses[user_] + true ); } else { return ( false, accessCriteria.nftContract, accessCriteria.merkleRoot, - accessCriteria.allowedAddresses + accessCriteria.allowedAddresses[user_] ); } } @@ -363,46 +358,58 @@ contract LM_PC_FundingPot_v1 is globalAccumulativeCaps_ ); } - // TODO should take in nft merkle root and list of allowed addresses - // SHould loop through the array and set the access criteria for each address to true in the mapping - /// @inheritdoc ILM_PC_FundingPot_v1 + /// @inheritdoc ILM_PC_FundingPot_v1 function setAccessCriteriaForRound( uint64 roundId_, - AccessCriteria memory accessCriteria_ + uint8 accessCriteriaId_, + address nftContract_, + bytes32 merkleRoot_, + address[] calldata allowedAddresses_ ) external onlyModuleRole(FUNDING_POT_ADMIN_ROLE) { Round storage round = rounds[roundId_]; _validateEditRoundParameters(round); if ( - /// TODO this would just check that the array being passed in is empty ( - accessCriteria_.accessCriteriaType == AccessCriteriaType.NFT - && accessCriteria_.nftContract == address(0) + accessCriteriaId_ == uint8(AccessCriteriaType.NFT) + && nftContract_ == address(0) ) || ( - accessCriteria_.accessCriteriaType == AccessCriteriaType.MERKLE - && accessCriteria_.merkleRoot == bytes32("") + accessCriteriaId_ == uint8(AccessCriteriaType.MERKLE) + && merkleRoot_ == bytes32("") ) || ( - accessCriteria_.accessCriteriaType == AccessCriteriaType.LIST - && accessCriteria_.allowedAddresses.length == 0 + accessCriteriaId_ == uint8(AccessCriteriaType.LIST) + && allowedAddresses_.length == 0 ) ) { revert Module__LM_PC_FundingPot__MissingRequiredAccessCriteriaData(); } - uint8 accessCriteriaId_ = uint8(accessCriteria_.accessCriteriaType); - round.accessCriterias[accessCriteriaId_] = accessCriteria_; - emit AccessCriteriaSet(roundId_, accessCriteriaId_, accessCriteria_); + AccessCriteriaType accessCriteriaType = + AccessCriteriaType(accessCriteriaId_); + round.accessCriterias[accessCriteriaId_].accessCriteriaType = + accessCriteriaType; + round.accessCriterias[accessCriteriaId_].nftContract = nftContract_; + round.accessCriterias[accessCriteriaId_].merkleRoot = merkleRoot_; + + for (uint i = 0; i < allowedAddresses_.length; i++) { + round.accessCriterias[accessCriteriaId_].allowedAddresses[allowedAddresses_[i]] + = true; + } + + emit AccessCriteriaSet(roundId_, accessCriteriaId_); } /// @inheritdoc ILM_PC_FundingPot_v1 function editAccessCriteriaForRound( uint64 roundId_, uint8 accessCriteriaId_, - AccessCriteria memory accessCriteria_ + address nftContract_, + bytes32 merkleRoot_, + address[] calldata allowedAddresses_ ) external onlyModuleRole(FUNDING_POT_ADMIN_ROLE) { Round storage round = rounds[roundId_]; if (accessCriteriaId_ > MAX_ACCESS_CRITERIA_ID) { @@ -411,9 +418,19 @@ contract LM_PC_FundingPot_v1 is _validateEditRoundParameters(round); - round.accessCriterias[accessCriteriaId_] = accessCriteria_; + AccessCriteriaType accessCriteriaType = + AccessCriteriaType(accessCriteriaId_); + round.accessCriterias[accessCriteriaId_].accessCriteriaType = + accessCriteriaType; + round.accessCriterias[accessCriteriaId_].nftContract = nftContract_; + round.accessCriterias[accessCriteriaId_].merkleRoot = merkleRoot_; - emit AccessCriteriaEdited(roundId_, accessCriteriaId_, accessCriteria_); + for (uint i = 0; i < allowedAddresses_.length; i++) { + round.accessCriterias[accessCriteriaId_].allowedAddresses[allowedAddresses_[i]] + = true; + } + + emit AccessCriteriaEdited(roundId_, accessCriteriaId_); } /// @inheritdoc ILM_PC_FundingPot_v1 @@ -784,8 +801,12 @@ contract LM_PC_FundingPot_v1 is ); } else if (accessCriteria.accessCriteriaType == AccessCriteriaType.LIST) { - return - _checkAllowedAddressList(accessCriteria.allowedAddresses, user_); + if (!accessCriteria.allowedAddresses[user_]) { + revert Module__LM_PC_FundingPot__AccessCriteriaListFailed(); + return false; + } + + return true; } // For OPEN access criteria type, no validation needed @@ -877,25 +898,6 @@ contract LM_PC_FundingPot_v1 is return totalUnusedCapacity; } - ///@notice Checks if a sender is in a list of allowed addresses - /// @dev Performs a linear search to validate address inclusion - /// @param allowedAddresses_ Array of addresses permitted to participate - /// @param sender_ Address to check for permission - /// @return Boolean indicating whether the sender is in the allowed list - /// TODO should check for address in mapping instead of looping through array - function _checkAllowedAddressList( - address[] memory allowedAddresses_, - address sender_ - ) internal pure returns (bool) { - uint lengthOfAddresses = allowedAddresses_.length; - for (uint i = 0; i < lengthOfAddresses; ++i) { - if (allowedAddresses_[i] == sender_) { - return true; - } - } - revert Module__LM_PC_FundingPot__AccessCriteriaListFailed(); - } - /// @notice Verifies NFT ownership for access control /// @dev Safely checks the NFT balance of a user using a try-catch block /// @param nftContract_ Address of the NFT contract diff --git a/src/modules/logicModule/interfaces/ILM_PC_FundingPot_v1.sol b/src/modules/logicModule/interfaces/ILM_PC_FundingPot_v1.sol index 50f8ba258..9bd19006a 100644 --- a/src/modules/logicModule/interfaces/ILM_PC_FundingPot_v1.sol +++ b/src/modules/logicModule/interfaces/ILM_PC_FundingPot_v1.sol @@ -34,12 +34,11 @@ interface ILM_PC_FundingPot_v1 is IERC20PaymentClientBase_v2 { /// @param nftContract Address of the NFT contract. /// @param merkleRoot Merkle root for the access criteria. /// @param allowedAddresses Mapping of addresses to their access status. - // TODO change array to mapping struct AccessCriteria { AccessCriteriaType accessCriteriaType; address nftContract; // NFT contract address (0x0 if unused) bytes32 merkleRoot; // Merkle root (0x0 if unused) - address[] allowedAddresses; // Explicit allowlist + mapping(address user => bool isAllowed) allowedAddresses; // Mapping of allowed addresses } struct AccessCriteriaPrivileges { @@ -111,25 +110,12 @@ interface ILM_PC_FundingPot_v1 is IERC20PaymentClientBase_v2 { /// @notice Emitted when access criteria is set for a round. /// @param roundId_ The unique identifier of the round. /// @param AccessCriteriaId The identifier of the access criteria. - /// @param accessCriteria_ The access criteria. - - // TODO remove last parameter - event AccessCriteriaSet( - uint64 indexed roundId_, - uint8 AccessCriteriaId, - AccessCriteria accessCriteria_ - ); + event AccessCriteriaSet(uint64 indexed roundId_, uint8 AccessCriteriaId); /// @notice Emitted when access criteria is edited for a round. /// @param roundId_ The unique identifier of the round. /// @param AccessCriteriaId The identifier of the access criteria. - /// @param accessCriteria_ The access criteria. - // TODO remove last parameter - event AccessCriteriaEdited( - uint64 indexed roundId_, - uint8 AccessCriteriaId, - AccessCriteria accessCriteria_ - ); + event AccessCriteriaEdited(uint64 indexed roundId_, uint8 AccessCriteriaId); /// @notice Emitted when access criteria Privileges are set for a round. /// @param roundId_ The unique identifier of the round. @@ -265,18 +251,23 @@ interface ILM_PC_FundingPot_v1 is IERC20PaymentClientBase_v2 { /// @notice Retrieves the access criteria for a specific funding round. /// @param roundId_ The unique identifier of the round to retrieve. /// @param accessCriteriaId_ The identifier of the access criteria to retrieve. + /// @param user_ The address of the user to check access for. /// @return isRoundOpen_ Whether the access criteria is open. /// @return nftContract_ The address of the NFT contract used for access control. /// @return merkleRoot_ The merkle root used for access verification. - /// @return allowedAddresses_ The list of explicitly allowed addresses. - function getRoundAccessCriteria(uint64 roundId_, uint8 accessCriteriaId_) + /// @return hasAccess_ The list of explicitly allowed addresses. + function getRoundAccessCriteria( + uint64 roundId_, + uint8 accessCriteriaId_, + address user_ + ) external view returns ( bool isRoundOpen_, address nftContract_, bytes32 merkleRoot_, - address[] memory allowedAddresses_ + bool hasAccess_ ); /// @notice Retrieves the access criteria Privileges for a specific funding round. @@ -345,7 +336,6 @@ interface ILM_PC_FundingPot_v1 is IERC20PaymentClientBase_v2 { /// @param hookFunction_ New encoded function call. /// @param autoClosure_ New closure mechanism setting. /// @param globalAccumulativeCaps_ New global accumulative caps setting. - /// TODO allow admin to pass nft merkle root and list of allowed addresses function editRound( uint64 roundId_, uint roundStart_, @@ -360,22 +350,31 @@ interface ILM_PC_FundingPot_v1 is IERC20PaymentClientBase_v2 { /// @notice Set Access Control Check. /// @dev Only callable by funding pot admin and only before the round has started. /// @param roundId_ ID of the round. - /// @param accessCriteria_ Access criteria to set. - /// TODO allow admin to pass nft merkle root and list of allowed addresses + /// @param accessCriteriaId_ ID of the access criteria. + /// @param nftContract_ Address of the NFT contract. + /// @param merkleRoot_ Merkle root for the access criteria. + /// @param allowedAddresses_ List of explicitly allowed addresses. function setAccessCriteriaForRound( uint64 roundId_, - AccessCriteria memory accessCriteria_ + uint8 accessCriteriaId_, + address nftContract_, + bytes32 merkleRoot_, + address[] memory allowedAddresses_ ) external; /// @notice Edits an existing access criteria for a round. /// @dev Only callable by funding pot admin and only before the round has started. /// @param roundId_ ID of the round. /// @param accessCriteriaId_ ID of the access criteria. - /// @param accessCriteria_ New access criteria. + /// @param nftContract_ Address of the NFT contract. + /// @param merkleRoot_ Merkle root for the access criteria. + /// @param allowedAddresses_ List of explicitly allowed addresses. function editAccessCriteriaForRound( uint64 roundId_, uint8 accessCriteriaId_, - AccessCriteria memory accessCriteria_ + address nftContract_, + bytes32 merkleRoot_, + address[] memory allowedAddresses_ ) external; /// @notice Set Access Criteria Privileges diff --git a/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol b/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol index 4dd68b1b4..73a331552 100644 --- a/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol +++ b/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol @@ -142,8 +142,6 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { true, true ); - - ///TODO : add array here for allowed addresses } // ------------------------------------------------------------------------- @@ -177,12 +175,12 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { │ └── When user attempts to create a round │ └── Then it should revert │ - ├── And round end time == 0 + ├── And round end time == 0 ├── And round cap == 0 │ └── When user attempts to create a round │ └── Then it should revert │ - ├── And round end time is set + ├── And round end time is set ├── And round end != 0 ├── And round end < round start │ └── When user attempts to create a round @@ -824,8 +822,11 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { testCreateRound(); uint64 roundId = fundingPot.getRoundCount(); - ILM_PC_FundingPot_v1.AccessCriteria memory accessCriteria = - _helper_createAccessCriteria(accessCriteriaEnum_); + ( + address nftContract, + bytes32 merkleRoot, + address[] memory allowedAddresses + ) = _helper_createAccessCriteria(accessCriteriaEnum_); vm.startPrank(user_); bytes32 roleId = _authorizer.generateRoleId( @@ -836,7 +837,13 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { IModule_v1.Module__CallerNotAuthorized.selector, roleId, user_ ) ); - fundingPot.setAccessCriteriaForRound(roundId, accessCriteria); + fundingPot.setAccessCriteriaForRound( + roundId, + accessCriteriaEnum_, + nftContract, + merkleRoot, + allowedAddresses + ); vm.stopPrank(); } @@ -847,8 +854,11 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { uint64 roundId = fundingPot.getRoundCount(); - ILM_PC_FundingPot_v1.AccessCriteria memory accessCriteria = - _helper_createAccessCriteria(accessCriteriaEnum); + ( + address nftContract, + bytes32 merkleRoot, + address[] memory allowedAddresses + ) = _helper_createAccessCriteria(accessCriteriaEnum); vm.expectRevert( abi.encodeWithSelector( @@ -857,7 +867,13 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { .selector ) ); - fundingPot.setAccessCriteriaForRound(roundId, accessCriteria); + fundingPot.setAccessCriteriaForRound( + roundId, + accessCriteriaEnum, + nftContract, + merkleRoot, + allowedAddresses + ); } function testFuzzSetAccessCriteria_revertsGivenRoundIsActive( @@ -868,8 +884,11 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { testCreateRound(); uint64 roundId = fundingPot.getRoundCount(); - ILM_PC_FundingPot_v1.AccessCriteria memory accessCriteria = - _helper_createAccessCriteria(accessCriteriaEnum); + ( + address nftContract, + bytes32 merkleRoot, + address[] memory allowedAddresses + ) = _helper_createAccessCriteria(accessCriteriaEnum); (uint roundStart,,,,,,) = fundingPot.getRoundGenericParameters(roundId); vm.warp(roundStart + 1); @@ -881,7 +900,13 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { .selector ) ); - fundingPot.setAccessCriteriaForRound(roundId, accessCriteria); + fundingPot.setAccessCriteriaForRound( + roundId, + accessCriteriaEnum, + nftContract, + merkleRoot, + allowedAddresses + ); } function testSetAccessCriteria_revertsGivenAccessCriteriaIdIsNFTAndNftContractIsZero( @@ -892,9 +917,12 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { testCreateRound(); uint64 roundId = fundingPot.getRoundCount(); - ILM_PC_FundingPot_v1.AccessCriteria memory accessCriteria = - _helper_createAccessCriteria(accessCriteriaEnum); - accessCriteria.nftContract = address(0); + ( + address nftContract, + bytes32 merkleRoot, + address[] memory allowedAddresses + ) = _helper_createAccessCriteria(accessCriteriaEnum); + nftContract = address(0); vm.expectRevert( abi.encodeWithSelector( @@ -903,7 +931,13 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { .selector ) ); - fundingPot.setAccessCriteriaForRound(roundId, accessCriteria); + fundingPot.setAccessCriteriaForRound( + roundId, + accessCriteriaEnum, + nftContract, + merkleRoot, + allowedAddresses + ); } function testSetAccessCriteria_revertsGivenAccessCriteriaIdIsMerkleAndMerkleRootIsZero( @@ -914,9 +948,12 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { testCreateRound(); uint64 roundId = fundingPot.getRoundCount(); - ILM_PC_FundingPot_v1.AccessCriteria memory accessCriteria = - _helper_createAccessCriteria(accessCriteriaEnum); - accessCriteria.merkleRoot = bytes32(uint(0x0)); + ( + address nftContract, + bytes32 merkleRoot, + address[] memory allowedAddresses + ) = _helper_createAccessCriteria(accessCriteriaEnum); + merkleRoot = bytes32(uint(0x0)); vm.expectRevert( abi.encodeWithSelector( @@ -925,7 +962,13 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { .selector ) ); - fundingPot.setAccessCriteriaForRound(roundId, accessCriteria); + fundingPot.setAccessCriteriaForRound( + roundId, + accessCriteriaEnum, + nftContract, + merkleRoot, + allowedAddresses + ); } function testSetAccessCriteria_revertsGivenAccessCriteriaIdIsListAndAllowedAddressesIsEmpty( @@ -936,9 +979,12 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { testCreateRound(); uint64 roundId = fundingPot.getRoundCount(); - ILM_PC_FundingPot_v1.AccessCriteria memory accessCriteria = - _helper_createAccessCriteria(accessCriteriaEnum); - accessCriteria.allowedAddresses = new address[](0); + ( + address nftContract, + bytes32 merkleRoot, + address[] memory allowedAddresses + ) = _helper_createAccessCriteria(accessCriteriaEnum); + allowedAddresses = new address[](0); vm.expectRevert( abi.encodeWithSelector( @@ -947,7 +993,13 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { .selector ) ); - fundingPot.setAccessCriteriaForRound(roundId, accessCriteria); + fundingPot.setAccessCriteriaForRound( + roundId, + accessCriteriaEnum, + nftContract, + merkleRoot, + allowedAddresses + ); } function testFuzzSetAccessCriteria(uint8 accessCriteriaEnum) public { @@ -956,22 +1008,37 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { testCreateRound(); uint64 roundId = fundingPot.getRoundCount(); - ILM_PC_FundingPot_v1.AccessCriteria memory accessCriteria = - _helper_createAccessCriteria(accessCriteriaEnum); - - fundingPot.setAccessCriteriaForRound(roundId, accessCriteria); - ( - bool isOpen, address nftContract, bytes32 merkleRoot, address[] memory allowedAddresses - ) = fundingPot.getRoundAccessCriteria(roundId, accessCriteriaEnum); + ) = _helper_createAccessCriteria(accessCriteriaEnum); + + fundingPot.setAccessCriteriaForRound( + roundId, + accessCriteriaEnum, + nftContract, + merkleRoot, + allowedAddresses + ); + + ( + bool isOpen, + address retreivedNftContract, + bytes32 retreivedMerkleRoot, + bool hasAccess + ) = fundingPot.getRoundAccessCriteria( + roundId, accessCriteriaEnum, address(0x2) + ); assertEq(isOpen, accessCriteriaEnum == 1); - assertEq(nftContract, accessCriteria.nftContract); - assertEq(merkleRoot, accessCriteria.merkleRoot); - assertEq(allowedAddresses, accessCriteria.allowedAddresses); + assertEq(retreivedNftContract, nftContract); + assertEq(retreivedMerkleRoot, merkleRoot); + if (accessCriteriaEnum == 1 || accessCriteriaEnum == 4) { + assertTrue(hasAccess); + } else { + assertFalse(hasAccess); + } } /* Test editAccessCriteriaForRound() @@ -1007,10 +1074,12 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { _helper_setupRoundWithAccessCriteria(accessCriteriaEnum); uint64 roundId = fundingPot.getRoundCount(); - uint8 accessCriteriaId = 0; - ILM_PC_FundingPot_v1.AccessCriteria memory accessCriteria = - _helper_createAccessCriteria(accessCriteriaEnum); + ( + address nftContract, + bytes32 merkleRoot, + address[] memory allowedAddresses + ) = _helper_createAccessCriteria(accessCriteriaEnum); vm.startPrank(user_); bytes32 roleId = _authorizer.generateRoleId( @@ -1022,7 +1091,11 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { ) ); fundingPot.editAccessCriteriaForRound( - roundId, accessCriteriaId, accessCriteria + roundId, + accessCriteriaEnum, + nftContract, + merkleRoot, + allowedAddresses ); vm.stopPrank(); } @@ -1036,8 +1109,11 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { uint64 roundId = fundingPot.getRoundCount(); uint8 accessCriteriaId = 10; // Invalid ID - ILM_PC_FundingPot_v1.AccessCriteria memory accessCriteria = - _helper_createAccessCriteria(accessCriteriaEnum); + ( + address nftContract, + bytes32 merkleRoot, + address[] memory allowedAddresses + ) = _helper_createAccessCriteria(accessCriteriaEnum); vm.expectRevert( abi.encodeWithSelector( @@ -1047,7 +1123,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { ) ); fundingPot.editAccessCriteriaForRound( - roundId, accessCriteriaId, accessCriteria + roundId, accessCriteriaId, nftContract, merkleRoot, allowedAddresses ); } @@ -1058,12 +1134,15 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { uint64 roundId = fundingPot.getRoundCount(); uint8 accessCriteriaId = 0; - ILM_PC_FundingPot_v1.AccessCriteria memory accessCriteria = - _helper_createAccessCriteria(accessCriteriaEnum); + ( + address nftContract, + bytes32 merkleRoot, + address[] memory allowedAddresses + ) = _helper_createAccessCriteria(accessCriteriaEnum); vm.expectRevert(); fundingPot.editAccessCriteriaForRound( - roundId, accessCriteriaId, accessCriteria + roundId, accessCriteriaId, nftContract, merkleRoot, allowedAddresses ); } @@ -1081,8 +1160,11 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { vm.warp(roundStart + 1); // Create a new access criteria to try to edit with - ILM_PC_FundingPot_v1.AccessCriteria memory newAccessCriteria = - _helper_createAccessCriteria((accessCriteriaEnum + 1) % 5); // Use a different access criteria type + ( + address nftContract, + bytes32 merkleRoot, + address[] memory allowedAddresses + ) = _helper_createAccessCriteria((accessCriteriaEnum + 1) % 5); // Use a different access criteria type // Expect revert when trying to edit access criteria for an active round vm.expectRevert( @@ -1094,7 +1176,9 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { ); // Attempt to edit the access criteria for the active round - fundingPot.editAccessCriteriaForRound(roundId, 0, newAccessCriteria); + fundingPot.editAccessCriteriaForRound( + roundId, 0, nftContract, merkleRoot, allowedAddresses + ); } /* Test: contributeToRound() unhappy paths @@ -1140,10 +1224,15 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { uint8 accessId = 1; uint amount = 250; - ILM_PC_FundingPot_v1.AccessCriteria memory accessCriteria = - _helper_createAccessCriteria(accessId); + ( + address nftContract, + bytes32 merkleRoot, + address[] memory allowedAddresses + ) = _helper_createAccessCriteria(accessId); - fundingPot.setAccessCriteriaForRound(roundId, accessCriteria); + fundingPot.setAccessCriteriaForRound( + roundId, accessId, nftContract, merkleRoot, allowedAddresses + ); fundingPot.setAccessCriteriaPrivileges( roundId, accessId, 500, 0, 0, 0, false, 0, 0, 0 ); @@ -1175,10 +1264,15 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { uint8 accessId = 0; uint amount = 250; - ILM_PC_FundingPot_v1.AccessCriteria memory accessCriteria = - _helper_createAccessCriteria(accessId); + ( + address nftContract, + bytes32 merkleRoot, + address[] memory allowedAddresses + ) = _helper_createAccessCriteria(accessId); - fundingPot.setAccessCriteriaForRound(roundId, accessCriteria); + fundingPot.setAccessCriteriaForRound( + roundId, accessId, nftContract, merkleRoot, allowedAddresses + ); fundingPot.setAccessCriteriaPrivileges( roundId, accessId, 500, 0, 0, 0, false, 0, 0, 0 ); @@ -1245,10 +1339,15 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { uint8 accessId = 3; uint amount = 250; - ILM_PC_FundingPot_v1.AccessCriteria memory accessCriteria = - _helper_createAccessCriteria(accessId); + ( + address nftContract, + bytes32 merkleRoot, + address[] memory allowedAddresses + ) = _helper_createAccessCriteria(accessId); - fundingPot.setAccessCriteriaForRound(roundId, accessCriteria); + fundingPot.setAccessCriteriaForRound( + roundId, accessId, nftContract, merkleRoot, allowedAddresses + ); fundingPot.setAccessCriteriaPrivileges( roundId, accessId, 500, 0, 0, 0, false, 0, 0, 0 ); @@ -1280,10 +1379,15 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { uint8 accessId = 4; uint amount = 250; - ILM_PC_FundingPot_v1.AccessCriteria memory accessCriteria = - _helper_createAccessCriteria(accessId); + ( + address nftContract, + bytes32 merkleRoot, + address[] memory allowedAddresses + ) = _helper_createAccessCriteria(accessId); - fundingPot.setAccessCriteriaForRound(roundId, accessCriteria); + fundingPot.setAccessCriteriaForRound( + roundId, accessId, nftContract, merkleRoot, allowedAddresses + ); fundingPot.setAccessCriteriaPrivileges( roundId, accessId, 500, 0, 0, 0, false, 0, 0, 0 ); @@ -1317,10 +1421,15 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { uint8 accessId = 1; uint amount = 500; - ILM_PC_FundingPot_v1.AccessCriteria memory accessCriteria = - _helper_createAccessCriteria(accessId); + ( + address nftContract, + bytes32 merkleRoot, + address[] memory allowedAddresses + ) = _helper_createAccessCriteria(accessId); - fundingPot.setAccessCriteriaForRound(roundId, accessCriteria); + fundingPot.setAccessCriteriaForRound( + roundId, accessId, nftContract, merkleRoot, allowedAddresses + ); fundingPot.setAccessCriteriaPrivileges( roundId, accessId, 500, 0, 0, 0, false, 0, 0, 0 ); @@ -1385,7 +1494,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { │ And the user has not fully utilized their personal contribution potential in previous rounds │ └── When the user wants to contribute to the current round │ └── Then they can contribute up to their personal limit of the current round plus unfilled potential from previous rounds - │ + │ ├── Given the round has been configured with global accumulative caps │ And in the previous round the round contribution cap was X │ And in total Y had been contributed in the previous round @@ -1402,10 +1511,15 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { uint8 accessId = 1; uint amount = 250; - ILM_PC_FundingPot_v1.AccessCriteria memory accessCriteria = - _helper_createAccessCriteria(accessId); + ( + address nftContract, + bytes32 merkleRoot, + address[] memory allowedAddresses + ) = _helper_createAccessCriteria(accessId); - fundingPot.setAccessCriteriaForRound(roundId, accessCriteria); + fundingPot.setAccessCriteriaForRound( + roundId, accessId, nftContract, merkleRoot, allowedAddresses + ); fundingPot.setAccessCriteriaPrivileges( roundId, accessId, 500, 100, 0, 0, false, 0, 0, 0 ); @@ -1448,10 +1562,15 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { uint8 accessId = 0; uint amount = 201; - ILM_PC_FundingPot_v1.AccessCriteria memory accessCriteria = - _helper_createAccessCriteria(accessId); + ( + address nftContract, + bytes32 merkleRoot, + address[] memory allowedAddresses + ) = _helper_createAccessCriteria(accessId); - fundingPot.setAccessCriteriaForRound(roundId, accessCriteria); + fundingPot.setAccessCriteriaForRound( + roundId, accessId, nftContract, merkleRoot, allowedAddresses + ); fundingPot.setAccessCriteriaPrivileges( roundId, accessId, 200, 0, 0, 0, false, 0, 0, 0 ); @@ -1486,10 +1605,15 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { uint firstAmount = 400; - ILM_PC_FundingPot_v1.AccessCriteria memory accessCriteria = - _helper_createAccessCriteria(accessId); + ( + address nftContract, + bytes32 merkleRoot, + address[] memory allowedAddresses + ) = _helper_createAccessCriteria(accessId); - fundingPot.setAccessCriteriaForRound(roundId, accessCriteria); + fundingPot.setAccessCriteriaForRound( + roundId, accessId, nftContract, merkleRoot, allowedAddresses + ); fundingPot.setAccessCriteriaPrivileges( roundId, accessId, 500, 0, 0, 0, false, 0, 0, 0 ); @@ -1536,10 +1660,15 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { uint8 accessId = 1; uint amount = 250; - ILM_PC_FundingPot_v1.AccessCriteria memory accessCriteria = - _helper_createAccessCriteria(accessId); + ( + address nftContract, + bytes32 merkleRoot, + address[] memory allowedAddresses + ) = _helper_createAccessCriteria(accessId); - fundingPot.setAccessCriteriaForRound(roundId, accessCriteria); + fundingPot.setAccessCriteriaForRound( + roundId, accessId, nftContract, merkleRoot, allowedAddresses + ); // Set privileges with override capability fundingPot.setAccessCriteriaPrivileges( @@ -1585,9 +1714,15 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { uint64 round1Id = fundingPot.getRoundCount(); uint8 accessId = 1; - ILM_PC_FundingPot_v1.AccessCriteria memory accessCriteria = - _helper_createAccessCriteria(1); - fundingPot.setAccessCriteriaForRound(round1Id, accessCriteria); + ( + address nftContract, + bytes32 merkleRoot, + address[] memory allowedAddresses + ) = _helper_createAccessCriteria(accessId); + + fundingPot.setAccessCriteriaForRound( + round1Id, accessId, nftContract, merkleRoot, allowedAddresses + ); fundingPot.setAccessCriteriaPrivileges( round1Id, accessId, 500, 0, 0, 0, false, 0, 0, 0 ); @@ -1604,7 +1739,9 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { _defaultRoundParams.globalAccumulativeCaps ); uint64 round2Id = fundingPot.getRoundCount(); - fundingPot.setAccessCriteriaForRound(round2Id, accessCriteria); + fundingPot.setAccessCriteriaForRound( + round2Id, accessId, nftContract, merkleRoot, allowedAddresses + ); fundingPot.setAccessCriteriaPrivileges( round2Id, accessId, 500, 0, 0, 0, false, 0, 0, 0 ); @@ -1664,9 +1801,14 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { uint64 round1Id = fundingPot.getRoundCount(); uint8 accessId = 0; - ILM_PC_FundingPot_v1.AccessCriteria memory accessCriteria = - _helper_createAccessCriteria(accessId); - fundingPot.setAccessCriteriaForRound(round1Id, accessCriteria); + ( + address nftContract, + bytes32 merkleRoot, + address[] memory allowedAddresses + ) = _helper_createAccessCriteria(accessId); + fundingPot.setAccessCriteriaForRound( + round1Id, accessId, nftContract, merkleRoot, allowedAddresses + ); fundingPot.setAccessCriteriaPrivileges( round1Id, accessId, 500, 0, 0, 0, false, 0, 0, 0 ); @@ -1683,7 +1825,9 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { _defaultRoundParams.globalAccumulativeCaps ); uint64 round2Id = fundingPot.getRoundCount(); - fundingPot.setAccessCriteriaForRound(round2Id, accessCriteria); + fundingPot.setAccessCriteriaForRound( + round2Id, accessId, nftContract, merkleRoot, allowedAddresses + ); fundingPot.setAccessCriteriaPrivileges( round2Id, accessId, 500, 0, 0, 0, false, 0, 0, 0 ); @@ -1845,9 +1989,14 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { // Set access criteria and privileges uint8 accessId = 0; - ILM_PC_FundingPot_v1.AccessCriteria memory accessCriteria = - _helper_createAccessCriteria(accessId); - fundingPot.setAccessCriteriaForRound(roundId1, accessCriteria); + ( + address nftContract, + bytes32 merkleRoot, + address[] memory allowedAddresses + ) = _helper_createAccessCriteria(accessId); + fundingPot.setAccessCriteriaForRound( + roundId1, accessId, nftContract, merkleRoot, allowedAddresses + ); fundingPot.setAccessCriteriaPrivileges( roundId1, accessId, @@ -1897,9 +2046,14 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { // Set access criteria for round 1 uint8 accessId = 0; - ILM_PC_FundingPot_v1.AccessCriteria memory accessCriteria = - _helper_createAccessCriteria(accessId); - fundingPot.setAccessCriteriaForRound(roundId1, accessCriteria); + ( + address nftContract, + bytes32 merkleRoot, + address[] memory allowedAddresses + ) = _helper_createAccessCriteria(accessId); + fundingPot.setAccessCriteriaForRound( + roundId1, accessId, nftContract, merkleRoot, allowedAddresses + ); fundingPot.setAccessCriteriaPrivileges( roundId1, accessId, @@ -1932,7 +2086,9 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { ); // Set access criteria for round 2 - fundingPot.setAccessCriteriaForRound(roundId2, accessCriteria); + fundingPot.setAccessCriteriaForRound( + roundId2, accessId, nftContract, merkleRoot, allowedAddresses + ); fundingPot.setAccessCriteriaPrivileges( roundId2, accessId, @@ -1984,9 +2140,14 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { // Set access criteria and privileges uint8 accessId = 0; - ILM_PC_FundingPot_v1.AccessCriteria memory accessCriteria = - _helper_createAccessCriteria(accessId); - fundingPot.setAccessCriteriaForRound(roundId1, accessCriteria); + ( + address nftContract, + bytes32 merkleRoot, + address[] memory allowedAddresses + ) = _helper_createAccessCriteria(accessId); + fundingPot.setAccessCriteriaForRound( + roundId1, accessId, nftContract, merkleRoot, allowedAddresses + ); fundingPot.setAccessCriteriaPrivileges( roundId1, accessId, @@ -2034,9 +2195,14 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { // Set access criteria and privileges uint8 accessId = 0; - ILM_PC_FundingPot_v1.AccessCriteria memory accessCriteria = - _helper_createAccessCriteria(accessId); - fundingPot.setAccessCriteriaForRound(roundId, accessCriteria); + ( + address nftContract, + bytes32 merkleRoot, + address[] memory allowedAddresses + ) = _helper_createAccessCriteria(accessId); + fundingPot.setAccessCriteriaForRound( + roundId, accessId, nftContract, merkleRoot, allowedAddresses + ); fundingPot.setAccessCriteriaPrivileges( roundId, accessId, @@ -2123,9 +2289,14 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { // Set access criteria and privileges uint8 accessId = 0; - ILM_PC_FundingPot_v1.AccessCriteria memory accessCriteria = - _helper_createAccessCriteria(accessId); - fundingPot.setAccessCriteriaForRound(roundId, accessCriteria); + ( + address nftContract, + bytes32 merkleRoot, + address[] memory allowedAddresses + ) = _helper_createAccessCriteria(accessId); + fundingPot.setAccessCriteriaForRound( + roundId, accessId, nftContract, merkleRoot, allowedAddresses + ); fundingPot.setAccessCriteriaPrivileges( roundId, accessId, @@ -2205,58 +2376,50 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { function _helper_createAccessCriteria(uint8 accessCriteriaEnum) internal view - returns (ILM_PC_FundingPot_v1.AccessCriteria memory accessCriteria) + returns ( + address nftContract_, + bytes32 merkleRoot_, + address[] memory allowedAddresses_ + ) { { if ( accessCriteriaEnum == uint8(ILM_PC_FundingPot_v1.AccessCriteriaType.OPEN) ) { - accessCriteria = ILM_PC_FundingPot_v1.AccessCriteria( - ILM_PC_FundingPot_v1.AccessCriteriaType.OPEN, - address(0x0), - bytes32(uint(0x0)), - new address[](0) - ); + nftContract_ = address(0x0); + merkleRoot_ = bytes32(uint(0x0)); + allowedAddresses_ = new address[](0); } else if ( accessCriteriaEnum == uint8(ILM_PC_FundingPot_v1.AccessCriteriaType.NFT) ) { address nftContract = address(mockNFTContract); - accessCriteria = ILM_PC_FundingPot_v1.AccessCriteria( - ILM_PC_FundingPot_v1.AccessCriteriaType.NFT, - nftContract, - bytes32(uint(0x0)), - new address[](0) - ); + nftContract_ = nftContract; + merkleRoot_ = bytes32(uint(0x0)); + allowedAddresses_ = new address[](0); } else if ( accessCriteriaEnum == uint8(ILM_PC_FundingPot_v1.AccessCriteriaType.MERKLE) ) { bytes32 merkleRoot = ROOT; - accessCriteria = ILM_PC_FundingPot_v1.AccessCriteria( - ILM_PC_FundingPot_v1.AccessCriteriaType.MERKLE, - address(0x0), - merkleRoot, - new address[](0) - ); + nftContract_ = address(0x0); + merkleRoot_ = merkleRoot; + allowedAddresses_ = new address[](0); } else if ( accessCriteriaEnum == uint8(ILM_PC_FundingPot_v1.AccessCriteriaType.LIST) ) { address[] memory allowedAddresses = new address[](3); - allowedAddresses[0] = address(0x1); + allowedAddresses[0] = address(this); allowedAddresses[1] = address(0x2); allowedAddresses[2] = address(0x3); - accessCriteria = ILM_PC_FundingPot_v1.AccessCriteria( - ILM_PC_FundingPot_v1.AccessCriteriaType.LIST, - address(0x0), - bytes32(uint(0x0)), - allowedAddresses - ); + nftContract_ = address(0x0); + merkleRoot_ = bytes32(uint(0x0)); + allowedAddresses_ = allowedAddresses; } } } @@ -2275,9 +2438,18 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { _defaultRoundParams.globalAccumulativeCaps ); - ILM_PC_FundingPot_v1.AccessCriteria memory accessCriteria = - _helper_createAccessCriteria(accessCriteriaEnum); + ( + address nftContract, + bytes32 merkleRoot, + address[] memory allowedAddresses + ) = _helper_createAccessCriteria(accessCriteriaEnum); - fundingPot.setAccessCriteriaForRound(roundId, accessCriteria); + fundingPot.setAccessCriteriaForRound( + roundId, + accessCriteriaEnum, + nftContract, + merkleRoot, + allowedAddresses + ); } } diff --git a/test/unit/modules/logicModule/LM_PC_FundingPot_v1_Exposed.sol b/test/unit/modules/logicModule/LM_PC_FundingPot_v1_Exposed.sol index 9f11775a4..71f09a037 100644 --- a/test/unit/modules/logicModule/LM_PC_FundingPot_v1_Exposed.sol +++ b/test/unit/modules/logicModule/LM_PC_FundingPot_v1_Exposed.sol @@ -99,16 +99,6 @@ contract LM_PC_FundingPot_v1_Exposed is LM_PC_FundingPot_v1 { _validateAccessCriteria(roundId_, accessId_, merkleProof_, user_); } - /** - * @notice Exposes the internal _checkAllowedAddressList function for testing - */ - function exposed_checkAllowedAddressList( - address[] memory allowedAddresses_, - address sender_ - ) external pure returns (bool) { - return _checkAllowedAddressList(allowedAddresses_, sender_); - } - /** * @notice Exposes the internal _checkNftOwnership function for testing */ From aace8fdae3ca72edbdaf9fb02c12f94c27f2c7a8 Mon Sep 17 00:00:00 2001 From: Zuhaib Mohammed Date: Sun, 13 Apr 2025 09:56:33 +0530 Subject: [PATCH 056/130] fix: PR Review Fix#1 --- .../interfaces/ILM_PC_FundingPot_v1.sol | 124 +++++++++--------- 1 file changed, 63 insertions(+), 61 deletions(-) diff --git a/src/modules/logicModule/interfaces/ILM_PC_FundingPot_v1.sol b/src/modules/logicModule/interfaces/ILM_PC_FundingPot_v1.sol index 9bd19006a..f796f77ef 100644 --- a/src/modules/logicModule/interfaces/ILM_PC_FundingPot_v1.sol +++ b/src/modules/logicModule/interfaces/ILM_PC_FundingPot_v1.sol @@ -109,42 +109,44 @@ interface ILM_PC_FundingPot_v1 is IERC20PaymentClientBase_v2 { /// @notice Emitted when access criteria is set for a round. /// @param roundId_ The unique identifier of the round. - /// @param AccessCriteriaId The identifier of the access criteria. - event AccessCriteriaSet(uint64 indexed roundId_, uint8 AccessCriteriaId); + /// @param accessCriteriaId_ The identifier of the access criteria. + event AccessCriteriaSet(uint64 indexed roundId_, uint8 accessCriteriaId_); /// @notice Emitted when access criteria is edited for a round. /// @param roundId_ The unique identifier of the round. - /// @param AccessCriteriaId The identifier of the access criteria. - event AccessCriteriaEdited(uint64 indexed roundId_, uint8 AccessCriteriaId); + /// @param accessCriteriaId_ The identifier of the access criteria. + event AccessCriteriaEdited( + uint64 indexed roundId_, uint8 accessCriteriaId_ + ); - /// @notice Emitted when access criteria Privileges are set for a round. + /// @notice Emitted when access criteria privileges are set for a round. /// @param roundId_ The unique identifier of the round. - /// @param AccessCriteriaId The identifier of the access criteria. + /// @param accessCriteriaId_ The identifier of the access criteria. /// @param personalCap_ The personal cap for the access criteria. - /// @param overrideCap_ Whether to override the global cap. - /// @param start_ The start timestamp for the access criteria. - /// @param cliff_ The cliff timestamp for the access criteria. - /// @param end_ The end timestamp for the access criteria. + /// @param overrideContributionSpan_ Whether to override the round contribution span. + /// @param start_ The start timestamp for for when the linear vesting starts. + /// @param cliff_ The time in seconds from start time at which the unlock starts. + /// @param end_ The end timestamp for when the linear vesting ends. event AccessCriteriaPrivilegesSet( uint64 indexed roundId_, - uint8 AccessCriteriaId, + uint8 accessCriteriaId_, uint personalCap_, - bool overrideCap_, + bool overrideContributionSpan_, uint start_, uint cliff_, uint end_ ); - /// @notice Emitted when a contribution is made to a round - /// @param roundId_ The ID of the round - /// @param contributor_ The address of the contributor - /// @param amount_ The amount contributed + /// @notice Emitted when a contribution is made to a round. + /// @param roundId_ The ID of the round. + /// @param contributor_ The address of the contributor. + /// @param amount_ The amount contributed. event ContributionMade(uint64 roundId_, address contributor_, uint amount_); - /// @notice Emitted when a round is closed - /// @param roundId_ The ID of the round - /// @param timestamp_ The timestamp when the round was closed - /// @param totalContributions_ The total contributions collected in the round + /// @notice Emitted when a round is closed. + /// @param roundId_ The ID of the round. + /// @param timestamp_ The timestamp when the round was closed. + /// @param totalContributions_ The total contributions collected in the round. event RoundClosed( uint64 roundId_, uint timestamp_, uint totalContributions_ ); @@ -184,43 +186,43 @@ interface ILM_PC_FundingPot_v1 is IERC20PaymentClientBase_v2 { /// @notice Invalid access criteria ID. error Module__LM_PC_FundingPot__InvalidAccessCriteriaId(); - /// @notice Cannot set Privileges for open access criteria + /// @notice Cannot set Privileges for open access criteria. error Module__LM_PC_FundingPot__CannotSetPrivilegesForOpenAccessCriteria(); - /// @notice Invalid times + /// @notice Invalid times. error Module__LM_PC_FundingPot__InvalidTimes(); - /// @notice Round has not started yet + /// @notice Round has not started yet. error Module__LM_PC_FundingPot__RoundHasNotStarted(); - /// @notice Round has already ended + /// @notice Round has already ended. error Module__LM_PC_FundingPot__RoundHasEnded(); - /// @notice User does not meet the NFT access criteria + /// @notice User does not meet the NFT access criteria. error Module__LM_PC_FundingPot__AccessCriteriaNftFailed(); - /// @notice User does not meet the merkle proof access criteria + /// @notice User does not meet the merkle proof access criteria. error Module__LM_PC_FundingPot__AccessCriteriaMerkleFailed(); - /// @notice User is not on the allowlist + /// @notice User is not on the allowlist. error Module__LM_PC_FundingPot__AccessCriteriaListFailed(); - /// @notice Invalid access criteria type + /// @notice Invalid access criteria type. error Module__LM_PC_FundingPot__InvalidAccessCriteriaType(); - /// @notice Access not permitted + /// @notice Access not permitted. error Module__LM_PC_FundingPot__AccessNotPermitted(); - /// @notice User has reached their personal contribution cap + /// @notice User has reached their personal contribution cap. error Module__LM_PC_FundingPot__PersonalCapReached(); - /// @notice Round contribution cap has been reached + /// @notice Round contribution cap has been reached. error Module__LM_PC_FundingPot__RoundCapReached(); - /// @notice Round Closure conditions are not met + /// @notice Round Closure conditions are not met. error Module__LM_PC_FundingPot__ClosureConditionsNotMet(); - /// @notice Hook execution failed + /// @notice Hook execution failed. error Module__LM_PC_FundingPot__HookExecutionFailed(); // ------------------------------------------------------------------------- @@ -252,7 +254,7 @@ interface ILM_PC_FundingPot_v1 is IERC20PaymentClientBase_v2 { /// @param roundId_ The unique identifier of the round to retrieve. /// @param accessCriteriaId_ The identifier of the access criteria to retrieve. /// @param user_ The address of the user to check access for. - /// @return isRoundOpen_ Whether the access criteria is open. + /// @return isRoundOpen_ Whether anyone can contribute as part of the access criteria. /// @return nftContract_ The address of the NFT contract used for access control. /// @return merkleRoot_ The merkle root used for access verification. /// @return hasAccess_ The list of explicitly allowed addresses. @@ -270,18 +272,18 @@ interface ILM_PC_FundingPot_v1 is IERC20PaymentClientBase_v2 { bool hasAccess_ ); - /// @notice Retrieves the access criteria Privileges for a specific funding round. + /// @notice Retrieves the access criteria privileges for a specific funding round. /// @param roundId_ The unique identifier of the round. - /// @param AccessCriteriaId The identifier of the access criteria. - /// @return isRoundOpen_ Whether the round is open - /// @return personalCap_ The personal cap for the access criteria - /// @return overrideContributionSpan_ Whether to override the round contribution span - /// @return start_ The start timestamp for the access criteria - /// @return cliff_ The cliff timestamp for the access criteria - /// @return end_ The end timestamp for the access criteria + /// @param accessCriteriaId_ The identifier of the access criteria. + /// @return isRoundOpen_ Whether anyone can contribute as part of the access criteria. + /// @return personalCap_ The personal cap for the access criteria. + /// @return overrideContributionSpan_ Whether to override the round contribution span. + /// @return start_ The start timestamp for the access criteria. + /// @return cliff_ The cliff timestamp for the access criteria. + /// @return end_ The end timestamp for the access criteria. function getRoundAccessCriteriaPrivileges( uint64 roundId_, - uint8 AccessCriteriaId + uint8 accessCriteriaId_ ) external view @@ -298,9 +300,9 @@ interface ILM_PC_FundingPot_v1 is IERC20PaymentClientBase_v2 { /// @return roundCount_ The total number of funding rounds. function getRoundCount() external view returns (uint64 roundCount_); - /// @notice Retrieves the closed status of a round - /// @param roundId_ The ID of the round - /// @return The closed status of the round + /// @notice Retrieves the closed status of a round. + /// @param roundId_ The ID of the round. + /// @return The closed status of the round. function isRoundClosed(uint64 roundId_) external view returns (bool); // ------------------------------------------------------------------------- @@ -377,21 +379,21 @@ interface ILM_PC_FundingPot_v1 is IERC20PaymentClientBase_v2 { address[] memory allowedAddresses_ ) external; - /// @notice Set Access Criteria Privileges - /// @dev Only callable by funding pot admin and only before the round has started - /// @param roundId_ ID of the round - /// @param AccessCriteriaId ID of the access criteria - /// @param personalCap_ Personal cap for the access criteria - /// @param capByNFT_ Cap by for the NFT access criteria - /// @param capByMerkle_ Cap for the Merkle root access criteria - /// @param capByList_ Cap by for the List access criteria - /// @param overrideContributionSpan_ Whether to override the round contribution span - /// @param start_ Start timestamp for the access criteria - /// @param cliff_ Cliff timestamp for the access criteria - /// @param end_ End timestamp for the access criteria + /// @notice Set access criteria privileges. + /// @dev Only callable by funding pot admin and only before the round has started. + /// @param roundId_ ID of the round. + /// @param accessCriteriaId_ ID of the access criteria. + /// @param personalCap_ Personal cap for the access criteria. + /// @param capByNFT_ Cap by for the NFT access criteria. + /// @param capByMerkle_ Cap for the Merkle root access criteria. + /// @param capByList_ Cap by for the List access criteria. + /// @param overrideContributionSpan_ Whether to override the round contribution span. + /// @param start_ Start timestamp for the access criteria. + /// @param cliff_ Cliff timestamp for the access criteria. + /// @param end_ End timestamp for the access criteria. function setAccessCriteriaPrivileges( uint64 roundId_, - uint8 AccessCriteriaId, + uint8 accessCriteriaId_, uint personalCap_, uint capByNFT_, uint capByMerkle_, @@ -415,7 +417,7 @@ interface ILM_PC_FundingPot_v1 is IERC20PaymentClientBase_v2 { bytes32[] calldata merkleProof_ ) external; - /// @notice Closes a round - /// @param roundId_ The ID of the round to close + /// @notice Closes a round. + /// @param roundId_ The ID of the round to close. function closeRound(uint64 roundId_) external; } From 930ba9e4a27c3cdb0bd49ef9f755722181a87cff Mon Sep 17 00:00:00 2001 From: Zuhaib Mohammed Date: Mon, 14 Apr 2025 12:52:45 +0530 Subject: [PATCH 057/130] fix: PR Review Fix#2 --- src/modules/logicModule/LM_PC_FundingPot_v1.sol | 13 ++++++------- .../logicModule/interfaces/ILM_PC_FundingPot_v1.sol | 6 ++++++ 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/src/modules/logicModule/LM_PC_FundingPot_v1.sol b/src/modules/logicModule/LM_PC_FundingPot_v1.sol index f605663a0..c38e854d2 100644 --- a/src/modules/logicModule/LM_PC_FundingPot_v1.sol +++ b/src/modules/logicModule/LM_PC_FundingPot_v1.sol @@ -522,20 +522,20 @@ contract LM_PC_FundingPot_v1 is bytes32[] calldata merkleProof_ ) external { uint adjustedAmount = _validateRoundContribution( - roundId_, accessCriteriaId_, merkleProof_, amount_, msg.sender + roundId_, accessCriteriaId_, merkleProof_, amount_, _msgSender() ); Round storage round = rounds[roundId_]; //Record contribution - userContributions[roundId_][msg.sender] += adjustedAmount; + userContributions[roundId_][_msgSender()] += adjustedAmount; roundTotalContributions[roundId_] += adjustedAmount; IERC20(contributionToken).safeTransferFrom( - msg.sender, address(this), adjustedAmount + _msgSender(), address(this), adjustedAmount ); - emit ContributionMade(roundId_, msg.sender, adjustedAmount); + emit ContributionMade(roundId_, _msgSender(), adjustedAmount); // contribution triggers automatic closure if (!roundClosed[roundId_] && round.autoClosure) { @@ -731,9 +731,9 @@ contract LM_PC_FundingPot_v1 is // Check and adjust for personal cap uint userPreviousContribution = - _getUserContributionToRound(roundId_, msg.sender); + _getUserContributionToRound(roundId_, _msgSender()); uint userPersonalCap = _getUserPersonalCapForRound( - roundId_, accessCriteriaId__, msg.sender + roundId_, accessCriteriaId__, _msgSender() ); if (userPreviousContribution + adjustedAmount > userPersonalCap) { @@ -803,7 +803,6 @@ contract LM_PC_FundingPot_v1 is { if (!accessCriteria.allowedAddresses[user_]) { revert Module__LM_PC_FundingPot__AccessCriteriaListFailed(); - return false; } return true; diff --git a/src/modules/logicModule/interfaces/ILM_PC_FundingPot_v1.sol b/src/modules/logicModule/interfaces/ILM_PC_FundingPot_v1.sol index f796f77ef..669aea228 100644 --- a/src/modules/logicModule/interfaces/ILM_PC_FundingPot_v1.sol +++ b/src/modules/logicModule/interfaces/ILM_PC_FundingPot_v1.sol @@ -41,6 +41,12 @@ interface ILM_PC_FundingPot_v1 is IERC20PaymentClientBase_v2 { mapping(address user => bool isAllowed) allowedAddresses; // Mapping of allowed addresses } + /// @notice Struct used to store information about a funding round's access criteria privileges. + /// @param personalCap Personal cap for the access criteria. + /// @param overrideContributionSpan Whether to override the round contribution span. + /// @param start The start timestamp for for when the linear vesting starts. + /// @param cliff The time in seconds from start time at which the unlock starts. + /// @param end The end timestamp for when the linear vesting ends. struct AccessCriteriaPrivileges { uint personalCap; bool overrideContributionSpan; From 25b66df25e48e0c209c0ac24fcff735239f85476 Mon Sep 17 00:00:00 2001 From: JeffreyJoel Date: Tue, 15 Apr 2025 14:46:40 +0100 Subject: [PATCH 058/130] fix: fix the personal cap rollover issue --- .../logicModule/LM_PC_FundingPot_v1.sol | 353 ++++++++++-------- .../interfaces/ILM_PC_FundingPot_v1.sol | 24 ++ .../logicModule/LM_PC_FundingPot_v1.t.sol | 306 +++------------ .../LM_PC_FundingPot_v1_Exposed.sol | 73 ++-- 4 files changed, 297 insertions(+), 459 deletions(-) diff --git a/src/modules/logicModule/LM_PC_FundingPot_v1.sol b/src/modules/logicModule/LM_PC_FundingPot_v1.sol index c38e854d2..9f5431dfa 100644 --- a/src/modules/logicModule/LM_PC_FundingPot_v1.sol +++ b/src/modules/logicModule/LM_PC_FundingPot_v1.sol @@ -135,9 +135,6 @@ contract LM_PC_FundingPot_v1 is /// @notice The current round count. uint64 private roundCount; - /// @notice The token used for contributions. - IERC20 private contributionToken; - /// @notice Storage gap for future upgrades. uint[50] private __gap; @@ -159,8 +156,6 @@ contract LM_PC_FundingPot_v1 is bytes memory configData_ ) external override(Module_v1) initializer { __Module_init(orchestrator_, metadata_); - address fundingPotToken; - (fundingPotToken) = abi.decode(configData_, (address)); // Set the flags for the PaymentOrders (this module uses 3 flags). bytes32 flags; flags |= bytes32(1 << FLAG_START); @@ -168,8 +163,6 @@ contract LM_PC_FundingPot_v1 is flags |= bytes32(1 << FLAG_END); __ERC20PaymentClientBase_v2_init(flags); - - contributionToken = IERC20(address(fundingPotToken)); } // ------------------------------------------------------------------------- @@ -521,31 +514,60 @@ contract LM_PC_FundingPot_v1 is uint8 accessCriteriaId_, bytes32[] calldata merkleProof_ ) external { - uint adjustedAmount = _validateRoundContribution( - roundId_, accessCriteriaId_, merkleProof_, amount_, _msgSender() + // Call the internal function with no additional unspent personal cap + _contributeToRound( + roundId_, amount_, accessCriteriaId_, merkleProof_, 0 ); + } - Round storage round = rounds[roundId_]; + /// @inheritdoc ILM_PC_FundingPot_v1 + function contributeToRound( + uint64 roundId_, + uint amount_, + uint8 accessCriteriaId_, + bytes32[] memory merkleProof_, + UnspentPersonalRoundCap[] calldata unspentPersonalRoundCaps_ + ) external { + uint unspentPersonalCap = 0; - //Record contribution - userContributions[roundId_][_msgSender()] += adjustedAmount; - roundTotalContributions[roundId_] += adjustedAmount; + // Process each previous round cap that the user wants to carry over + for (uint i = 0; i < unspentPersonalRoundCaps_.length; i++) { + UnspentPersonalRoundCap memory roundCap = + unspentPersonalRoundCaps_[i]; - IERC20(contributionToken).safeTransferFrom( - _msgSender(), address(this), adjustedAmount - ); + Round storage prevRound = rounds[roundCap.roundId]; + if (!prevRound.globalAccumulativeCaps) continue; - emit ContributionMade(roundId_, _msgSender(), adjustedAmount); + // Verify the user was eligible for this access criteria in the previous round + bool isEligible = _checkAccessCriteriaEligibility( + roundCap.roundId, + roundCap.accessCriteriaId, + roundCap.merkleProof, + _msgSender() + ); - // contribution triggers automatic closure - if (!roundClosed[roundId_] && round.autoClosure) { - bool readyToClose = _checkRoundClosureConditions(roundId_); - if (readyToClose) { - _closeRound(roundId_); - } else { - revert Module__LM_PC_FundingPot__ClosureConditionsNotMet(); + if (isEligible) { + AccessCriteriaPrivileges storage privileges = + accessCriteriaPrivileges[roundCap.roundId][roundCap + .accessCriteriaId]; + + uint userContribution = + _getUserContributionToRound(roundCap.roundId, _msgSender()); + uint personalCap = privileges.personalCap; + + if (userContribution < personalCap) { + unspentPersonalCap += (personalCap - userContribution); + } } } + + _contributeToRound( + roundId_, + amount_, + accessCriteriaId_, + merkleProof_, + unspentPersonalCap + ); } /// @inheritdoc ILM_PC_FundingPot_v1 @@ -636,28 +658,27 @@ contract LM_PC_FundingPot_v1 is return start_ + cliff_ <= end_; } - /// @notice Validates the round existence and access criteria - /// @param roundId_ ID of the round to validate - /// @param accessCriteriaId_ ID of the access criteria to check - /// @param merkleProof_ Merkle proof for validation if needed - /// @param amount_ The amount sent by the user - /// @param user_ The address of the user - /// @return adjustedAmount The potentially adjusted contribution amount based on personal and round caps - function _validateRoundContribution( + /// @notice Contributes to a round with unused capacity from previous rounds + /// @param roundId_ The ID of the round to contribute to + /// @param amount_ The amount to contribute + /// @param accessCriteriaId_ The ID of the access criteria to use for this contribution + /// @param merkleProof_ The Merkle proof for validation if needed + /// @param unspentPersonalCap_ The amount of unused capacity from previous rounds + function _contributeToRound( uint64 roundId_, - uint8 accessCriteriaId_, - bytes32[] calldata merkleProof_, uint amount_, - address user_ - ) internal view returns (uint adjustedAmount) { - Round storage round = rounds[roundId_]; - uint currentTime = block.timestamp; - adjustedAmount = amount_; - + uint8 accessCriteriaId_, + bytes32[] memory merkleProof_, + uint unspentPersonalCap_ + ) internal { if (amount_ == 0) { revert Module__LM_PC_FundingPot__InvalidDepositAmount(); } - // Validate round exists. + + Round storage round = rounds[roundId_]; + uint currentTime = block.timestamp; + + // Validate round exists if (round.roundEnd == 0 && round.roundCap == 0) { revert Module__LM_PC_FundingPot__RoundNotCreated(); } @@ -667,13 +688,13 @@ contract LM_PC_FundingPot_v1 is revert Module__LM_PC_FundingPot__RoundHasNotStarted(); } + // Validate access criteria _validateAccessCriteria( - roundId_, accessCriteriaId_, merkleProof_, user_ + roundId_, accessCriteriaId_, merkleProof_, _msgSender() ); AccessCriteriaPrivileges storage privileges = accessCriteriaPrivileges[roundId_][accessCriteriaId_]; - bool canOverrideContributionSpan = privileges.overrideContributionSpan; // Allow contributions after the round end if the user can override the contribution span @@ -684,23 +705,87 @@ contract LM_PC_FundingPot_v1 is revert Module__LM_PC_FundingPot__RoundHasEnded(); } - adjustedAmount = _validateAndAdjustCaps( - roundId_, amount_, accessCriteriaId_, canOverrideContributionSpan + // Calculate the adjusted amount considering caps + uint adjustedAmount = _validateAndAdjustCapsWithUnspentCap( + roundId_, + amount_, + accessCriteriaId_, + canOverrideContributionSpan, + unspentPersonalCap_ ); - return adjustedAmount; + // Record contribution + userContributions[roundId_][_msgSender()] += adjustedAmount; + roundTotalContributions[roundId_] += adjustedAmount; + + __Module_orchestrator.fundingManager().token().safeTransferFrom( + _msgSender(), address(this), adjustedAmount + ); + + emit ContributionMade(roundId_, _msgSender(), adjustedAmount); + + // contribution triggers automatic closure + if (!roundClosed[roundId_] && round.autoClosure) { + bool readyToClose = _checkRoundClosureConditions(roundId_); + if (readyToClose) { + _closeRound(roundId_); + } else { + revert Module__LM_PC_FundingPot__ClosureConditionsNotMet(); + } + } } - /// @notice Validates cap constraints and adjusts amount if needed - /// @param roundId_ ID of the round - /// @param amount_ Requested contribution amount - /// @param canOverrideContributionSpan_ Whether the user can override cap constraints - /// @return adjustedAmount The potentially adjusted contribution amount - function _validateAndAdjustCaps( + /// @notice Validates access criteria for a specific round and access type + /// @dev Checks if a user meets the access requirements based on the round's access criteria + /// @param roundId_ The ID of the round being validated + /// @param accessCriteriaId_ The ID of the specific access criteria + /// @param merkleProof_ Merkle proof for Merkle tree-based access (optional) + /// @param user_ The address of the user to validate + function _validateAccessCriteria( + uint64 roundId_, + uint8 accessCriteriaId_, + bytes32[] memory merkleProof_, + address user_ + ) internal view { + Round storage round = rounds[roundId_]; + AccessCriteria storage accessCriteria = + round.accessCriterias[accessCriteriaId_]; + + bool isEligible = _checkAccessCriteriaEligibility( + roundId_, accessCriteriaId_, merkleProof_, user_ + ); + + if (!isEligible) { + if (accessCriteriaId_ > MAX_ACCESS_CRITERIA_ID) { + revert Module__LM_PC_FundingPot__InvalidAccessCriteriaId(); + } + + if (accessCriteria.accessCriteriaType == AccessCriteriaType.NFT) { + revert Module__LM_PC_FundingPot__AccessCriteriaNftFailed(); + } + if (accessCriteria.accessCriteriaType == AccessCriteriaType.MERKLE) + { + revert Module__LM_PC_FundingPot__AccessCriteriaMerkleFailed(); + } + + if (accessCriteria.accessCriteriaType == AccessCriteriaType.LIST) { + revert Module__LM_PC_FundingPot__AccessCriteriaListFailed(); + } + } + } + + /// @notice Validates and adjusts the contribution amount considering caps and unspent capacity + /// @param roundId_ The ID of the round to contribute to + /// @param amount_ The amount to contribute + /// @param accessCriteriaId__ The ID of the access criteria to use for this contribution + /// @param canOverrideContributionSpan_ Whether the contribution span can be overridden + /// @param unspentPersonalCap_ The amount of unused capacity from previous rounds + function _validateAndAdjustCapsWithUnspentCap( uint64 roundId_, uint amount_, uint8 accessCriteriaId__, - bool canOverrideContributionSpan_ + bool canOverrideContributionSpan_, + uint unspentPersonalCap_ ) internal view returns (uint adjustedAmount) { adjustedAmount = amount_; @@ -732,9 +817,16 @@ contract LM_PC_FundingPot_v1 is // Check and adjust for personal cap uint userPreviousContribution = _getUserContributionToRound(roundId_, _msgSender()); - uint userPersonalCap = _getUserPersonalCapForRound( - roundId_, accessCriteriaId__, _msgSender() - ); + + // Get the base personal cap for this round and criteria + AccessCriteriaPrivileges storage privileges = + accessCriteriaPrivileges[roundId_][accessCriteriaId__]; + uint userPersonalCap = privileges.personalCap; + + // Add unspent capacity if global accumulative caps are enabled + if (round.globalAccumulativeCaps) { + userPersonalCap += unspentPersonalCap_; + } if (userPreviousContribution + adjustedAmount > userPersonalCap) { if (userPreviousContribution < userPersonalCap) { @@ -747,69 +839,67 @@ contract LM_PC_FundingPot_v1 is return adjustedAmount; } - /// @notice Calculates unused capacity from previous rounds - /// @param roundId_ The ID of the current round - /// @return unusedCapacityFromPrevious The total unused capacity from previous rounds - function _calculateUnusedCapacityFromPreviousRounds(uint64 roundId_) - internal - view - returns (uint unusedCapacityFromPrevious) - { - unusedCapacityFromPrevious = 0; - // Iterate through all previous rounds (1 to roundId_-1) - for (uint64 i = 1; i < roundId_; ++i) { - Round storage prevRound = rounds[i]; - if (!prevRound.globalAccumulativeCaps) continue; - - uint prevRoundTotal = _getTotalRoundContribution(i); - if (prevRoundTotal < prevRound.roundCap) { - unusedCapacityFromPrevious += - (prevRound.roundCap - prevRoundTotal); - } - } - return unusedCapacityFromPrevious; - } - - /// @notice Validates access criteria for a specific round and access type - /// @dev Checks if a user meets the access requirements based on the round's access criteria + /// @notice Checks if a user meets the access criteria for a specific round and access type + /// @dev Returns true if the user meets the access criteria, reverts otherwise /// @param roundId_ The ID of the round being validated /// @param accessCriteriaId_ The ID of the specific access criteria /// @param merkleProof_ Merkle proof for Merkle tree-based access (optional) /// @param user_ The address of the user to validate - /// @return True if the user meets the access criteria, reverts otherwise - function _validateAccessCriteria( + /// @return isEligible True if the user meets the access criteria, false otherwise + function _checkAccessCriteriaEligibility( uint64 roundId_, uint8 accessCriteriaId_, - bytes32[] calldata merkleProof_, + bytes32[] memory merkleProof_, address user_ - ) internal view returns (bool) { + ) internal view returns (bool isEligible) { + if (accessCriteriaId_ > MAX_ACCESS_CRITERIA_ID) { + isEligible = false; + } + Round storage round = rounds[roundId_]; AccessCriteria storage accessCriteria = round.accessCriterias[accessCriteriaId_]; - if (accessCriteriaId_ > MAX_ACCESS_CRITERIA_ID) { - revert Module__LM_PC_FundingPot__InvalidAccessCriteriaId(); + if (accessCriteria.accessCriteriaType == AccessCriteriaType.OPEN) { + isEligible = true; } - if (accessCriteria.accessCriteriaType == AccessCriteriaType.NFT) { - return _checkNftOwnership(accessCriteria.nftContract, user_); + isEligible = _checkNftOwnership(accessCriteria.nftContract, user_); } else if ( accessCriteria.accessCriteriaType == AccessCriteriaType.MERKLE ) { - return _validateMerkleProof( + isEligible = _validateMerkleProof( accessCriteria.merkleRoot, merkleProof_, user_, roundId_ ); } else if (accessCriteria.accessCriteriaType == AccessCriteriaType.LIST) { - if (!accessCriteria.allowedAddresses[user_]) { - revert Module__LM_PC_FundingPot__AccessCriteriaListFailed(); - } - - return true; + isEligible = accessCriteria.allowedAddresses[user_]; } - // For OPEN access criteria type, no validation needed - return true; + return isEligible; + } + + /// @notice Calculates unused capacity from previous rounds + /// @param roundId_ The ID of the current round + /// @return unusedCapacityFromPrevious The total unused capacity from previous rounds + function _calculateUnusedCapacityFromPreviousRounds(uint64 roundId_) + internal + view + returns (uint unusedCapacityFromPrevious) + { + unusedCapacityFromPrevious = 0; + // Iterate through all previous rounds (1 to roundId_-1) + for (uint64 i = 1; i < roundId_; ++i) { + Round storage prevRound = rounds[i]; + if (!prevRound.globalAccumulativeCaps) continue; + + uint prevRoundTotal = _getTotalRoundContribution(i); + if (prevRoundTotal < prevRound.roundCap) { + unusedCapacityFromPrevious += + (prevRound.roundCap - prevRoundTotal); + } + } + return unusedCapacityFromPrevious; } /// @notice Retrieves the total contribution for a specific round @@ -837,66 +927,6 @@ contract LM_PC_FundingPot_v1 is return userContributions[roundId_][user_]; } - /// @notice Calculates the personal contribution cap for a user in a specific round - /// @dev Determines the maximum amount a user can contribute based on global or round-specific rules - /// @param roundId_ The ID of the current round - /// @param user_ The address of the user - /// @return The personal contribution cap for the user - function _getUserPersonalCapForRound( - uint64 roundId_, - uint8 accessCriteriaId__, - address user_ - ) internal view returns (uint) { - AccessCriteriaPrivileges storage privileges = - accessCriteriaPrivileges[roundId_][accessCriteriaId__]; - - uint personalCap = privileges.personalCap; - - Round storage round = rounds[roundId_]; - if (round.globalAccumulativeCaps) { - uint unusedCapacity = - _getUserUnusedCapacityFromPreviousRounds(user_, roundId_); - return personalCap + unusedCapacity; - } - return personalCap; - } - - /// @notice Calculates unused contribution capacity from previous rounds - /// @dev Aggregates unused contribution caps from previous rounds with global accumulative caps - /// @param user_ The address of the user - /// @param currentRoundId_ The ID of the current round - /// @return Total unused contribution capacity from previous rounds - function _getUserUnusedCapacityFromPreviousRounds( - address user_, - uint64 currentRoundId_ - ) internal view returns (uint) { - uint totalUnusedCapacity = 0; - - for (uint64 i = 1; i < currentRoundId_; ++i) { - Round storage prevRound = rounds[i]; - if (!prevRound.globalAccumulativeCaps) continue; - - uint personalCap = 0; - - // Iterate through all possible access criteria IDs (0 to MAX_ACCESS_CRITERIA_ID) - for (uint8 j = 0; j <= MAX_ACCESS_CRITERIA_ID; ++j) { - AccessCriteriaPrivileges storage privileges = - accessCriteriaPrivileges[i][j]; - - // return only the highest personal cap from the selected access criteria - if (privileges.personalCap > personalCap) { - personalCap = privileges.personalCap; - } - } - - uint userContribution = _getUserContributionToRound(i, user_); - if (userContribution < personalCap) { - totalUnusedCapacity += (personalCap - userContribution); - } - } - return totalUnusedCapacity; - } - /// @notice Verifies NFT ownership for access control /// @dev Safely checks the NFT balance of a user using a try-catch block /// @param nftContract_ Address of the NFT contract @@ -913,11 +943,13 @@ contract LM_PC_FundingPot_v1 is try IERC721(nftContract_).balanceOf(user_) returns (uint balance) { if (balance == 0) { - revert Module__LM_PC_FundingPot__AccessCriteriaNftFailed(); + return false; + // revert Module__LM_PC_FundingPot__AccessCriteriaNftFailed(); } return true; } catch { - revert Module__LM_PC_FundingPot__AccessCriteriaNftFailed(); + return false; + // revert Module__LM_PC_FundingPot__AccessCriteriaNftFailed(); } } @@ -937,7 +969,8 @@ contract LM_PC_FundingPot_v1 is bytes32 leaf = keccak256(abi.encodePacked(user_, roundId_)); if (!MerkleProof.verify(merkleProof_, root_, leaf)) { - revert Module__LM_PC_FundingPot__AccessCriteriaMerkleFailed(); + return false; + // revert Module__LM_PC_FundingPot__AccessCriteriaMerkleFailed(); } return true; diff --git a/src/modules/logicModule/interfaces/ILM_PC_FundingPot_v1.sol b/src/modules/logicModule/interfaces/ILM_PC_FundingPot_v1.sol index 669aea228..40b198276 100644 --- a/src/modules/logicModule/interfaces/ILM_PC_FundingPot_v1.sol +++ b/src/modules/logicModule/interfaces/ILM_PC_FundingPot_v1.sol @@ -55,6 +55,16 @@ interface ILM_PC_FundingPot_v1 is IERC20PaymentClientBase_v2 { uint end; } + /// @notice Struct used to specify previous round's access criteria for carry-over capacity + /// @param roundId The ID of the previous round + /// @param accessCriteriaId The ID of the access criteria in that round + /// @param merkleProof The Merkle proof needed to validate eligibility (if needed) + struct UnspentPersonalRoundCap { + uint64 roundId; + uint8 accessCriteriaId; + bytes32[] merkleProof; + } + // ------------------------------------------------------------------------- // Enums @@ -423,6 +433,20 @@ interface ILM_PC_FundingPot_v1 is IERC20PaymentClientBase_v2 { bytes32[] calldata merkleProof_ ) external; + /// @notice Allows a user to contribute to a round with unused capacity from previous rounds + /// @param roundId_ The ID of the round to contribute to + /// @param amount_ The amount to contribute + /// @param accessCriteriaId_ The ID of the access criteria to use for this contribution + /// @param merkleProof_ The Merkle proof for validation if needed + /// @param unspentPersonalRoundCaps_ Array of previous rounds and access criteria to calculate unused capacity from + function contributeToRound( + uint64 roundId_, + uint amount_, + uint8 accessCriteriaId_, + bytes32[] calldata merkleProof_, + UnspentPersonalRoundCap[] calldata unspentPersonalRoundCaps_ + ) external; + /// @notice Closes a round. /// @param roundId_ The ID of the round to close. function closeRound(uint64 roundId_) external; diff --git a/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol b/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol index 73a331552..484c735d9 100644 --- a/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol +++ b/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol @@ -114,7 +114,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { _setUpOrchestrator(fundingPot); // Initiate the Logic Module with the metadata and config data - fundingPot.init(_orchestrator, _METADATA, abi.encode(address(_token))); + fundingPot.init(_orchestrator, _METADATA, abi.encode("")); _authorizer.setIsAuthorized(address(this), true); @@ -1604,6 +1604,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { uint8 accessId = 1; uint firstAmount = 400; + uint personalCap = 500; ( address nftContract, @@ -1615,7 +1616,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { roundId, accessId, nftContract, merkleRoot, allowedAddresses ); fundingPot.setAccessCriteriaPrivileges( - roundId, accessId, 500, 0, 0, 0, false, 0, 0, 0 + roundId, accessId, personalCap, 0, 0, 0, false, 0, 0, 0 ); mockNFTContract.mint(contributor1_); @@ -1633,11 +1634,6 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { roundId, firstAmount, accessId, new bytes32[](0) ); - // Get the personal cap - uint personalCap = fundingPot.exposed_getUserPersonalCapForRound( - roundId, accessId, contributor1_ - ); - uint secondAmount = 200; vm.prank(contributor1_); @@ -1648,6 +1644,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { uint totalContribution = fundingPot.exposed_getUserContributionToRound( roundId, contributor1_ ); + assertEq(totalContribution, personalCap); } @@ -1698,9 +1695,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { } function testContributeToRound_worksGivenPersonalCapAccumulation() public { - _defaultRoundParams.globalAccumulativeCaps = true; // global accumulative caps enabled - - // Create Round 1 + _defaultRoundParams.globalAccumulativeCaps = true; fundingPot.createRound( _defaultRoundParams.roundStart, _defaultRoundParams.roundEnd, @@ -1713,22 +1708,26 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { uint64 round1Id = fundingPot.getRoundCount(); - uint8 accessId = 1; + uint8 accessCriteriaId = 1; ( address nftContract, bytes32 merkleRoot, address[] memory allowedAddresses - ) = _helper_createAccessCriteria(accessId); - + ) = _helper_createAccessCriteria(accessCriteriaId); fundingPot.setAccessCriteriaForRound( - round1Id, accessId, nftContract, merkleRoot, allowedAddresses + round1Id, + accessCriteriaId, + nftContract, + merkleRoot, + allowedAddresses ); + fundingPot.setAccessCriteriaPrivileges( - round1Id, accessId, 500, 0, 0, 0, false, 0, 0, 0 + round1Id, accessCriteriaId, 500, 0, 0, 0, false, 0, 0, 0 ); + mockNFTContract.mint(contributor1_); - // Create Round 2 fundingPot.createRound( _defaultRoundParams.roundStart + 3 days, _defaultRoundParams.roundEnd + 3 days, @@ -1739,47 +1738,54 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { _defaultRoundParams.globalAccumulativeCaps ); uint64 round2Id = fundingPot.getRoundCount(); + fundingPot.setAccessCriteriaForRound( - round2Id, accessId, nftContract, merkleRoot, allowedAddresses + round2Id, accessCriteriaId, address(0), bytes32(0), allowedAddresses ); + + // Set personal cap of 400 for round 2 fundingPot.setAccessCriteriaPrivileges( - round2Id, accessId, 500, 0, 0, 0, false, 0, 0, 0 + round2Id, accessCriteriaId, 400, 0, 0, 0, false, 0, 0, 0 ); - vm.startPrank(contributor1_); - _token.approve(address(fundingPot), 1500); - - // Contribute to Round 1 vm.warp(_defaultRoundParams.roundStart + 1); - uint round1Contribution = 200; + vm.startPrank(contributor1_); + _token.approve(address(fundingPot), 1000); fundingPot.contributeToRound( - round1Id, round1Contribution, accessId, new bytes32[](0) + round1Id, 200, accessCriteriaId, new bytes32[](0) ); - // Move to Round 2 + // Warp to round 2 vm.warp(_defaultRoundParams.roundStart + 3 days + 1); - // Calculate unused capacity from Round 1 - uint personalCap = fundingPot.exposed_getUserPersonalCapForRound( - round1Id, accessId, contributor1_ - ); + // Create unspent capacity structure + ILM_PC_FundingPot_v1.UnspentPersonalRoundCap[] memory unspentCaps = + new ILM_PC_FundingPot_v1.UnspentPersonalRoundCap[](1); + unspentCaps[0] = ILM_PC_FundingPot_v1.UnspentPersonalRoundCap({ + roundId: round1Id, + accessCriteriaId: accessCriteriaId, + merkleProof: new bytes32[](0) + }); + // Contribute to round 2 with unspent capacity from round 1 fundingPot.contributeToRound( - round2Id, personalCap, accessId, new bytes32[](0) + round2Id, 700, accessCriteriaId, new bytes32[](0), unspentCaps ); vm.stopPrank(); + // Verify contributions are recorded correctly assertEq( fundingPot.exposed_getUserContributionToRound( round1Id, contributor1_ ), - round1Contribution + 200 ); + assertEq( fundingPot.exposed_getUserContributionToRound( round2Id, contributor1_ ), - personalCap + 700 ); } @@ -1912,25 +1918,30 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { } } - function testFuzz_validateAndAdjustCaps( + function testFuzz_validateAndAdjustCapsWithUnspentCap( uint64 roundId_, uint amount_, uint8 accessId_, - bool canOverrideContributionSpan_ + bool canOverrideContributionSpan_, + uint unspentPersonalCap_ ) external { vm.assume(roundId_ > 0 && roundId_ >= fundingPot.getRoundCount()); vm.assume(amount_ <= 1000); vm.assume(accessId_ <= 4); - - try fundingPot.exposed_validateAndAdjustCaps( - roundId_, amount_, accessId_, canOverrideContributionSpan_ + vm.assume(unspentPersonalCap_ >= 0); + + try fundingPot.exposed_validateAndAdjustCapsWithUnspentCap( + roundId_, + amount_, + accessId_, + canOverrideContributionSpan_, + unspentPersonalCap_ ) returns (uint adjustedAmount) { assertLe( adjustedAmount, amount_, "Adjusted amount should be <= amount_" ); assertGe(adjustedAmount, 0, "Adjusted amount should be >= 0"); } catch (bytes memory reason) { - // Compare using keccak256 hash rather than direct string comparison bytes32 roundCapReachedSelector = keccak256( abi.encodeWithSignature( "Module__LM_PC_FundingPot__RoundCapReached()" @@ -1948,7 +1959,6 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { "Should not revert RoundCapReached when canOverrideContributionSpan is true" ); } else if (keccak256(reason) == personalCapReachedSelector) { - // We expect this sometimes assertTrue(true, "Personal cap reached as expected"); } else { assertTrue(false, "Unexpected revert reason"); @@ -1956,224 +1966,6 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { } } - // ------------------------------------------------------------------------- - // Test: _calculateUnusedCapacityFromPreviousRounds - - function test_calculateUnusedCapacityFromPreviousRounds_returnsZeroForFirstRound( - ) public { - // First round should have no unused capacity from previous rounds - uint64 roundId = 1; - uint unusedCapacity = fundingPot - .exposed_calculateUnusedCapacityFromPreviousRounds(roundId); - assertEq(unusedCapacity, 0); - } - - function test_calculateUnusedCapacityFromPreviousRounds_withPartiallyUsedCap( - ) public { - // Create first round with cap 1000 but only use 600 - RoundParams memory params = _defaultRoundParams; - params.roundStart = block.timestamp + 1 days; - params.roundEnd = block.timestamp + 2 days; - params.roundCap = 1000; - params.globalAccumulativeCaps = true; - - uint64 roundId1 = fundingPot.createRound( - params.roundStart, - params.roundEnd, - params.roundCap, - params.hookContract, - params.hookFunction, - params.autoClosure, - params.globalAccumulativeCaps - ); - - // Set access criteria and privileges - uint8 accessId = 0; - ( - address nftContract, - bytes32 merkleRoot, - address[] memory allowedAddresses - ) = _helper_createAccessCriteria(accessId); - fundingPot.setAccessCriteriaForRound( - roundId1, accessId, nftContract, merkleRoot, allowedAddresses - ); - fundingPot.setAccessCriteriaPrivileges( - roundId1, - accessId, - 1000, // personal cap high enough for test - 0, // no NFT cap - 0, // no merkle cap - 0, // no list cap - false, - 0, // no start - 0, // no cliff - 0 // no end - ); - - // Contribute 600 to first round - vm.warp(params.roundStart + 1); - vm.startPrank(contributor1_); - _token.approve(address(fundingPot), 600); - fundingPot.contributeToRound(roundId1, 600, accessId, new bytes32[](0)); - vm.stopPrank(); - - // Check unused capacity for next round (should be 400) - uint64 roundId2 = 2; - uint unusedCapacity = fundingPot - .exposed_calculateUnusedCapacityFromPreviousRounds(roundId2); - assertEq(unusedCapacity, 400); - } - - function test_calculateUnusedCapacityFromPreviousRounds_withMultipleRounds() - public - { - // Create first round with cap 1000, use 600 - RoundParams memory params = _defaultRoundParams; - params.roundStart = block.timestamp + 1 days; - params.roundEnd = block.timestamp + 2 days; - params.roundCap = 1000; - params.globalAccumulativeCaps = true; - - uint64 roundId1 = fundingPot.createRound( - params.roundStart, - params.roundEnd, - params.roundCap, - params.hookContract, - params.hookFunction, - params.autoClosure, - params.globalAccumulativeCaps - ); - - // Set access criteria for round 1 - uint8 accessId = 0; - ( - address nftContract, - bytes32 merkleRoot, - address[] memory allowedAddresses - ) = _helper_createAccessCriteria(accessId); - fundingPot.setAccessCriteriaForRound( - roundId1, accessId, nftContract, merkleRoot, allowedAddresses - ); - fundingPot.setAccessCriteriaPrivileges( - roundId1, - accessId, - 1000, // personal cap high enough for the test - 0, // no NFT cap - 0, // no merkle cap - 0, // no list cap - false, - 0, // no start - 0, // no cliff - 0 // no end - ); - - // Warp to round 1 start time and contribute - vm.warp(params.roundStart + 1); - vm.startPrank(contributor1_); - _token.approve(address(fundingPot), 600); - fundingPot.contributeToRound(roundId1, 600, accessId, new bytes32[](0)); - vm.stopPrank(); - - // Create second round with cap 2000 - uint64 roundId2 = fundingPot.createRound( - params.roundStart + 2 days, // Start when first round ends - params.roundStart + 4 days, // End 2 days after start - 2000, // Cap of 2000 - params.hookContract, - params.hookFunction, - params.autoClosure, - params.globalAccumulativeCaps - ); - - // Set access criteria for round 2 - fundingPot.setAccessCriteriaForRound( - roundId2, accessId, nftContract, merkleRoot, allowedAddresses - ); - fundingPot.setAccessCriteriaPrivileges( - roundId2, - accessId, - 2000, // personal cap high enough for the test - 0, // no NFT cap - 0, // no merkle cap - 0, // no list cap - false, - 0, // no start - 0, // no cliff - 0 // no end - ); - - // Warp to round 2 start time and contribute - vm.warp(params.roundStart + 2 days + 1); - vm.startPrank(contributor2_); - _token.approve(address(fundingPot), 1500); - fundingPot.contributeToRound(roundId2, 1500, accessId, new bytes32[](0)); - vm.stopPrank(); - - // Check unused capacity for third round - // Round 1: 400 unused (1000-600) - // Round 2: 500 unused (2000-1500) - // Total: 900 unused - uint64 roundId3 = 3; - uint unusedCapacity = fundingPot - .exposed_calculateUnusedCapacityFromPreviousRounds(roundId3); - assertEq(unusedCapacity, 900); - } - - function test_calculateUnusedCapacityFromPreviousRounds_withoutGlobalAccumulativeCaps( - ) public { - // Create first round with cap 1000, use 600, but no global accumulative caps - RoundParams memory params = _defaultRoundParams; - params.roundStart = block.timestamp + 1 days; - params.roundEnd = block.timestamp + 2 days; - params.roundCap = 1000; - params.globalAccumulativeCaps = false; - - uint64 roundId1 = fundingPot.createRound( - params.roundStart, - params.roundEnd, - params.roundCap, - params.hookContract, - params.hookFunction, - params.autoClosure, - params.globalAccumulativeCaps - ); - - // Set access criteria and privileges - uint8 accessId = 0; - ( - address nftContract, - bytes32 merkleRoot, - address[] memory allowedAddresses - ) = _helper_createAccessCriteria(accessId); - fundingPot.setAccessCriteriaForRound( - roundId1, accessId, nftContract, merkleRoot, allowedAddresses - ); - fundingPot.setAccessCriteriaPrivileges( - roundId1, - accessId, - 1000, // personal cap high enough for test - 0, // no NFT cap - 0, // no merkle cap - 0, // no list cap - false, - 0, // no start - 0, // no cliff - 0 // no end - ); - - vm.warp(params.roundStart + 1); - vm.startPrank(contributor1_); - _token.approve(address(fundingPot), 600); - fundingPot.contributeToRound(roundId1, 600, accessId, new bytes32[](0)); - vm.stopPrank(); - - // Should return 0 since global accumulative caps is disabled - uint64 roundId2 = 2; - uint unusedCapacity = fundingPot - .exposed_calculateUnusedCapacityFromPreviousRounds(roundId2); - assertEq(unusedCapacity, 0); - } - // ------------------------------------------------------------------------- // Test: _checkRoundClosureConditions diff --git a/test/unit/modules/logicModule/LM_PC_FundingPot_v1_Exposed.sol b/test/unit/modules/logicModule/LM_PC_FundingPot_v1_Exposed.sol index 71f09a037..2ec31a341 100644 --- a/test/unit/modules/logicModule/LM_PC_FundingPot_v1_Exposed.sol +++ b/test/unit/modules/logicModule/LM_PC_FundingPot_v1_Exposed.sol @@ -10,6 +10,9 @@ contract LM_PC_FundingPot_v1_Exposed is LM_PC_FundingPot_v1 { // Use the `exposed_` prefix for functions to expose internal functions for // testing. + /** + * @notice Exposes the internal _getTotalRoundContribution function for testing + */ function exposed_getTotalRoundContributions(uint64 roundId_) external view @@ -18,6 +21,9 @@ contract LM_PC_FundingPot_v1_Exposed is LM_PC_FundingPot_v1 { return _getTotalRoundContribution(roundId_); } + /** + * @notice Exposes the internal _getUserContributionToRound function for testing + */ function exposed_getUserContributionToRound(uint64 roundId_, address user_) external view @@ -26,27 +32,6 @@ contract LM_PC_FundingPot_v1_Exposed is LM_PC_FundingPot_v1 { return _getUserContributionToRound(roundId_, user_); } - /** - * @notice Exposes the internal _getUserPersonalCapForRound function for testing - */ - function exposed_getUserPersonalCapForRound( - uint64 roundId_, - uint8 accessId_, - address user_ - ) external view returns (uint) { - return _getUserPersonalCapForRound(roundId_, accessId_, user_); - } - - /** - * @notice Exposes the internal _getUserUnusedCapacityFromPreviousRounds function for testing - */ - function exposed_getUserUnusedCapacityFromPreviousRounds( - address user_, - uint64 currentRoundId_ - ) external view returns (uint) { - return _getUserUnusedCapacityFromPreviousRounds(user_, currentRoundId_); - } - /** * @notice Exposes the internal _validTimes function for testing */ @@ -59,31 +44,21 @@ contract LM_PC_FundingPot_v1_Exposed is LM_PC_FundingPot_v1 { } /** - * @notice Exposes the internal _validateRoundContribution function for testing + * @notice Exposes the internal _validateAndAdjustCapsWithUnspentCap function for testing */ - function exposed_validateRoundContribution( + function exposed_validateAndAdjustCapsWithUnspentCap( uint64 roundId_, - uint8 accessCriteriaId_, - bytes32[] calldata merkleProof_, uint amount_, - address user_ - ) external view returns (uint) { - return _validateRoundContribution( - roundId_, accessCriteriaId_, merkleProof_, amount_, user_ - ); - } - - /** - * @notice Exposes the internal _validateAndAdjustCaps function for testing - */ - function exposed_validateAndAdjustCaps( - uint64 roundId_, - uint amount_, - uint8 accessId_, - bool canOverrideContributionSpan_ + uint8 accessCriteriaId__, + bool canOverrideContributionSpan_, + uint unspentPersonalCap_ ) external view returns (uint) { - return _validateAndAdjustCaps( - roundId_, amount_, accessId_, canOverrideContributionSpan_ + return _validateAndAdjustCapsWithUnspentCap( + roundId_, + amount_, + accessCriteriaId__, + canOverrideContributionSpan_, + unspentPersonalCap_ ); } @@ -99,6 +74,20 @@ contract LM_PC_FundingPot_v1_Exposed is LM_PC_FundingPot_v1 { _validateAccessCriteria(roundId_, accessId_, merkleProof_, user_); } + /** + * @notice Exposes the internal _checkAccessCriteriaEligibility function for testing + */ + function exposed_checkAccessCriteriaEligibility( + uint64 roundId_, + uint8 accessCriteriaId_, + bytes32[] memory merkleProof_, + address user_ + ) external view returns (bool) { + return _checkAccessCriteriaEligibility( + roundId_, accessCriteriaId_, merkleProof_, user_ + ); + } + /** * @notice Exposes the internal _checkNftOwnership function for testing */ From 892cbeec3c8dd0e83bae1d0793478e8853f1494f Mon Sep 17 00:00:00 2001 From: JeffreyJoel Date: Tue, 15 Apr 2025 15:02:21 +0100 Subject: [PATCH 059/130] fix: remove redundant code --- src/modules/logicModule/LM_PC_FundingPot_v1.sol | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/modules/logicModule/LM_PC_FundingPot_v1.sol b/src/modules/logicModule/LM_PC_FundingPot_v1.sol index 9f5431dfa..6f9e74f1d 100644 --- a/src/modules/logicModule/LM_PC_FundingPot_v1.sol +++ b/src/modules/logicModule/LM_PC_FundingPot_v1.sol @@ -729,8 +729,6 @@ contract LM_PC_FundingPot_v1 is bool readyToClose = _checkRoundClosureConditions(roundId_); if (readyToClose) { _closeRound(roundId_); - } else { - revert Module__LM_PC_FundingPot__ClosureConditionsNotMet(); } } } From 8740dd6a1e23e41c7a074e5bf012d73146241a4e Mon Sep 17 00:00:00 2001 From: JeffreyJoel Date: Thu, 17 Apr 2025 09:20:16 +0100 Subject: [PATCH 060/130] fix: PR Review Fix#3 --- .../logicModule/LM_PC_FundingPot_v1.sol | 146 ++++++++---------- .../interfaces/ILM_PC_FundingPot_v1.sol | 27 +++- .../logicModule/LM_PC_FundingPot_v1.t.sol | 32 ++-- 3 files changed, 95 insertions(+), 110 deletions(-) diff --git a/src/modules/logicModule/LM_PC_FundingPot_v1.sol b/src/modules/logicModule/LM_PC_FundingPot_v1.sol index 6f9e74f1d..31a38dead 100644 --- a/src/modules/logicModule/LM_PC_FundingPot_v1.sol +++ b/src/modules/logicModule/LM_PC_FundingPot_v1.sol @@ -121,16 +121,17 @@ contract LM_PC_FundingPot_v1 is mapping( uint64 roundId => mapping(uint8 accessCriteriaId_ => AccessCriteriaPrivileges) - ) private accessCriteriaPrivileges; + ) private roundItToAccessCriteriaIdToPrivileges; /// @notice Maps round IDs to user addresses to contribution amounts - mapping(uint64 => mapping(address => uint)) private userContributions; + mapping(uint64 => mapping(address => uint)) private + roundIdToUserToContribution; /// @notice Maps round IDs to total contributions - mapping(uint64 => uint) private roundTotalContributions; + mapping(uint64 => uint) private roundIdToTotalContributions; /// @notice Maps round IDs to closed status - mapping(uint64 => bool) private roundClosed; + mapping(uint64 => bool) private roundIdToClosedStatus; /// @notice The current round count. uint64 private roundCount; @@ -195,7 +196,11 @@ contract LM_PC_FundingPot_v1 is } /// @inheritdoc ILM_PC_FundingPot_v1 - function getRoundAccessCriteria(uint64 roundId_, uint8 id_, address user_) + function getRoundAccessCriteria( + uint64 roundId_, + uint8 accessCriteriaId_, + address user_ + ) external view returns ( @@ -206,7 +211,8 @@ contract LM_PC_FundingPot_v1 is ) { Round storage round = rounds[roundId_]; - AccessCriteria storage accessCriteria = round.accessCriterias[id_]; + AccessCriteria storage accessCriteria = + round.accessCriterias[accessCriteriaId_]; if (accessCriteria.accessCriteriaType == AccessCriteriaType.OPEN) { return ( @@ -233,7 +239,6 @@ contract LM_PC_FundingPot_v1 is external view returns ( - bool isRoundOpen_, uint personalCap_, bool overrideContributionSpan_, uint start_, @@ -246,20 +251,19 @@ contract LM_PC_FundingPot_v1 is round.accessCriterias[accessCriteriaId__]; if (accessCriteria.accessCriteriaType == AccessCriteriaType.OPEN) { - return (true, 0, false, 0, 0, 0); + return (0, false, 0, 0, 0); } // Store the privileges in a local variable to reduce stack usage. - AccessCriteriaPrivileges storage priviledges = - accessCriteriaPrivileges[roundId_][accessCriteriaId__]; + AccessCriteriaPrivileges storage privileges = + roundItToAccessCriteriaIdToPrivileges[roundId_][accessCriteriaId__]; return ( - false, - priviledges.personalCap, - priviledges.overrideContributionSpan, - priviledges.start, - priviledges.cliff, - priviledges.end + privileges.personalCap, + privileges.overrideContributionSpan, + privileges.start, + privileges.cliff, + privileges.end ); } @@ -270,7 +274,7 @@ contract LM_PC_FundingPot_v1 is /// @inheritdoc ILM_PC_FundingPot_v1 function isRoundClosed(uint64 roundId_) external view returns (bool) { - return roundClosed[roundId_]; + return roundIdToClosedStatus[roundId_]; } // ------------------------------------------------------------------------- @@ -426,14 +430,33 @@ contract LM_PC_FundingPot_v1 is emit AccessCriteriaEdited(roundId_, accessCriteriaId_); } + function removeAccessCriteriaAddressesForRound( + uint64 roundId_, + uint8 accessCriteriaId_, + address[] calldata addressesToRemove_ + ) external onlyModuleRole(FUNDING_POT_ADMIN_ROLE) { + Round storage round = rounds[roundId_]; + if (accessCriteriaId_ > MAX_ACCESS_CRITERIA_ID) { + revert Module__LM_PC_FundingPot__InvalidAccessCriteriaId(); + } + + _validateEditRoundParameters(round); + + for (uint i = 0; i < addressesToRemove_.length; i++) { + round.accessCriterias[accessCriteriaId_].allowedAddresses[addressesToRemove_[i]] + = false; + } + + emit AccessCriteriaAddressesRemoved( + roundId_, accessCriteriaId_, addressesToRemove_ + ); + } + /// @inheritdoc ILM_PC_FundingPot_v1 function setAccessCriteriaPrivileges( uint64 roundId_, - uint8 accessCriteriaId__, + uint8 accessCriteriaId_, uint personalCap_, - uint capByNFT_, - uint capByMerkle_, - uint capByList_, bool overrideContributionSpan_, uint start_, uint cliff_, @@ -441,53 +464,14 @@ contract LM_PC_FundingPot_v1 is ) external onlyModuleRole(FUNDING_POT_ADMIN_ROLE) { Round storage round = rounds[roundId_]; - uint highestCap; - _validateEditRoundParameters(round); - if ( - round.accessCriterias[accessCriteriaId__].accessCriteriaType - == AccessCriteriaType.OPEN - ) { - highestCap = personalCap_; - } - - if ( - round.accessCriterias[accessCriteriaId__].accessCriteriaType - == AccessCriteriaType.NFT && capByNFT_ > 0 - ) { - uint nftCap = personalCap_ + capByNFT_; - if (nftCap > highestCap) { - highestCap = nftCap; - } - } - - if ( - round.accessCriterias[accessCriteriaId__].accessCriteriaType - == AccessCriteriaType.MERKLE && capByMerkle_ > 0 - ) { - uint merkleCap = personalCap_ + capByMerkle_; - if (merkleCap > highestCap) { - highestCap = merkleCap; - } - } - - if ( - round.accessCriterias[accessCriteriaId__].accessCriteriaType - == AccessCriteriaType.LIST && capByList_ > 0 - ) { - uint listCap = personalCap_ + capByList_; - if (listCap > highestCap) { - highestCap = listCap; - } - } - if (!_validTimes(start_, cliff_, end_)) { revert Module__LM_PC_FundingPot__InvalidTimes(); } AccessCriteriaPrivileges storage accessCriteriaPrivileges = - accessCriteriaPrivileges[roundId_][accessCriteriaId__]; + roundItToAccessCriteriaIdToPrivileges[roundId_][accessCriteriaId_]; accessCriteriaPrivileges.personalCap = personalCap_; accessCriteriaPrivileges.overrideContributionSpan = @@ -498,7 +482,7 @@ contract LM_PC_FundingPot_v1 is emit AccessCriteriaPrivilegesSet( roundId_, - accessCriteriaId__, + accessCriteriaId_, personalCap_, overrideContributionSpan_, start_, @@ -528,7 +512,7 @@ contract LM_PC_FundingPot_v1 is bytes32[] memory merkleProof_, UnspentPersonalRoundCap[] calldata unspentPersonalRoundCaps_ ) external { - uint unspentPersonalCap = 0; + uint unspentPersonalCap; // Process each previous round cap that the user wants to carry over for (uint i = 0; i < unspentPersonalRoundCaps_.length; i++) { @@ -548,7 +532,7 @@ contract LM_PC_FundingPot_v1 is if (isEligible) { AccessCriteriaPrivileges storage privileges = - accessCriteriaPrivileges[roundCap.roundId][roundCap + roundItToAccessCriteriaIdToPrivileges[roundCap.roundId][roundCap .accessCriteriaId]; uint userContribution = @@ -580,7 +564,7 @@ contract LM_PC_FundingPot_v1 is } // Check if round is already closed - if (roundClosed[roundId_]) { + if (roundIdToClosedStatus[roundId_]) { revert Module__LM_PC_FundingPot__RoundHasEnded(); } @@ -688,13 +672,17 @@ contract LM_PC_FundingPot_v1 is revert Module__LM_PC_FundingPot__RoundHasNotStarted(); } + if (accessCriteriaId_ > MAX_ACCESS_CRITERIA_ID) { + revert Module__LM_PC_FundingPot__InvalidAccessCriteriaId(); + } + // Validate access criteria _validateAccessCriteria( roundId_, accessCriteriaId_, merkleProof_, _msgSender() ); AccessCriteriaPrivileges storage privileges = - accessCriteriaPrivileges[roundId_][accessCriteriaId_]; + roundItToAccessCriteriaIdToPrivileges[roundId_][accessCriteriaId_]; bool canOverrideContributionSpan = privileges.overrideContributionSpan; // Allow contributions after the round end if the user can override the contribution span @@ -715,8 +703,8 @@ contract LM_PC_FundingPot_v1 is ); // Record contribution - userContributions[roundId_][_msgSender()] += adjustedAmount; - roundTotalContributions[roundId_] += adjustedAmount; + roundIdToUserToContribution[roundId_][_msgSender()] += adjustedAmount; + roundIdToTotalContributions[roundId_] += adjustedAmount; __Module_orchestrator.fundingManager().token().safeTransferFrom( _msgSender(), address(this), adjustedAmount @@ -725,7 +713,7 @@ contract LM_PC_FundingPot_v1 is emit ContributionMade(roundId_, _msgSender(), adjustedAmount); // contribution triggers automatic closure - if (!roundClosed[roundId_] && round.autoClosure) { + if (!roundIdToClosedStatus[roundId_] && round.autoClosure) { bool readyToClose = _checkRoundClosureConditions(roundId_); if (readyToClose) { _closeRound(roundId_); @@ -754,10 +742,6 @@ contract LM_PC_FundingPot_v1 is ); if (!isEligible) { - if (accessCriteriaId_ > MAX_ACCESS_CRITERIA_ID) { - revert Module__LM_PC_FundingPot__InvalidAccessCriteriaId(); - } - if (accessCriteria.accessCriteriaType == AccessCriteriaType.NFT) { revert Module__LM_PC_FundingPot__AccessCriteriaNftFailed(); } @@ -818,7 +802,7 @@ contract LM_PC_FundingPot_v1 is // Get the base personal cap for this round and criteria AccessCriteriaPrivileges storage privileges = - accessCriteriaPrivileges[roundId_][accessCriteriaId__]; + roundItToAccessCriteriaIdToPrivileges[roundId_][accessCriteriaId__]; uint userPersonalCap = privileges.personalCap; // Add unspent capacity if global accumulative caps are enabled @@ -850,10 +834,6 @@ contract LM_PC_FundingPot_v1 is bytes32[] memory merkleProof_, address user_ ) internal view returns (bool isEligible) { - if (accessCriteriaId_ > MAX_ACCESS_CRITERIA_ID) { - isEligible = false; - } - Round storage round = rounds[roundId_]; AccessCriteria storage accessCriteria = round.accessCriterias[accessCriteriaId_]; @@ -909,7 +889,7 @@ contract LM_PC_FundingPot_v1 is view returns (uint) { - return roundTotalContributions[roundId_]; + return roundIdToTotalContributions[roundId_]; } /// @notice Retrieves the contribution amount for a specific user in a round @@ -922,7 +902,7 @@ contract LM_PC_FundingPot_v1 is view returns (uint) { - return userContributions[roundId_][user_]; + return roundIdToUserToContribution[roundId_][user_]; } /// @notice Verifies NFT ownership for access control @@ -981,7 +961,7 @@ contract LM_PC_FundingPot_v1 is Round storage round = rounds[roundId_]; // Mark round as closed - roundClosed[roundId_] = true; + roundIdToClosedStatus[roundId_] = true; // Execute hook if configured if (round.hookContract != address(0) && round.hookFunction.length > 0) { @@ -993,7 +973,7 @@ contract LM_PC_FundingPot_v1 is // Emit event for round closure emit RoundClosed( - roundId_, block.timestamp, roundTotalContributions[roundId_] + roundId_, block.timestamp, roundIdToTotalContributions[roundId_] ); } @@ -1006,7 +986,7 @@ contract LM_PC_FundingPot_v1 is returns (bool) { Round storage round = rounds[roundId_]; - uint totalContribution = roundTotalContributions[roundId_]; + uint totalContribution = roundIdToTotalContributions[roundId_]; bool capReached = round.roundCap > 0 && totalContribution == round.roundCap; bool timeEnded = round.roundEnd > 0 && block.timestamp >= round.roundEnd; diff --git a/src/modules/logicModule/interfaces/ILM_PC_FundingPot_v1.sol b/src/modules/logicModule/interfaces/ILM_PC_FundingPot_v1.sol index 40b198276..d17b5145b 100644 --- a/src/modules/logicModule/interfaces/ILM_PC_FundingPot_v1.sol +++ b/src/modules/logicModule/interfaces/ILM_PC_FundingPot_v1.sol @@ -167,6 +167,14 @@ interface ILM_PC_FundingPot_v1 is IERC20PaymentClientBase_v2 { uint64 roundId_, uint timestamp_, uint totalContributions_ ); + /// @notice Emitted when addresses are removed from an access criteria's allowed list. + /// @param roundId_ The ID of the round. + /// @param accessCriteriaId_ The ID of the access criteria. + /// @param addressesRemoved_ The addresses that were removed from the allowlist. + event AccessCriteriaAddressesRemoved( + uint64 roundId_, uint8 accessCriteriaId_, address[] addressesRemoved_ + ); + // ------------------------------------------------------------------------- // Errors @@ -291,7 +299,6 @@ interface ILM_PC_FundingPot_v1 is IERC20PaymentClientBase_v2 { /// @notice Retrieves the access criteria privileges for a specific funding round. /// @param roundId_ The unique identifier of the round. /// @param accessCriteriaId_ The identifier of the access criteria. - /// @return isRoundOpen_ Whether anyone can contribute as part of the access criteria. /// @return personalCap_ The personal cap for the access criteria. /// @return overrideContributionSpan_ Whether to override the round contribution span. /// @return start_ The start timestamp for the access criteria. @@ -304,7 +311,6 @@ interface ILM_PC_FundingPot_v1 is IERC20PaymentClientBase_v2 { external view returns ( - bool isRoundOpen_, uint personalCap_, bool overrideContributionSpan_, uint start_, @@ -395,14 +401,22 @@ interface ILM_PC_FundingPot_v1 is IERC20PaymentClientBase_v2 { address[] memory allowedAddresses_ ) external; + /// @notice Removes addresses from the allowed list for a specific access criteria. + /// @dev Only callable by funding pot admin and only before the round has started. + /// @param roundId_ ID of the round. + /// @param accessCriteriaId_ ID of the access criteria. + /// @param addressesToRemove_ List of addresses to remove from the allowed list. + function removeAccessCriteriaAddressesForRound( + uint64 roundId_, + uint8 accessCriteriaId_, + address[] calldata addressesToRemove_ + ) external; + /// @notice Set access criteria privileges. /// @dev Only callable by funding pot admin and only before the round has started. /// @param roundId_ ID of the round. /// @param accessCriteriaId_ ID of the access criteria. /// @param personalCap_ Personal cap for the access criteria. - /// @param capByNFT_ Cap by for the NFT access criteria. - /// @param capByMerkle_ Cap for the Merkle root access criteria. - /// @param capByList_ Cap by for the List access criteria. /// @param overrideContributionSpan_ Whether to override the round contribution span. /// @param start_ Start timestamp for the access criteria. /// @param cliff_ Cliff timestamp for the access criteria. @@ -411,9 +425,6 @@ interface ILM_PC_FundingPot_v1 is IERC20PaymentClientBase_v2 { uint64 roundId_, uint8 accessCriteriaId_, uint personalCap_, - uint capByNFT_, - uint capByMerkle_, - uint capByList_, bool overrideContributionSpan_, uint start_, uint cliff_, diff --git a/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol b/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol index 484c735d9..086ca24ef 100644 --- a/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol +++ b/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol @@ -1234,7 +1234,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { roundId, accessId, nftContract, merkleRoot, allowedAddresses ); fundingPot.setAccessCriteriaPrivileges( - roundId, accessId, 500, 0, 0, 0, false, 0, 0, 0 + roundId, accessId, 500, false, 0, 0, 0 ); // Approve @@ -1274,7 +1274,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { roundId, accessId, nftContract, merkleRoot, allowedAddresses ); fundingPot.setAccessCriteriaPrivileges( - roundId, accessId, 500, 0, 0, 0, false, 0, 0, 0 + roundId, accessId, 500, false, 0, 0, 0 ); (uint roundStart,,,,,,) = fundingPot.getRoundGenericParameters(roundId); @@ -1349,7 +1349,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { roundId, accessId, nftContract, merkleRoot, allowedAddresses ); fundingPot.setAccessCriteriaPrivileges( - roundId, accessId, 500, 0, 0, 0, false, 0, 0, 0 + roundId, accessId, 500, false, 0, 0, 0 ); (uint roundStart,,,,,,) = fundingPot.getRoundGenericParameters(roundId); @@ -1389,7 +1389,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { roundId, accessId, nftContract, merkleRoot, allowedAddresses ); fundingPot.setAccessCriteriaPrivileges( - roundId, accessId, 500, 0, 0, 0, false, 0, 0, 0 + roundId, accessId, 500, false, 0, 0, 0 ); (uint roundStart,,,,,,) = fundingPot.getRoundGenericParameters(roundId); @@ -1431,7 +1431,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { roundId, accessId, nftContract, merkleRoot, allowedAddresses ); fundingPot.setAccessCriteriaPrivileges( - roundId, accessId, 500, 0, 0, 0, false, 0, 0, 0 + roundId, accessId, 500, false, 0, 0, 0 ); mockNFTContract.mint(contributor1_); @@ -1521,7 +1521,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { roundId, accessId, nftContract, merkleRoot, allowedAddresses ); fundingPot.setAccessCriteriaPrivileges( - roundId, accessId, 500, 100, 0, 0, false, 0, 0, 0 + roundId, accessId, 500, false, 0, 0, 0 ); mockNFTContract.mint(contributor1_); @@ -1572,7 +1572,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { roundId, accessId, nftContract, merkleRoot, allowedAddresses ); fundingPot.setAccessCriteriaPrivileges( - roundId, accessId, 200, 0, 0, 0, false, 0, 0, 0 + roundId, accessId, 200, false, 0, 0, 0 ); (uint roundStart,,,,,,) = fundingPot.getRoundGenericParameters(roundId); @@ -1616,7 +1616,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { roundId, accessId, nftContract, merkleRoot, allowedAddresses ); fundingPot.setAccessCriteriaPrivileges( - roundId, accessId, personalCap, 0, 0, 0, false, 0, 0, 0 + roundId, accessId, personalCap, false, 0, 0, 0 ); mockNFTContract.mint(contributor1_); @@ -1669,7 +1669,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { // Set privileges with override capability fundingPot.setAccessCriteriaPrivileges( - roundId, accessId, 500, 200, 0, 0, true, 0, 0, 0 + roundId, accessId, 500, true, 0, 0, 0 ); mockNFTContract.mint(contributor1_); @@ -1723,7 +1723,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { ); fundingPot.setAccessCriteriaPrivileges( - round1Id, accessCriteriaId, 500, 0, 0, 0, false, 0, 0, 0 + round1Id, accessCriteriaId, 500, false, 0, 0, 0 ); mockNFTContract.mint(contributor1_); @@ -1745,7 +1745,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { // Set personal cap of 400 for round 2 fundingPot.setAccessCriteriaPrivileges( - round2Id, accessCriteriaId, 400, 0, 0, 0, false, 0, 0, 0 + round2Id, accessCriteriaId, 400, false, 0, 0, 0 ); vm.warp(_defaultRoundParams.roundStart + 1); @@ -1816,7 +1816,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { round1Id, accessId, nftContract, merkleRoot, allowedAddresses ); fundingPot.setAccessCriteriaPrivileges( - round1Id, accessId, 500, 0, 0, 0, false, 0, 0, 0 + round1Id, accessId, 500, false, 0, 0, 0 ); // Round 2 with a different cap @@ -1835,7 +1835,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { round2Id, accessId, nftContract, merkleRoot, allowedAddresses ); fundingPot.setAccessCriteriaPrivileges( - round2Id, accessId, 500, 0, 0, 0, false, 0, 0, 0 + round2Id, accessId, 500, false, 0, 0, 0 ); // Round 1: Multiple users contribute, but don't reach the cap @@ -1999,9 +1999,6 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { roundId, accessId, 1000, // personal cap equal to round cap - 0, // no NFT cap - 0, // no merkle cap - 0, // no list cap false, 0, // no start 0, // no cliff @@ -2093,9 +2090,6 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { roundId, accessId, 1000, // personal cap equal to round cap - 0, // no NFT cap - 0, // no merkle cap - 0, // no list cap false, 0, // no start 0, // no cliff From 8d18c8550a2e89e4ce15e18f7a76185d3f251c69 Mon Sep 17 00:00:00 2001 From: JeffreyJoel Date: Fri, 18 Apr 2025 15:20:51 +0100 Subject: [PATCH 061/130] fix: PR Review Fix#4 --- .../logicModule/LM_PC_FundingPot_v1.sol | 79 ++++++++++++++++--- .../interfaces/ILM_PC_FundingPot_v1.sol | 42 +++++++--- .../logicModule/LM_PC_FundingPot_v1.t.sol | 12 ++- 3 files changed, 107 insertions(+), 26 deletions(-) diff --git a/src/modules/logicModule/LM_PC_FundingPot_v1.sol b/src/modules/logicModule/LM_PC_FundingPot_v1.sol index 31a38dead..6639526a5 100644 --- a/src/modules/logicModule/LM_PC_FundingPot_v1.sol +++ b/src/modules/logicModule/LM_PC_FundingPot_v1.sol @@ -196,18 +196,14 @@ contract LM_PC_FundingPot_v1 is } /// @inheritdoc ILM_PC_FundingPot_v1 - function getRoundAccessCriteria( - uint64 roundId_, - uint8 accessCriteriaId_, - address user_ - ) + function getRoundAccessCriteria(uint64 roundId_, uint8 accessCriteriaId_) external view returns ( bool isRoundOpen_, address nftContract_, bytes32 merkleRoot_, - bool hasAccess_ + bool isList_ ) { Round storage round = rounds[roundId_]; @@ -221,12 +217,20 @@ contract LM_PC_FundingPot_v1 is accessCriteria.merkleRoot, true ); + } else if (accessCriteria.accessCriteriaType == AccessCriteriaType.LIST) + { + return ( + false, + accessCriteria.nftContract, + accessCriteria.merkleRoot, + true + ); } else { return ( false, accessCriteria.nftContract, accessCriteria.merkleRoot, - accessCriteria.allowedAddresses[user_] + false ); } } @@ -277,6 +281,63 @@ contract LM_PC_FundingPot_v1 is return roundIdToClosedStatus[roundId_]; } + /// @inheritdoc ILM_PC_FundingPot_v1 + function getUserEligibility( + uint64 roundId_, + bytes32[] memory merkleProof_, + address user_ + ) external view returns (RoundUserEligibility memory eligibility) { + Round storage round = rounds[roundId_]; + + if (round.roundEnd == 0 && round.roundCap == 0) { + revert Module__LM_PC_FundingPot__RoundNotCreated(); + } + + for (uint8 i = 0; i <= MAX_ACCESS_CRITERIA_ID; i++) { + AccessCriteria storage accessCriteria = round.accessCriterias[i]; + + if (accessCriteria.accessCriteriaType == AccessCriteriaType.UNSET) { + continue; + } + + bool isEligible = _checkAccessCriteriaEligibility( + roundId_, i, merkleProof_, user_ + ); + + if (isEligible) { + eligibility.isEligible = true; + + if (accessCriteria.accessCriteriaType == AccessCriteriaType.NFT) + { + eligibility.isNftHolder = true; + } else if ( + accessCriteria.accessCriteriaType + == AccessCriteriaType.MERKLE + ) { + eligibility.isInMerkleTree = true; + } else if ( + accessCriteria.accessCriteriaType == AccessCriteriaType.LIST + ) { + eligibility.isInAllowlist = true; + } + + // Check personal cap and contribution span override + AccessCriteriaPrivileges storage privileges = + roundItToAccessCriteriaIdToPrivileges[roundId_][i]; + + if (privileges.personalCap > eligibility.highestPersonalCap) { + eligibility.highestPersonalCap = privileges.personalCap; + } + + if (privileges.overrideContributionSpan) { + eligibility.canOverrideContributionSpan = true; + } + } + } + + return eligibility; + } + // ------------------------------------------------------------------------- // Public - Mutating @@ -430,7 +491,7 @@ contract LM_PC_FundingPot_v1 is emit AccessCriteriaEdited(roundId_, accessCriteriaId_); } - function removeAccessCriteriaAddressesForRound( + function removeAllowlistedAddresses( uint64 roundId_, uint8 accessCriteriaId_, address[] calldata addressesToRemove_ @@ -447,7 +508,7 @@ contract LM_PC_FundingPot_v1 is = false; } - emit AccessCriteriaAddressesRemoved( + emit AllowlistedAddressesRemoved( roundId_, accessCriteriaId_, addressesToRemove_ ); } diff --git a/src/modules/logicModule/interfaces/ILM_PC_FundingPot_v1.sol b/src/modules/logicModule/interfaces/ILM_PC_FundingPot_v1.sol index d17b5145b..1b7565081 100644 --- a/src/modules/logicModule/interfaces/ILM_PC_FundingPot_v1.sol +++ b/src/modules/logicModule/interfaces/ILM_PC_FundingPot_v1.sol @@ -65,6 +65,22 @@ interface ILM_PC_FundingPot_v1 is IERC20PaymentClientBase_v2 { bytes32[] merkleProof; } + /// @notice Struct to represent a user's complete eligibility information for a round + /// @param isEligible Whether the user is eligible for the round through any criteria + /// @param isNftHolder Whether the user is eligible through NFT holding + /// @param isInMerkleTree Whether the user is eligible through Merkle proof + /// @param isInAllowlist Whether the user is eligible through allowlist + /// @param highestPersonalCap The highest personal cap the user can access + /// @param canOverrideContributionSpan Whether the user has any criteria that can override contribution span + struct RoundUserEligibility { + bool isEligible; + bool isNftHolder; + bool isInMerkleTree; + bool isInAllowlist; + uint highestPersonalCap; + bool canOverrideContributionSpan; + } + // ------------------------------------------------------------------------- // Enums @@ -171,7 +187,7 @@ interface ILM_PC_FundingPot_v1 is IERC20PaymentClientBase_v2 { /// @param roundId_ The ID of the round. /// @param accessCriteriaId_ The ID of the access criteria. /// @param addressesRemoved_ The addresses that were removed from the allowlist. - event AccessCriteriaAddressesRemoved( + event AllowlistedAddressesRemoved( uint64 roundId_, uint8 accessCriteriaId_, address[] addressesRemoved_ ); @@ -277,23 +293,18 @@ interface ILM_PC_FundingPot_v1 is IERC20PaymentClientBase_v2 { /// @notice Retrieves the access criteria for a specific funding round. /// @param roundId_ The unique identifier of the round to retrieve. /// @param accessCriteriaId_ The identifier of the access criteria to retrieve. - /// @param user_ The address of the user to check access for. /// @return isRoundOpen_ Whether anyone can contribute as part of the access criteria. /// @return nftContract_ The address of the NFT contract used for access control. /// @return merkleRoot_ The merkle root used for access verification. - /// @return hasAccess_ The list of explicitly allowed addresses. - function getRoundAccessCriteria( - uint64 roundId_, - uint8 accessCriteriaId_, - address user_ - ) + /// @return isList_ If the access criteria is a list, this will be true. + function getRoundAccessCriteria(uint64 roundId_, uint8 accessCriteriaId_) external view returns ( bool isRoundOpen_, address nftContract_, bytes32 merkleRoot_, - bool hasAccess_ + bool isList_ ); /// @notice Retrieves the access criteria privileges for a specific funding round. @@ -327,6 +338,17 @@ interface ILM_PC_FundingPot_v1 is IERC20PaymentClientBase_v2 { /// @return The closed status of the round. function isRoundClosed(uint64 roundId_) external view returns (bool); + /// @notice Gets eligibility information for a user in a specific round + /// @param roundId_ The ID of the round to check eligibility for + /// @param merkleProof_ The Merkle proof for validation if needed + /// @param user_ The address of the user to check + /// @return eligibility Complete eligibility information for the user + function getUserEligibility( + uint64 roundId_, + bytes32[] memory merkleProof_, + address user_ + ) external view returns (RoundUserEligibility memory eligibility); + // ------------------------------------------------------------------------- // Public - Mutating @@ -406,7 +428,7 @@ interface ILM_PC_FundingPot_v1 is IERC20PaymentClientBase_v2 { /// @param roundId_ ID of the round. /// @param accessCriteriaId_ ID of the access criteria. /// @param addressesToRemove_ List of addresses to remove from the allowed list. - function removeAccessCriteriaAddressesForRound( + function removeAllowlistedAddresses( uint64 roundId_, uint8 accessCriteriaId_, address[] calldata addressesToRemove_ diff --git a/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol b/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol index 086ca24ef..6eb05733d 100644 --- a/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol +++ b/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol @@ -1024,16 +1024,14 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { ( bool isOpen, - address retreivedNftContract, - bytes32 retreivedMerkleRoot, + address retrievedNftContract, + bytes32 retrievedMerkleRoot, bool hasAccess - ) = fundingPot.getRoundAccessCriteria( - roundId, accessCriteriaEnum, address(0x2) - ); + ) = fundingPot.getRoundAccessCriteria(roundId, accessCriteriaEnum); assertEq(isOpen, accessCriteriaEnum == 1); - assertEq(retreivedNftContract, nftContract); - assertEq(retreivedMerkleRoot, merkleRoot); + assertEq(retrievedNftContract, nftContract); + assertEq(retrievedMerkleRoot, merkleRoot); if (accessCriteriaEnum == 1 || accessCriteriaEnum == 4) { assertTrue(hasAccess); } else { From 760f6ef95352a770a4ac3cf243d4a866c14f1bfa Mon Sep 17 00:00:00 2001 From: JeffreyJoel Date: Mon, 21 Apr 2025 14:25:47 +0100 Subject: [PATCH 062/130] PR Review Fix#5 --- .../logicModule/LM_PC_FundingPot_v1.sol | 73 +++++++++---------- .../interfaces/ILM_PC_FundingPot_v1.sol | 10 ++- 2 files changed, 44 insertions(+), 39 deletions(-) diff --git a/src/modules/logicModule/LM_PC_FundingPot_v1.sol b/src/modules/logicModule/LM_PC_FundingPot_v1.sol index 6639526a5..043143a97 100644 --- a/src/modules/logicModule/LM_PC_FundingPot_v1.sol +++ b/src/modules/logicModule/LM_PC_FundingPot_v1.sol @@ -284,58 +284,57 @@ contract LM_PC_FundingPot_v1 is /// @inheritdoc ILM_PC_FundingPot_v1 function getUserEligibility( uint64 roundId_, + uint8 accessCriteriaId_, bytes32[] memory merkleProof_, address user_ - ) external view returns (RoundUserEligibility memory eligibility) { + ) + external + view + returns (bool isEligible, uint remainingAmountAllowedToContribute) + { + if (accessCriteriaId_ > MAX_ACCESS_CRITERIA_ID) { + revert Module__LM_PC_FundingPot__InvalidAccessCriteriaId(); + } + Round storage round = rounds[roundId_]; if (round.roundEnd == 0 && round.roundCap == 0) { revert Module__LM_PC_FundingPot__RoundNotCreated(); } - for (uint8 i = 0; i <= MAX_ACCESS_CRITERIA_ID; i++) { - AccessCriteria storage accessCriteria = round.accessCriterias[i]; + AccessCriteria storage accessCriteria = + round.accessCriterias[accessCriteriaId_]; - if (accessCriteria.accessCriteriaType == AccessCriteriaType.UNSET) { - continue; - } + if (accessCriteria.accessCriteriaType == AccessCriteriaType.UNSET) { + return (false, 0); + } - bool isEligible = _checkAccessCriteriaEligibility( - roundId_, i, merkleProof_, user_ - ); + isEligible = _checkAccessCriteriaEligibility( + roundId_, accessCriteriaId_, merkleProof_, user_ + ); - if (isEligible) { - eligibility.isEligible = true; - - if (accessCriteria.accessCriteriaType == AccessCriteriaType.NFT) - { - eligibility.isNftHolder = true; - } else if ( - accessCriteria.accessCriteriaType - == AccessCriteriaType.MERKLE - ) { - eligibility.isInMerkleTree = true; - } else if ( - accessCriteria.accessCriteriaType == AccessCriteriaType.LIST - ) { - eligibility.isInAllowlist = true; - } + if (isEligible) { + AccessCriteriaPrivileges storage privileges = + roundItToAccessCriteriaIdToPrivileges[roundId_][accessCriteriaId_]; + uint userPersonalCap = privileges.personalCap; + uint userContribution = _getUserContributionToRound(roundId_, user_); - // Check personal cap and contribution span override - AccessCriteriaPrivileges storage privileges = - roundItToAccessCriteriaIdToPrivileges[roundId_][i]; + uint personalCapRemaining = userPersonalCap > userContribution + ? userPersonalCap - userContribution + : 0; - if (privileges.personalCap > eligibility.highestPersonalCap) { - eligibility.highestPersonalCap = privileges.personalCap; - } + uint totalContributions = roundIdToTotalContributions[roundId_]; + uint roundCapRemaining = round.roundCap > totalContributions + ? round.roundCap - totalContributions + : 0; - if (privileges.overrideContributionSpan) { - eligibility.canOverrideContributionSpan = true; - } - } - } + remainingAmountAllowedToContribute = personalCapRemaining + < roundCapRemaining ? personalCapRemaining : roundCapRemaining; - return eligibility; + return (true, remainingAmountAllowedToContribute); + } else { + return (false, 0); + } } // ------------------------------------------------------------------------- diff --git a/src/modules/logicModule/interfaces/ILM_PC_FundingPot_v1.sol b/src/modules/logicModule/interfaces/ILM_PC_FundingPot_v1.sol index 1b7565081..b4180d3d3 100644 --- a/src/modules/logicModule/interfaces/ILM_PC_FundingPot_v1.sol +++ b/src/modules/logicModule/interfaces/ILM_PC_FundingPot_v1.sol @@ -340,14 +340,20 @@ interface ILM_PC_FundingPot_v1 is IERC20PaymentClientBase_v2 { /// @notice Gets eligibility information for a user in a specific round /// @param roundId_ The ID of the round to check eligibility for + /// @param accessCriteriaId_ The ID of the access criteria to check eligibility for /// @param merkleProof_ The Merkle proof for validation if needed /// @param user_ The address of the user to check - /// @return eligibility Complete eligibility information for the user + /// @return isEligible Whether the user is eligible for the round through any criteria + /// @return remainingAmountAllowedToContribute The remaining contribution the user can make function getUserEligibility( uint64 roundId_, + uint8 accessCriteriaId_, bytes32[] memory merkleProof_, address user_ - ) external view returns (RoundUserEligibility memory eligibility); + ) + external + view + returns (bool isEligible, uint remainingAmountAllowedToContribute); // ------------------------------------------------------------------------- // Public - Mutating From da88e993d5bdbed108f686e9d2494c02b55d1892 Mon Sep 17 00:00:00 2001 From: Zuhaib Mohammed Date: Fri, 18 Apr 2025 11:00:09 +0530 Subject: [PATCH 063/130] fix: git rebase --- .../logicModule/LM_PC_FundingPot_v1.sol | 192 +++++++++++++++++- .../interfaces/ILM_PC_FundingPot_v1.sol | 18 ++ .../LM_PC_FundingPot_v1ERC20Mock.sol | 12 ++ .../logicModule/LM_PC_FundingPot_v1.t.sol | 66 ++++++ 4 files changed, 287 insertions(+), 1 deletion(-) create mode 100644 test/mocks/modules/logicModule/LM_PC_FundingPot_v1ERC20Mock.sol diff --git a/src/modules/logicModule/LM_PC_FundingPot_v1.sol b/src/modules/logicModule/LM_PC_FundingPot_v1.sol index 043143a97..0918ea968 100644 --- a/src/modules/logicModule/LM_PC_FundingPot_v1.sol +++ b/src/modules/logicModule/LM_PC_FundingPot_v1.sol @@ -14,6 +14,8 @@ import { ERC20PaymentClientBase_v2, Module_v1 } from "@lm/abstracts/ERC20PaymentClientBase_v2.sol"; +import {IBondingCurveBase_v1} from + "@fm/bondingCurve/interfaces/IBondingCurveBase_v1.sol"; // External import {IERC20} from "@oz/token/ERC20/IERC20.sol"; @@ -23,6 +25,7 @@ import {ERC165Upgradeable} from "@oz-up/utils/introspection/ERC165Upgradeable.sol"; import "@oz/utils/cryptography/MerkleProof.sol"; +import {EnumerableSet} from "@oz/utils/structs/EnumerableSet.sol"; /** * @title Inverter Funding Pot Logic Module @@ -133,6 +136,19 @@ contract LM_PC_FundingPot_v1 is /// @notice Maps round IDs to closed status mapping(uint64 => bool) private roundIdToClosedStatus; + /// @notice Maps round IDs to bonding curve tokens bought + mapping(uint64 => uint) private roundTokensBought; + + /// @notice Maps round IDs to contributors recipients + mapping(uint64 => EnumerableSet.AddressSet) private contributorsByRound; + + /// @notice Maps round IDs to user addresses to contribution amounts by access criteria + mapping(uint64 => mapping(address => mapping(uint8 => uint))) private + userContributionsByAccessCriteria; + + /// @notice The token that is being issued by the funding pot. + address public issuanceToken; + /// @notice The current round count. uint64 private roundCount; @@ -340,6 +356,10 @@ contract LM_PC_FundingPot_v1 is // ------------------------------------------------------------------------- // Public - Mutating + function setIssuanceToken(address issuanceToken_) external { + issuanceToken = issuanceToken_; + } + /// @inheritdoc ILM_PC_FundingPot_v1 function createRound( uint roundStart_, @@ -631,6 +651,25 @@ contract LM_PC_FundingPot_v1 is bool readyToClose = _checkRoundClosureConditions(roundId_); if (readyToClose) { _closeRound(roundId_); + + uint totalContributions = _getTotalRoundContribution(roundId_); + + // address fundingManager = + // address(__Module_orchestrator.fundingManager()); + // address issuanceToken = + // address(IBondingCurveBase_v1(fundingManager).getIssuanceToken()); + + uint balanceBefore = IERC20(issuanceToken).balanceOf(address(this)); + IBondingCurveBase_v1(issuanceToken).buyFor( + address(this), totalContributions, 0 + ); + uint balanceAfter = IERC20(issuanceToken).balanceOf(address(this)); + + uint tokensBought = balanceAfter - balanceBefore; + roundTokensBought[roundId_] = tokensBought; + + // TODO: Create payment orders for all contributors based on their access criteria + _createPaymentOrdersForContributors(roundId_); } else { revert Module__LM_PC_FundingPot__ClosureConditionsNotMet(); } @@ -765,11 +804,15 @@ contract LM_PC_FundingPot_v1 is // Record contribution roundIdToUserToContribution[roundId_][_msgSender()] += adjustedAmount; roundIdToTotalContributions[roundId_] += adjustedAmount; + userContributionsByAccessCriteria[roundId_][_msgSender()][accessCriteriaId_] + += adjustedAmount; __Module_orchestrator.fundingManager().token().safeTransferFrom( _msgSender(), address(this), adjustedAmount ); + EnumerableSet.add(contributorsByRound[roundId_], _msgSender()); + emit ContributionMade(roundId_, _msgSender(), adjustedAmount); // contribution triggers automatic closure @@ -777,6 +820,28 @@ contract LM_PC_FundingPot_v1 is bool readyToClose = _checkRoundClosureConditions(roundId_); if (readyToClose) { _closeRound(roundId_); + + uint totalContributions = _getTotalRoundContribution(roundId_); + + // address fundingManager = + // address(__Module_orchestrator.fundingManager()); + // address issuanceToken = address( + // IBondingCurveBase_v1(fundingManager).getIssuanceToken() + // ); + + uint balanceBefore = + IERC20(issuanceToken).balanceOf(address(this)); + IBondingCurveBase_v1(issuanceToken).buyFor( + address(this), totalContributions, 0 + ); + uint balanceAfter = + IERC20(issuanceToken).balanceOf(address(this)); + + uint tokensBought = balanceAfter - balanceBefore; + roundTokensBought[roundId_] = tokensBought; + + // Create payment orders for all contributors based on their access criteria + _createPaymentOrdersForContributors(roundId_); } } } @@ -991,7 +1056,7 @@ contract LM_PC_FundingPot_v1 is } } - /// @notice Verifies a Merkle proof for access control + /// @notice Verifies a Merkle p roof for access control /// @dev Validates that the user's address is part of the Merkle tree /// @param root_ The Merkle root to validate against /// @param user_ The address of the user to check @@ -1037,6 +1102,131 @@ contract LM_PC_FundingPot_v1 is ); } + /// @notice Creates payment orders for all contributors in a round based on their access criteria + /// @dev Loops through all contributors and creates payment orders with appropriate vesting schedules + /// @param roundId_ The ID of the round to create payment orders for + function _createPaymentOrdersForContributors(uint64 roundId_) internal { + Round storage round = rounds[roundId_]; + uint totalContributions = roundIdToTotalContributions[roundId_]; + uint tokensBought = roundTokensBought[roundId_]; + + if (totalContributions == 0 || tokensBought == 0) return; + + address[] memory contributors = + EnumerableSet.values(contributorsByRound[roundId_]); + // address issuanceToken = address( + // IBondingCurveBase_v1( + // address(__Module_orchestrator.fundingManager()) + // ).getIssuanceToken() + // ); + + for (uint i = 0; i < contributors.length; i++) { + address contributor = contributors[i]; + uint contributorTotal = + roundIdToUserToContribution[roundId_][contributor]; + + // Skip if no contribution + if (contributorTotal == 0) continue; + + // Calculate tokens for this contributor proportionally + uint contributorTokens = + (contributorTotal * tokensBought) / totalContributions; + + // Find which access criteria this contributor used and create appropriate payment order + for ( + uint8 accessCriteriaId = 0; + accessCriteriaId <= MAX_ACCESS_CRITERIA_ID; + accessCriteriaId++ + ) { + uint contributionByAccessCriteria = + userContributionsByAccessCriteria[roundId_][contributor][accessCriteriaId]; + + // Skip if no contribution under this access criteria + if (contributionByAccessCriteria == 0) continue; + + // Get privileges for this access criteria + AccessCriteriaPrivileges storage privileges = + roundItToAccessCriteriaIdToPrivileges[roundId_][accessCriteriaId]; + + // Calculate tokens for this specific access criteria contribution + uint tokensForThisAccessCriteria = ( + contributionByAccessCriteria * tokensBought + ) / totalContributions; + + // Determine vesting parameters + uint start = privileges.overrideContributionSpan + ? privileges.start + : round.roundEnd; + uint cliff = + privileges.overrideContributionSpan ? privileges.cliff : 0; + uint end = privileges.overrideContributionSpan + ? privileges.end + : round.roundEnd; + + // If no override and no end time specified, use current time + if (start == 0) start = block.timestamp; + if (end == 0) end = block.timestamp; + + // Prepare flags and data for the payment order + bytes32 flags = 0; + bytes32[] memory data = new bytes32[](3); // For start, cliff, and end + uint8 flagCount = 0; + + // Set start flag (flag 1) + if (start > 0) { + flags |= bytes32(uint(1) << 1); // Flag 1 for start + data[flagCount] = bytes32(start); + flagCount++; + } + + // Set cliff flag (flag 2) + if (cliff > 0) { + flags |= bytes32(uint(1) << 2); // Flag 2 for cliff + data[flagCount] = bytes32(cliff); + flagCount++; + } + + // Set end flag (flag 3) + if (end > 0) { + flags |= bytes32(uint(1) << 3); // Flag 3 for end + data[flagCount] = bytes32(end); + flagCount++; + } + + // Resize data array to match actual flag count + bytes32[] memory finalData = new bytes32[](flagCount); + for (uint8 j = 0; j < flagCount; j++) { + finalData[j] = data[j]; + } + + // Create payment order + IERC20PaymentClientBase_v2.PaymentOrder memory paymentOrder = + IERC20PaymentClientBase_v2.PaymentOrder({ + recipient: contributor, + paymentToken: issuanceToken, + amount: tokensForThisAccessCriteria, + originChainId: 0, // Assuming same chain + targetChainId: 0, // Assuming same chain + flags: flags, + data: finalData + }); + + // Submit payment order to payment processor + _addPaymentOrder(paymentOrder); + + emit PaymentOrderCreated( + roundId_, + contributor, + accessCriteriaId, + tokensForThisAccessCriteria, + start, + cliff, + end + ); + } + } + } + /// @notice Checks if a round has reached its cap or time limit /// @param roundId_ The ID of the round to check /// @return Boolean indicating if the round has reached its cap or time limit diff --git a/src/modules/logicModule/interfaces/ILM_PC_FundingPot_v1.sol b/src/modules/logicModule/interfaces/ILM_PC_FundingPot_v1.sol index b4180d3d3..f2ef56168 100644 --- a/src/modules/logicModule/interfaces/ILM_PC_FundingPot_v1.sol +++ b/src/modules/logicModule/interfaces/ILM_PC_FundingPot_v1.sol @@ -191,6 +191,24 @@ interface ILM_PC_FundingPot_v1 is IERC20PaymentClientBase_v2 { uint64 roundId_, uint8 accessCriteriaId_, address[] addressesRemoved_ ); + /// @notice Emitted when a payment order is created. + /// @param roundId_ The ID of the round. + /// @param contributor_ The address of the contributor. + /// @param accessCriteriaId_ The ID of the access criteria. + /// @param tokensForThisAccessCriteria_ The amount of tokens contributed for this access criteria. + /// @param start_ The start timestamp for for when the linear vesting starts. + /// @param cliff_ The time in seconds from start time at which the unlock starts. + /// @param end_ The end timestamp for when the linear vesting ends. + event PaymentOrderCreated( + uint64 roundId_, + address contributor_, + uint8 accessCriteriaId_, + uint tokensForThisAccessCriteria_, + uint start_, + uint cliff_, + uint end_ + ); + // ------------------------------------------------------------------------- // Errors diff --git a/test/mocks/modules/logicModule/LM_PC_FundingPot_v1ERC20Mock.sol b/test/mocks/modules/logicModule/LM_PC_FundingPot_v1ERC20Mock.sol new file mode 100644 index 000000000..7d4b9320e --- /dev/null +++ b/test/mocks/modules/logicModule/LM_PC_FundingPot_v1ERC20Mock.sol @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: LGPL-3.0-only +pragma solidity ^0.8.0; + +import {ERC20} from "@oz/token/ERC20/ERC20.sol"; + +contract LM_PC_FundingPot_v1ERC20Mock is ERC20 { + constructor(string memory name, string memory symbol) ERC20(name, symbol) {} + + function buyFor(address to, uint value, uint minTokens) public { + _mint(to, value); + } +} diff --git a/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol b/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol index 6eb05733d..90512222d 100644 --- a/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol +++ b/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol @@ -20,6 +20,8 @@ import { } from "test/utils/mocks/modules/paymentClient/ERC20PaymentClientBaseV2Mock.sol"; import {ERC721Mock} from "test/utils/mocks/modules/logicModules/LM_PC_FundingPot_v2NFTMock.sol"; +import {LM_PC_FundingPot_v1ERC20Mock} from + "test/utils/mocks/modules/logicModules/LM_PC_FundingPot_v1ERC20Mock.sol"; // System under Test (SuT) import {LM_PC_FundingPot_v1_Exposed} from @@ -93,9 +95,12 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { } ERC721Mock mockNFTContract = new ERC721Mock("NFT Mock", "NFT"); + LM_PC_FundingPot_v1ERC20Mock issuanceERC20Token = + new LM_PC_FundingPot_v1ERC20Mock("ERC20 Mock", "ERC20"); // ------------------------------------------------------------------------- // Setup + function setUp() public { // Deploy the SuT address impl = address(new LM_PC_FundingPot_v1_Exposed()); @@ -1898,6 +1903,67 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { } // ------------------------------------------------------------------------- + // Test: closeRound() + + function testCloseRound_testWorks() public { + fundingPot.setIssuanceToken(address(issuanceERC20Token)); + + testCreateRound(); + + uint64 roundId = fundingPot.getRoundCount(); + uint8 accessId = 1; + uint amount = 1000; + + ( + address nftContract, + bytes32 merkleRoot, + address[] memory allowedAddresses + ) = _helper_createAccessCriteria(accessId); + + fundingPot.setAccessCriteriaForRound( + roundId, accessId, nftContract, merkleRoot, allowedAddresses + ); + fundingPot.setAccessCriteriaPrivileges( + roundId, accessId, 1000, false, 0, 0, 0 + ); + mockNFTContract.mint(contributor1_); + + (uint roundStart,,,,,,) = fundingPot.getRoundGenericParameters(roundId); + vm.warp(roundStart + 1); + + // Approve + vm.prank(contributor1_); + _token.approve(address(fundingPot), 1000); + + vm.prank(contributor1_); + fundingPot.contributeToRound( + roundId, amount, accessId, new bytes32[](0) + ); + + uint totalContributions = + fundingPot.exposed_getTotalRoundContributions(roundId); + + assertEq(totalContributions, amount); + + uint personalContributions = fundingPot + .exposed_getUserContributionToRound(roundId, contributor1_); + assertEq(personalContributions, amount); + + fundingPot.closeRound(roundId); + + console2.log("-----------------CLOSE ROUND------------"); + console2.log( + "balnce of issuanceToken: ", + issuanceERC20Token.balanceOf(address(fundingPot)) + ); + console2.log( + "balnce of contributor1: ", + issuanceERC20Token.balanceOf(contributor1_) + ); + } + + // ------------------------------------------------------------------------- + // Internal Functions function testFuzz_validateAccessCriteria( uint64 roundId_, From d79bd6e724acc79ebf392d080c2e642b21ccfe18 Mon Sep 17 00:00:00 2001 From: Zuhaib Mohammed Date: Fri, 18 Apr 2025 15:36:06 +0530 Subject: [PATCH 064/130] test: add unit tests closeRound --- .../logicModule/LM_PC_FundingPot_v1.sol | 15 +- .../logicModule/LM_PC_FundingPot_v1.t.sol | 260 +++++++++++++++++- 2 files changed, 254 insertions(+), 21 deletions(-) diff --git a/src/modules/logicModule/LM_PC_FundingPot_v1.sol b/src/modules/logicModule/LM_PC_FundingPot_v1.sol index 0918ea968..5d28d9412 100644 --- a/src/modules/logicModule/LM_PC_FundingPot_v1.sol +++ b/src/modules/logicModule/LM_PC_FundingPot_v1.sol @@ -144,7 +144,7 @@ contract LM_PC_FundingPot_v1 is /// @notice Maps round IDs to user addresses to contribution amounts by access criteria mapping(uint64 => mapping(address => mapping(uint8 => uint))) private - userContributionsByAccessCriteria; + roundIdTouserContributionsByAccessCriteria; /// @notice The token that is being issued by the funding pot. address public issuanceToken; @@ -635,7 +635,10 @@ contract LM_PC_FundingPot_v1 is } /// @inheritdoc ILM_PC_FundingPot_v1 - function closeRound(uint64 roundId_) external { + function closeRound(uint64 roundId_) + external + onlyModuleRole(FUNDING_POT_ADMIN_ROLE) + { Round storage round = rounds[roundId_]; // Validate round exists @@ -804,7 +807,7 @@ contract LM_PC_FundingPot_v1 is // Record contribution roundIdToUserToContribution[roundId_][_msgSender()] += adjustedAmount; roundIdToTotalContributions[roundId_] += adjustedAmount; - userContributionsByAccessCriteria[roundId_][_msgSender()][accessCriteriaId_] + roundIdTouserContributionsByAccessCriteria[roundId_][_msgSender()][accessCriteriaId_] += adjustedAmount; __Module_orchestrator.fundingManager().token().safeTransferFrom( @@ -1139,7 +1142,7 @@ contract LM_PC_FundingPot_v1 is accessCriteriaId++ ) { uint contributionByAccessCriteria = - userContributionsByAccessCriteria[roundId_][contributor][accessCriteriaId]; + roundIdTouserContributionsByAccessCriteria[roundId_][contributor][accessCriteriaId]; // Skip if no contribution under this access criteria if (contributionByAccessCriteria == 0) continue; @@ -1156,7 +1159,7 @@ contract LM_PC_FundingPot_v1 is // Determine vesting parameters uint start = privileges.overrideContributionSpan ? privileges.start - : round.roundEnd; + : round.roundStart; uint cliff = privileges.overrideContributionSpan ? privileges.cliff : 0; uint end = privileges.overrideContributionSpan @@ -1211,7 +1214,7 @@ contract LM_PC_FundingPot_v1 is data: finalData }); - // Submit payment order to payment processor + // Add payment order to payment processor _addPaymentOrder(paymentOrder); emit PaymentOrderCreated( diff --git a/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol b/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol index 90512222d..125b081f5 100644 --- a/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol +++ b/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol @@ -1905,7 +1905,184 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { // ------------------------------------------------------------------------- // Test: closeRound() - function testCloseRound_testWorks() public { + /* + ├── Given user does not have FUNDING_POT_ADMIN_ROLE + │ └── When user attempts to close a round + │ └── Then it should revert with Module__CallerNotAuthorized + │ + ├── Given round does not exist + │ └── When user attempts to close the round + │ └── Then it should revert with Module__LM_PC_FundingPot__RoundNotCreated + │ + ├── Given round is already closed + │ └── When user attempts to close the round again + │ └── Then it should revert with Module__LM_PC_FundingPot__RoundHasEnded + │ + ├── Given round has started but not ended + │ └── And round cap has not been reached + │ └── And user has contributed successfully + │ └── When user attempts to close the round + │ └── Then it should not revert and round should be closed + │ └── And payment orders should be created correctly + │ + ├── Given round has ended (by time) + │ └── And user has contributed during active round + │ └── When user attempts to close the round + │ └── Then it should not revert and round should be closed + │ └── And payment orders should be created correctly + │ + ├── Given round cap has been reached + │ └── And user has contributed up to the cap + │ └── When user attempts to close the round + │ └── Then it should not revert and round should be closed + │ └── And payment orders should be created correctly + │ + └── Given multiple users contributed before round ended or cap reached + └── When round is closed + └── Then it should not revert and round should be closed + └── And payment orders should be created for all contributors + */ + function testCloseRound_revertsGivenUserIsNotFundingPotAdmin(address user_) + public + { + vm.assume(user_ != address(0) && user_ != address(this)); + + testCreateRound(); + uint64 roundId = fundingPot.getRoundCount(); + + vm.startPrank(user_); + bytes32 roleId = _authorizer.generateRoleId( + address(fundingPot), fundingPot.FUNDING_POT_ADMIN_ROLE() + ); + vm.expectRevert( + abi.encodeWithSelector( + IModule_v1.Module__CallerNotAuthorized.selector, roleId, user_ + ) + ); + fundingPot.closeRound(roundId); + vm.stopPrank(); + } + + function testCloseRound_revertsGivenRoundDoesNotExist() public { + uint64 nonExistentRoundId = 999; + + vm.expectRevert( + abi.encodeWithSelector( + ILM_PC_FundingPot_v1 + .Module__LM_PC_FundingPot__RoundNotCreated + .selector + ) + ); + fundingPot.closeRound(nonExistentRoundId); + } + + function testCloseRound_revertsGivenRoundIsAlreadyClosed() public { + testCloseRound_worksGivenRoundCapHasBeenReached(); + // Try to close it again + uint64 roundId = fundingPot.getRoundCount(); + + vm.expectRevert( + abi.encodeWithSelector( + ILM_PC_FundingPot_v1 + .Module__LM_PC_FundingPot__RoundHasEnded + .selector + ) + ); + fundingPot.closeRound(roundId); + } + + function testCloseRound_worksGivenRoundHasStartedButNotEnded() public { + fundingPot.setIssuanceToken(address(issuanceERC20Token)); + + testCreateRound(); + uint64 roundId = fundingPot.getRoundCount(); + + // Set up access criteria + uint8 accessId = 1; + ( + address nftContract, + bytes32 merkleRoot, + address[] memory allowedAddresses + ) = _helper_createAccessCriteria(accessId); + + fundingPot.setAccessCriteriaForRound( + roundId, accessId, nftContract, merkleRoot, allowedAddresses + ); + fundingPot.setAccessCriteriaPrivileges( + roundId, accessId, 1000, false, 0, 0, 0 + ); + + // Warp to round start + (uint roundStart,,,,,,) = fundingPot.getRoundGenericParameters(roundId); + vm.warp(roundStart + 1); + + // Make a contribution + vm.startPrank(contributor1_); + _token.approve(address(fundingPot), 1000); + fundingPot.contributeToRound(roundId, 1000, accessId, new bytes32[](0)); + vm.stopPrank(); + + // Close the round + fundingPot.closeRound(roundId); + + // Verify round is closed + assertEq(fundingPot.isRoundClosed(roundId), true); + + // Verify payment orders + IERC20PaymentClientBase_v2.PaymentOrder[] memory orders = + fundingPot.paymentOrders(); + assertEq(orders.length, 1); + assertEq(orders[0].amount, 1000); + } + + function testCloseRound_worksGivenRoundHasEnded() public { + fundingPot.setIssuanceToken(address(issuanceERC20Token)); + + testCreateRound(); + uint64 roundId = fundingPot.getRoundCount(); + + // Set up access criteria + uint8 accessId = 1; + ( + address nftContract, + bytes32 merkleRoot, + address[] memory allowedAddresses + ) = _helper_createAccessCriteria(accessId); + + fundingPot.setAccessCriteriaForRound( + roundId, accessId, nftContract, merkleRoot, allowedAddresses + ); + fundingPot.setAccessCriteriaPrivileges( + roundId, accessId, 1000, false, 0, 0, 0 + ); + + // Make a contribution + (uint roundStart, uint roundEnd,,,,,) = + fundingPot.getRoundGenericParameters(roundId); + vm.warp(roundStart + 1); + + vm.startPrank(contributor1_); + _token.approve(address(fundingPot), 500); + fundingPot.contributeToRound(roundId, 500, accessId, new bytes32[](0)); + vm.stopPrank(); + + // Warp to after round end + vm.warp(roundEnd + 1); + + // Close the round + fundingPot.closeRound(roundId); + + // Verify round is closed + assertEq(fundingPot.isRoundClosed(roundId), true); + + // Verify payment orders + IERC20PaymentClientBase_v2.PaymentOrder[] memory orders = + fundingPot.paymentOrders(); + assertEq(orders.length, 1); + assertEq(orders[0].amount, 500); + } + + function testCloseRound_worksGivenRoundCapHasBeenReached() public { fundingPot.setIssuanceToken(address(issuanceERC20Token)); testCreateRound(); @@ -1940,26 +2117,79 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { roundId, amount, accessId, new bytes32[](0) ); - uint totalContributions = - fundingPot.exposed_getTotalRoundContributions(roundId); + assertEq(fundingPot.isRoundClosed(roundId), false); + fundingPot.closeRound(roundId); + assertEq(fundingPot.isRoundClosed(roundId), true); - assertEq(totalContributions, amount); + // Get the payment orders and store them in a variable + IERC20PaymentClientBase_v2.PaymentOrder[] memory orders = + fundingPot.paymentOrders(); - uint personalContributions = fundingPot - .exposed_getUserContributionToRound(roundId, contributor1_); - assertEq(personalContributions, amount); + assertEq(orders.length, 1); - fundingPot.closeRound(roundId); + // // Now you can use the orders array + // console2.log("Number of payment orders:", orders.length); + + // If you want to log details of each order + for (uint i = 0; i < orders.length; i++) { + console2.log("Order", i, "recipient:", orders[i].recipient); + console2.log("Order", i, "amount:", orders[i].amount); + console2.logBytes32(orders[i].flags); + // Log other fields as needed + } + } + + function testCloseRound_worksWithMultipleContributors() public { + fundingPot.setIssuanceToken(address(issuanceERC20Token)); + + testCreateRound(); + uint64 roundId = fundingPot.getRoundCount(); - console2.log("-----------------CLOSE ROUND------------"); - console2.log( - "balnce of issuanceToken: ", - issuanceERC20Token.balanceOf(address(fundingPot)) + // Set up access criteria + uint8 accessId = 0; // Using OPEN access criteria for simplicity + ( + address nftContract, + bytes32 merkleRoot, + address[] memory allowedAddresses + ) = _helper_createAccessCriteria(accessId); + + fundingPot.setAccessCriteriaForRound( + roundId, accessId, nftContract, merkleRoot, allowedAddresses ); - console2.log( - "balnce of contributor1: ", - issuanceERC20Token.balanceOf(contributor1_) + fundingPot.setAccessCriteriaPrivileges( + roundId, accessId, 1000, false, 0, 0, 0 ); + + // Warp to round start + (uint roundStart,,,,,,) = fundingPot.getRoundGenericParameters(roundId); + vm.warp(roundStart + 1); + + // Multiple contributors + vm.startPrank(contributor1_); + _token.approve(address(fundingPot), 500); + fundingPot.contributeToRound(roundId, 500, accessId, new bytes32[](0)); + vm.stopPrank(); + + vm.startPrank(contributor2_); + _token.approve(address(fundingPot), 200); + fundingPot.contributeToRound(roundId, 200, accessId, new bytes32[](0)); + vm.stopPrank(); + + vm.startPrank(contributor3_); + _token.approve(address(fundingPot), 300); + fundingPot.contributeToRound(roundId, 300, accessId, new bytes32[](0)); + vm.stopPrank(); + + // Close the round + fundingPot.closeRound(roundId); + + // Verify round is closed + assertEq(fundingPot.isRoundClosed(roundId), true); + + // Verify payment orders + IERC20PaymentClientBase_v2.PaymentOrder[] memory orders = + fundingPot.paymentOrders(); + assertEq(orders.length, 3); } // ------------------------------------------------------------------------- From 23ac110e800a5aa86ed57076a0c2e2cab4028922 Mon Sep 17 00:00:00 2001 From: Zuhaib Mohammed Date: Fri, 18 Apr 2025 16:36:39 +0530 Subject: [PATCH 065/130] fix: code format and unit tests --- .../logicModule/LM_PC_FundingPot_v1.sol | 56 +++++++---------- .../logicModule/LM_PC_FundingPot_v1.t.sol | 63 +++++++++++++++---- 2 files changed, 73 insertions(+), 46 deletions(-) diff --git a/src/modules/logicModule/LM_PC_FundingPot_v1.sol b/src/modules/logicModule/LM_PC_FundingPot_v1.sol index 5d28d9412..8778fadc6 100644 --- a/src/modules/logicModule/LM_PC_FundingPot_v1.sol +++ b/src/modules/logicModule/LM_PC_FundingPot_v1.sol @@ -655,21 +655,8 @@ contract LM_PC_FundingPot_v1 is if (readyToClose) { _closeRound(roundId_); - uint totalContributions = _getTotalRoundContribution(roundId_); - - // address fundingManager = - // address(__Module_orchestrator.fundingManager()); - // address issuanceToken = - // address(IBondingCurveBase_v1(fundingManager).getIssuanceToken()); - - uint balanceBefore = IERC20(issuanceToken).balanceOf(address(this)); - IBondingCurveBase_v1(issuanceToken).buyFor( - address(this), totalContributions, 0 - ); - uint balanceAfter = IERC20(issuanceToken).balanceOf(address(this)); - - uint tokensBought = balanceAfter - balanceBefore; - roundTokensBought[roundId_] = tokensBought; + // Buy the bonding curve token + _buyBondingCurveToken(roundId_); // TODO: Create payment orders for all contributors based on their access criteria _createPaymentOrdersForContributors(roundId_); @@ -824,24 +811,8 @@ contract LM_PC_FundingPot_v1 is if (readyToClose) { _closeRound(roundId_); - uint totalContributions = _getTotalRoundContribution(roundId_); - - // address fundingManager = - // address(__Module_orchestrator.fundingManager()); - // address issuanceToken = address( - // IBondingCurveBase_v1(fundingManager).getIssuanceToken() - // ); - - uint balanceBefore = - IERC20(issuanceToken).balanceOf(address(this)); - IBondingCurveBase_v1(issuanceToken).buyFor( - address(this), totalContributions, 0 - ); - uint balanceAfter = - IERC20(issuanceToken).balanceOf(address(this)); - - uint tokensBought = balanceAfter - balanceBefore; - roundTokensBought[roundId_] = tokensBought; + // Buy the bonding curve token + _buyBondingCurveToken(roundId_); // Create payment orders for all contributors based on their access criteria _createPaymentOrdersForContributors(roundId_); @@ -1230,6 +1201,25 @@ contract LM_PC_FundingPot_v1 is } } + function _buyBondingCurveToken(uint64 roundId_) internal { + uint totalContributions = _getTotalRoundContribution(roundId_); + + // address fundingManager = + // address(__Module_orchestrator.fundingManager()); + // address issuanceToken = address( + // IBondingCurveBase_v1(fundingManager).getIssuanceToken() + // ); + + uint balanceBefore = IERC20(issuanceToken).balanceOf(address(this)); + IBondingCurveBase_v1(issuanceToken).buyFor( + address(this), totalContributions, 0 + ); + uint balanceAfter = IERC20(issuanceToken).balanceOf(address(this)); + + uint tokensBought = balanceAfter - balanceBefore; + roundTokensBought[roundId_] = tokensBought; + } + /// @notice Checks if a round has reached its cap or time limit /// @param roundId_ The ID of the round to check /// @return Boolean indicating if the round has reached its cap or time limit diff --git a/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol b/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol index 125b081f5..bd5a28252 100644 --- a/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol +++ b/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol @@ -1936,11 +1936,15 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { │ └── When user attempts to close the round │ └── Then it should not revert and round should be closed │ └── And payment orders should be created correctly - │ + -── Given round cap has been reached + │ └── And the round is set up for autoclosure + │ └── And user has contributed up to the cap + │ └── Then it should not revert and round should be closed + │ └── And payment orders should be created correctly └── Given multiple users contributed before round ended or cap reached - └── When round is closed - └── Then it should not revert and round should be closed - └── And payment orders should be created for all contributors + └── When round is closed + └── Then it should not revert and round should be closed + └── And payment orders should be created for all contributors */ function testCloseRound_revertsGivenUserIsNotFundingPotAdmin(address user_) public @@ -2126,17 +2130,50 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { fundingPot.paymentOrders(); assertEq(orders.length, 1); + } - // // Now you can use the orders array - // console2.log("Number of payment orders:", orders.length); + function testCloseRound_worksGivenRoundisAutoClosure() public { + fundingPot.setIssuanceToken(address(issuanceERC20Token)); - // If you want to log details of each order - for (uint i = 0; i < orders.length; i++) { - console2.log("Order", i, "recipient:", orders[i].recipient); - console2.log("Order", i, "amount:", orders[i].amount); - console2.logBytes32(orders[i].flags); - // Log other fields as needed - } + testEditRound(); + + uint64 roundId = fundingPot.getRoundCount(); + uint8 accessId = 1; + uint amount = 2000; + + ( + address nftContract, + bytes32 merkleRoot, + address[] memory allowedAddresses + ) = _helper_createAccessCriteria(accessId); + + fundingPot.setAccessCriteriaForRound( + roundId, accessId, nftContract, merkleRoot, allowedAddresses + ); + fundingPot.setAccessCriteriaPrivileges( + roundId, accessId, 2000, false, 0, 0, 0 + ); + mockNFTContract.mint(contributor1_); + + (uint roundStart,,,,,,) = fundingPot.getRoundGenericParameters(roundId); + vm.warp(roundStart + 1); + + // Approve + vm.prank(contributor1_); + _token.approve(address(fundingPot), 2000); + + vm.prank(contributor1_); + fundingPot.contributeToRound( + roundId, amount, accessId, new bytes32[](0) + ); + + assertEq(fundingPot.isRoundClosed(roundId), true); + + // Get the payment orders and store them in a variable + IERC20PaymentClientBase_v2.PaymentOrder[] memory orders = + fundingPot.paymentOrders(); + + assertEq(orders.length, 1); } function testCloseRound_worksWithMultipleContributors() public { From ae54934622f021c84e949606fdcbe02ffa24ba1d Mon Sep 17 00:00:00 2001 From: Zuhaib Mohammed Date: Fri, 18 Apr 2025 23:06:54 +0530 Subject: [PATCH 066/130] fix: mock the bonding curve token --- .../logicModule/LM_PC_FundingPot_v1.sol | 24 ++++++++++++------- .../LM_PC_FundingPot_v1ERC20Mock.sol | 8 +++++++ .../logicModule/LM_PC_FundingPot_v1.t.sol | 14 +++-------- 3 files changed, 26 insertions(+), 20 deletions(-) diff --git a/src/modules/logicModule/LM_PC_FundingPot_v1.sol b/src/modules/logicModule/LM_PC_FundingPot_v1.sol index 8778fadc6..93e7cc2b1 100644 --- a/src/modules/logicModule/LM_PC_FundingPot_v1.sol +++ b/src/modules/logicModule/LM_PC_FundingPot_v1.sol @@ -16,6 +16,8 @@ import { } from "@lm/abstracts/ERC20PaymentClientBase_v2.sol"; import {IBondingCurveBase_v1} from "@fm/bondingCurve/interfaces/IBondingCurveBase_v1.sol"; +import {FM_BC_Bancor_Redeeming_VirtualSupply_v1} from + "src/modules/fundingManager/bondingCurve/FM_BC_Bancor_Redeeming_VirtualSupply_v1.sol"; // External import {IERC20} from "@oz/token/ERC20/IERC20.sol"; @@ -146,8 +148,8 @@ contract LM_PC_FundingPot_v1 is mapping(uint64 => mapping(address => mapping(uint8 => uint))) private roundIdTouserContributionsByAccessCriteria; - /// @notice The token that is being issued by the funding pot. - address public issuanceToken; + /// @notice Bancor Bonding Curve Funding Manager + FM_BC_Bancor_Redeeming_VirtualSupply_v1 bancorFM; /// @notice The current round count. uint64 private roundCount; @@ -180,6 +182,9 @@ contract LM_PC_FundingPot_v1 is flags |= bytes32(1 << FLAG_END); __ERC20PaymentClientBase_v2_init(flags); + + address bancorFMaddress = abi.decode(configData_, (address)); + bancorFM = FM_BC_Bancor_Redeeming_VirtualSupply_v1(bancorFMaddress); } // ------------------------------------------------------------------------- @@ -356,10 +361,6 @@ contract LM_PC_FundingPot_v1 is // ------------------------------------------------------------------------- // Public - Mutating - function setIssuanceToken(address issuanceToken_) external { - issuanceToken = issuanceToken_; - } - /// @inheritdoc ILM_PC_FundingPot_v1 function createRound( uint roundStart_, @@ -1088,11 +1089,14 @@ contract LM_PC_FundingPot_v1 is address[] memory contributors = EnumerableSet.values(contributorsByRound[roundId_]); + // address issuanceToken = address( // IBondingCurveBase_v1( // address(__Module_orchestrator.fundingManager()) // ).getIssuanceToken() // ); + //@note: This is for testing purpose, the above snippet should be used to fetch the token address, talk to Fabi! + address issuanceToken = bancorFM.getIssuanceToken(); for (uint i = 0; i < contributors.length; i++) { address contributor = contributors[i]; @@ -1204,12 +1208,14 @@ contract LM_PC_FundingPot_v1 is function _buyBondingCurveToken(uint64 roundId_) internal { uint totalContributions = _getTotalRoundContribution(roundId_); - // address fundingManager = - // address(__Module_orchestrator.fundingManager()); // address issuanceToken = address( - // IBondingCurveBase_v1(fundingManager).getIssuanceToken() + // IBondingCurveBase_v1( + // address(__Module_orchestrator.fundingManager()) + // ).getIssuanceToken() // ); + address issuanceToken = bancorFM.getIssuanceToken(); + uint balanceBefore = IERC20(issuanceToken).balanceOf(address(this)); IBondingCurveBase_v1(issuanceToken).buyFor( address(this), totalContributions, 0 diff --git a/test/mocks/modules/logicModule/LM_PC_FundingPot_v1ERC20Mock.sol b/test/mocks/modules/logicModule/LM_PC_FundingPot_v1ERC20Mock.sol index 7d4b9320e..b1df65d8f 100644 --- a/test/mocks/modules/logicModule/LM_PC_FundingPot_v1ERC20Mock.sol +++ b/test/mocks/modules/logicModule/LM_PC_FundingPot_v1ERC20Mock.sol @@ -9,4 +9,12 @@ contract LM_PC_FundingPot_v1ERC20Mock is ERC20 { function buyFor(address to, uint value, uint minTokens) public { _mint(to, value); } + + function token() public view returns (address) { + return address(this); + } + + function getIssuanceToken() public view returns (address) { + return address(this); + } } diff --git a/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol b/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol index bd5a28252..3848606d9 100644 --- a/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol +++ b/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol @@ -119,7 +119,9 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { _setUpOrchestrator(fundingPot); // Initiate the Logic Module with the metadata and config data - fundingPot.init(_orchestrator, _METADATA, abi.encode("")); + fundingPot.init( + _orchestrator, _METADATA, abi.encode(address(issuanceERC20Token)) + ); // @note: The address of issuancetoken is being passed for testing purpose as of now, We arent actually sure how to use the mock contract alongside the fundingpot contract. _authorizer.setIsAuthorized(address(this), true); @@ -1996,8 +1998,6 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { } function testCloseRound_worksGivenRoundHasStartedButNotEnded() public { - fundingPot.setIssuanceToken(address(issuanceERC20Token)); - testCreateRound(); uint64 roundId = fundingPot.getRoundCount(); @@ -2040,8 +2040,6 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { } function testCloseRound_worksGivenRoundHasEnded() public { - fundingPot.setIssuanceToken(address(issuanceERC20Token)); - testCreateRound(); uint64 roundId = fundingPot.getRoundCount(); @@ -2087,8 +2085,6 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { } function testCloseRound_worksGivenRoundCapHasBeenReached() public { - fundingPot.setIssuanceToken(address(issuanceERC20Token)); - testCreateRound(); uint64 roundId = fundingPot.getRoundCount(); @@ -2133,8 +2129,6 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { } function testCloseRound_worksGivenRoundisAutoClosure() public { - fundingPot.setIssuanceToken(address(issuanceERC20Token)); - testEditRound(); uint64 roundId = fundingPot.getRoundCount(); @@ -2177,8 +2171,6 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { } function testCloseRound_worksWithMultipleContributors() public { - fundingPot.setIssuanceToken(address(issuanceERC20Token)); - testCreateRound(); uint64 roundId = fundingPot.getRoundCount(); From 38671608d7ef61b713edbceab8a9a6c49f5a6938 Mon Sep 17 00:00:00 2001 From: Zuhaib Mohammed Date: Sat, 19 Apr 2025 13:31:31 +0530 Subject: [PATCH 067/130] fix: PR Review fixes#1 --- .../logicModule/LM_PC_FundingPot_v1.sol | 37 +------------------ 1 file changed, 2 insertions(+), 35 deletions(-) diff --git a/src/modules/logicModule/LM_PC_FundingPot_v1.sol b/src/modules/logicModule/LM_PC_FundingPot_v1.sol index 93e7cc2b1..d4cdab0e5 100644 --- a/src/modules/logicModule/LM_PC_FundingPot_v1.sol +++ b/src/modules/logicModule/LM_PC_FundingPot_v1.sol @@ -642,12 +642,10 @@ contract LM_PC_FundingPot_v1 is { Round storage round = rounds[roundId_]; - // Validate round exists if (round.roundEnd == 0 && round.roundCap == 0) { revert Module__LM_PC_FundingPot__RoundNotCreated(); } - // Check if round is already closed if (roundIdToClosedStatus[roundId_]) { revert Module__LM_PC_FundingPot__RoundHasEnded(); } @@ -656,10 +654,8 @@ contract LM_PC_FundingPot_v1 is if (readyToClose) { _closeRound(roundId_); - // Buy the bonding curve token _buyBondingCurveToken(roundId_); - // TODO: Create payment orders for all contributors based on their access criteria _createPaymentOrdersForContributors(roundId_); } else { revert Module__LM_PC_FundingPot__ClosureConditionsNotMet(); @@ -727,8 +723,6 @@ contract LM_PC_FundingPot_v1 is pure returns (bool) { - // start_ + cliff_ should be less or equal to end_ - // this already implies that start_ is not greater than end_ return start_ + cliff_ <= end_; } @@ -752,12 +746,10 @@ contract LM_PC_FundingPot_v1 is Round storage round = rounds[roundId_]; uint currentTime = block.timestamp; - // Validate round exists if (round.roundEnd == 0 && round.roundCap == 0) { revert Module__LM_PC_FundingPot__RoundNotCreated(); } - // Validate contribution timing if (currentTime < round.roundStart) { revert Module__LM_PC_FundingPot__RoundHasNotStarted(); } @@ -766,7 +758,6 @@ contract LM_PC_FundingPot_v1 is revert Module__LM_PC_FundingPot__InvalidAccessCriteriaId(); } - // Validate access criteria _validateAccessCriteria( roundId_, accessCriteriaId_, merkleProof_, _msgSender() ); @@ -775,7 +766,6 @@ contract LM_PC_FundingPot_v1 is roundItToAccessCriteriaIdToPrivileges[roundId_][accessCriteriaId_]; bool canOverrideContributionSpan = privileges.overrideContributionSpan; - // Allow contributions after the round end if the user can override the contribution span if ( round.roundEnd > 0 && currentTime > round.roundEnd && !canOverrideContributionSpan @@ -792,7 +782,6 @@ contract LM_PC_FundingPot_v1 is unspentPersonalCap_ ); - // Record contribution roundIdToUserToContribution[roundId_][_msgSender()] += adjustedAmount; roundIdToTotalContributions[roundId_] += adjustedAmount; roundIdTouserContributionsByAccessCriteria[roundId_][_msgSender()][accessCriteriaId_] @@ -812,10 +801,8 @@ contract LM_PC_FundingPot_v1 is if (readyToClose) { _closeRound(roundId_); - // Buy the bonding curve token _buyBondingCurveToken(roundId_); - // Create payment orders for all contributors based on their access criteria _createPaymentOrdersForContributors(roundId_); } } @@ -1022,12 +1009,10 @@ contract LM_PC_FundingPot_v1 is try IERC721(nftContract_).balanceOf(user_) returns (uint balance) { if (balance == 0) { return false; - // revert Module__LM_PC_FundingPot__AccessCriteriaNftFailed(); } return true; } catch { return false; - // revert Module__LM_PC_FundingPot__AccessCriteriaNftFailed(); } } @@ -1048,7 +1033,6 @@ contract LM_PC_FundingPot_v1 is if (!MerkleProof.verify(merkleProof_, root_, leaf)) { return false; - // revert Module__LM_PC_FundingPot__AccessCriteriaMerkleFailed(); } return true; @@ -1060,10 +1044,8 @@ contract LM_PC_FundingPot_v1 is function _closeRound(uint64 roundId_) internal { Round storage round = rounds[roundId_]; - // Mark round as closed roundIdToClosedStatus[roundId_] = true; - // Execute hook if configured if (round.hookContract != address(0) && round.hookFunction.length > 0) { (bool success,) = round.hookContract.call(round.hookFunction); if (!success) { @@ -1071,7 +1053,6 @@ contract LM_PC_FundingPot_v1 is } } - // Emit event for round closure emit RoundClosed( roundId_, block.timestamp, roundIdToTotalContributions[roundId_] ); @@ -1103,14 +1084,12 @@ contract LM_PC_FundingPot_v1 is uint contributorTotal = roundIdToUserToContribution[roundId_][contributor]; - // Skip if no contribution if (contributorTotal == 0) continue; // Calculate tokens for this contributor proportionally uint contributorTokens = (contributorTotal * tokensBought) / totalContributions; - // Find which access criteria this contributor used and create appropriate payment order for ( uint8 accessCriteriaId = 0; accessCriteriaId <= MAX_ACCESS_CRITERIA_ID; @@ -1119,19 +1098,15 @@ contract LM_PC_FundingPot_v1 is uint contributionByAccessCriteria = roundIdTouserContributionsByAccessCriteria[roundId_][contributor][accessCriteriaId]; - // Skip if no contribution under this access criteria if (contributionByAccessCriteria == 0) continue; - // Get privileges for this access criteria AccessCriteriaPrivileges storage privileges = roundItToAccessCriteriaIdToPrivileges[roundId_][accessCriteriaId]; - // Calculate tokens for this specific access criteria contribution uint tokensForThisAccessCriteria = ( contributionByAccessCriteria * tokensBought ) / totalContributions; - // Determine vesting parameters uint start = privileges.overrideContributionSpan ? privileges.start : round.roundStart; @@ -1141,55 +1116,47 @@ contract LM_PC_FundingPot_v1 is ? privileges.end : round.roundEnd; - // If no override and no end time specified, use current time if (start == 0) start = block.timestamp; if (end == 0) end = block.timestamp; - // Prepare flags and data for the payment order bytes32 flags = 0; bytes32[] memory data = new bytes32[](3); // For start, cliff, and end uint8 flagCount = 0; - // Set start flag (flag 1) if (start > 0) { flags |= bytes32(uint(1) << 1); // Flag 1 for start data[flagCount] = bytes32(start); flagCount++; } - // Set cliff flag (flag 2) if (cliff > 0) { flags |= bytes32(uint(1) << 2); // Flag 2 for cliff data[flagCount] = bytes32(cliff); flagCount++; } - // Set end flag (flag 3) if (end > 0) { flags |= bytes32(uint(1) << 3); // Flag 3 for end data[flagCount] = bytes32(end); flagCount++; } - // Resize data array to match actual flag count bytes32[] memory finalData = new bytes32[](flagCount); for (uint8 j = 0; j < flagCount; j++) { finalData[j] = data[j]; } - // Create payment order IERC20PaymentClientBase_v2.PaymentOrder memory paymentOrder = IERC20PaymentClientBase_v2.PaymentOrder({ recipient: contributor, paymentToken: issuanceToken, amount: tokensForThisAccessCriteria, - originChainId: 0, // Assuming same chain - targetChainId: 0, // Assuming same chain + originChainId: block.chainid, + targetChainId: block.chainid, flags: flags, data: finalData }); - // Add payment order to payment processor _addPaymentOrder(paymentOrder); emit PaymentOrderCreated( From bd83c80a9909b7576e46c2195378290104e5698e Mon Sep 17 00:00:00 2001 From: Zuhaib Mohammed Date: Sun, 20 Apr 2025 12:38:09 +0530 Subject: [PATCH 068/130] fix: PR Review fixes#2 --- .../logicModule/LM_PC_FundingPot_v1_Exposed.sol | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/test/unit/modules/logicModule/LM_PC_FundingPot_v1_Exposed.sol b/test/unit/modules/logicModule/LM_PC_FundingPot_v1_Exposed.sol index 2ec31a341..10ca25738 100644 --- a/test/unit/modules/logicModule/LM_PC_FundingPot_v1_Exposed.sol +++ b/test/unit/modules/logicModule/LM_PC_FundingPot_v1_Exposed.sol @@ -139,4 +139,20 @@ contract LM_PC_FundingPot_v1_Exposed is LM_PC_FundingPot_v1 { { return _checkRoundClosureConditions(roundId_); } + + /** + * @notice Exposes the internal _buyBondingCurveToken function for testing + */ + function exposed_buyBondingCurveToken(uint64 roundId_) external { + return _buyBondingCurveToken(roundId_); + } + + /** + * @notice Exposes the internal _createPaymentOrdersForContributors function for testing + */ + function exposed_createPaymentOrdersForContributors(uint64 roundId_) + external + { + return _createPaymentOrdersForContributors(roundId_); + } } From ba6588909a0d92eb08a478510b8566d5227fa19f Mon Sep 17 00:00:00 2001 From: Zuhaib Mohammed Date: Mon, 21 Apr 2025 08:59:46 +0530 Subject: [PATCH 069/130] fix: accessId unit test bug --- src/modules/logicModule/LM_PC_FundingPot_v1.sol | 2 +- .../modules/logicModule/LM_PC_FundingPot_v1.t.sol | 14 +++++++------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/modules/logicModule/LM_PC_FundingPot_v1.sol b/src/modules/logicModule/LM_PC_FundingPot_v1.sol index d4cdab0e5..80deed81b 100644 --- a/src/modules/logicModule/LM_PC_FundingPot_v1.sol +++ b/src/modules/logicModule/LM_PC_FundingPot_v1.sol @@ -275,7 +275,7 @@ contract LM_PC_FundingPot_v1 is AccessCriteria storage accessCriteria = round.accessCriterias[accessCriteriaId__]; - if (accessCriteria.accessCriteriaType == AccessCriteriaType.OPEN) { + if (accessCriteria.accessCriteriaType == AccessCriteriaType.UNSET) { return (0, false, 0, 0, 0); } diff --git a/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol b/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol index 3848606d9..e2b74e23f 100644 --- a/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol +++ b/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol @@ -2001,7 +2001,6 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { testCreateRound(); uint64 roundId = fundingPot.getRoundCount(); - // Set up access criteria uint8 accessId = 1; ( address nftContract, @@ -2043,7 +2042,6 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { testCreateRound(); uint64 roundId = fundingPot.getRoundCount(); - // Set up access criteria uint8 accessId = 1; ( address nftContract, @@ -2088,7 +2086,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { testCreateRound(); uint64 roundId = fundingPot.getRoundCount(); - uint8 accessId = 1; + uint8 accessId = 2; uint amount = 1000; ( @@ -2103,6 +2101,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { fundingPot.setAccessCriteriaPrivileges( roundId, accessId, 1000, false, 0, 0, 0 ); + mockNFTContract.mint(contributor1_); (uint roundStart,,,,,,) = fundingPot.getRoundGenericParameters(roundId); @@ -2132,7 +2131,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { testEditRound(); uint64 roundId = fundingPot.getRoundCount(); - uint8 accessId = 1; + uint8 accessId = 2; uint amount = 2000; ( @@ -2144,6 +2143,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { fundingPot.setAccessCriteriaForRound( roundId, accessId, nftContract, merkleRoot, allowedAddresses ); + fundingPot.setAccessCriteriaPrivileges( roundId, accessId, 2000, false, 0, 0, 0 ); @@ -2175,7 +2175,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { uint64 roundId = fundingPot.getRoundCount(); // Set up access criteria - uint8 accessId = 0; // Using OPEN access criteria for simplicity + uint8 accessId = 1; ( address nftContract, bytes32 merkleRoot, @@ -2309,7 +2309,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { ); // Set access criteria and privileges - uint8 accessId = 0; + uint8 accessId = 1; ( address nftContract, bytes32 merkleRoot, @@ -2400,7 +2400,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { ); // Set access criteria and privileges - uint8 accessId = 0; + uint8 accessId = 1; ( address nftContract, bytes32 merkleRoot, From dc87617e5c826eef689b92c20c4ce2688510068e Mon Sep 17 00:00:00 2001 From: Zuhaib Mohammed Date: Mon, 21 Apr 2025 13:37:00 +0530 Subject: [PATCH 070/130] fix: PR Review fixes#3 --- .../logicModule/LM_PC_FundingPot_v1.t.sol | 44 +++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol b/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol index e2b74e23f..7907776a9 100644 --- a/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol +++ b/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol @@ -2458,6 +2458,50 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { assertTrue(fundingPot.exposed_checkRoundClosureConditions(roundId)); } + // Test exposed internal function closeRound + function test_closeRound_WhenCapReached() public { + testCreateRound(); + + uint64 roundId = fundingPot.getRoundCount(); + uint8 accessId = 2; + uint amount = 1000; + + ( + address nftContract, + bytes32 merkleRoot, + address[] memory allowedAddresses + ) = _helper_createAccessCriteria(accessId); + + fundingPot.setAccessCriteriaForRound( + roundId, accessId, nftContract, merkleRoot, allowedAddresses + ); + fundingPot.setAccessCriteriaPrivileges( + roundId, accessId, 1000, false, 0, 0, 0 + ); + + mockNFTContract.mint(contributor1_); + + (uint roundStart,,,,,,) = fundingPot.getRoundGenericParameters(roundId); + vm.warp(roundStart + 1); + + // Approve + vm.prank(contributor1_); + _token.approve(address(fundingPot), 1000); + + vm.prank(contributor1_); + fundingPot.contributeToRound( + roundId, amount, accessId, new bytes32[](0) + ); + + assertTrue(fundingPot.exposed_checkRoundClosureConditions(roundId)); + + fundingPot.exposed_closeRound(roundId); + fundingPot.exposed_buyBondingCurveToken(roundId); + fundingPot.exposed_createPaymentOrdersForContributors(roundId); + + assertTrue(fundingPot.isRoundClosed(roundId)); + } + // ------------------------------------------------------------------------- // Helper Functions From 709ff0f0596cde1e2745975e022886f9586d853c Mon Sep 17 00:00:00 2001 From: Zuhaib Mohammed Date: Tue, 22 Apr 2025 12:10:14 +0530 Subject: [PATCH 071/130] fix: PR Review fixes#4 --- .../logicModule/LM_PC_FundingPot_v1.sol | 58 +++++++++---------- .../logicModule/LM_PC_FundingPot_v1.t.sol | 2 +- 2 files changed, 28 insertions(+), 32 deletions(-) diff --git a/src/modules/logicModule/LM_PC_FundingPot_v1.sol b/src/modules/logicModule/LM_PC_FundingPot_v1.sol index 80deed81b..0d3b7ab56 100644 --- a/src/modules/logicModule/LM_PC_FundingPot_v1.sol +++ b/src/modules/logicModule/LM_PC_FundingPot_v1.sol @@ -32,47 +32,43 @@ import {EnumerableSet} from "@oz/utils/structs/EnumerableSet.sol"; /** * @title Inverter Funding Pot Logic Module * - * @notice Manages contribution rounds for fundraising within the Inverter Network, enabling - * configurable access control, contribution limits, and automated distribution. - * Supports multiple concurrent access criteria per round with customizable privileges. + * @notice A sophisticated funding management system that enables configurable fundraising rounds + * with multiple access criteria, contribution limits, and automated distribution. + * This module provides a flexible framework for managing token sales and fundraising + * campaigns with granular access control and contribution management. * - * @dev Implements a sophisticated round-based funding system with features including: - * - Configurable round parameters (start/end times, caps, hooks) - * - Multiple access criteria types (NFT holding, allowlist, Merkle proof) - * - Customizable privileges per access criteria - * - Global accumulative caps across rounds - * - Automatic and manual round closure mechanisms - * - Hook system for post-round actions + * @dev Implements a comprehensive funding system with the following features: + * - Round Configuration + * Supports configurable start/end times, caps, and post-round hooks. * - * DISCLAIMER: Known Limitations - * 1. Storage Considerations: - * The contract stores significant data per round (access criteria, privileges, - * contributions). While this enables flexible round configuration, it may lead - * to higher gas costs as the number of rounds and contributors increases. + * - Access Control + * Multiple access criteria types: + * - Allowlist-based access + * - NFT ownership verification + * - Merkle proof validation + * - Open access * - * 2. Round Management: - * Rounds cannot be modified once started. This is a security feature but - * requires careful initial configuration. Additionally, rounds must be created - * sequentially and cannot run concurrently. + * - Contribution Management + * - Personal contribution caps + * - Round-level caps + * - Global accumulative caps across rounds + * - Configurable contribution time windows * - * 3. Access Criteria: - * The contract supports multiple access criteria per round, but each address - * can only contribute under one access criteria type per round. This is to - * prevent double-counting of privileges and caps. - * - * CAUTION: Administrators should carefully consider round configurations, - * particularly when using global accumulative caps and multiple access criteria, - * as these features interact in complex ways that affect contribution limits. + * - Automated Processing + * - Automatic round closure based on time or cap + * - Post-round hook execution + * - Payment order creation for contributors * * @custom:security-contact security@inverter.network - * In case of any concerns or findings, please refer to our Security Policy - * at security.inverter.network or email us directly! + * In case of any concerns or findings, please refer to + * our Security Policy at security.inverter.network or + * email us directly! * * @custom:version v1.0.0 * - * @custom:inverter-standard-version v0.1.0 + * @custom:inverter-standard-version v0.1.0 * - * @author Inverter Network + * @author 33Audits */ contract LM_PC_FundingPot_v1 is ILM_PC_FundingPot_v1, diff --git a/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol b/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol index 7907776a9..fca2c69bf 100644 --- a/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol +++ b/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol @@ -2459,7 +2459,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { } // Test exposed internal function closeRound - function test_closeRound_WhenCapReached() public { + function test_closeRound_worksGivenCapReached() public { testCreateRound(); uint64 roundId = fundingPot.getRoundCount(); From 31008f788cab4edc3d8216ba8c6f31b0f98435da Mon Sep 17 00:00:00 2001 From: Zuhaib Mohammed Date: Wed, 23 Apr 2025 10:41:01 +0530 Subject: [PATCH 072/130] fix: unit test closeround via mock --- .../logicModule/LM_PC_FundingPot_v1.sol | 30 +++++++------------ test/unit/modules/ModuleTest.sol | 10 +++++-- .../logicModule/LM_PC_FundingPot_v1.t.sol | 13 ++++---- 3 files changed, 25 insertions(+), 28 deletions(-) diff --git a/src/modules/logicModule/LM_PC_FundingPot_v1.sol b/src/modules/logicModule/LM_PC_FundingPot_v1.sol index 0d3b7ab56..b35cf778b 100644 --- a/src/modules/logicModule/LM_PC_FundingPot_v1.sol +++ b/src/modules/logicModule/LM_PC_FundingPot_v1.sol @@ -144,9 +144,6 @@ contract LM_PC_FundingPot_v1 is mapping(uint64 => mapping(address => mapping(uint8 => uint))) private roundIdTouserContributionsByAccessCriteria; - /// @notice Bancor Bonding Curve Funding Manager - FM_BC_Bancor_Redeeming_VirtualSupply_v1 bancorFM; - /// @notice The current round count. uint64 private roundCount; @@ -178,9 +175,6 @@ contract LM_PC_FundingPot_v1 is flags |= bytes32(1 << FLAG_END); __ERC20PaymentClientBase_v2_init(flags); - - address bancorFMaddress = abi.decode(configData_, (address)); - bancorFM = FM_BC_Bancor_Redeeming_VirtualSupply_v1(bancorFMaddress); } // ------------------------------------------------------------------------- @@ -1067,13 +1061,11 @@ contract LM_PC_FundingPot_v1 is address[] memory contributors = EnumerableSet.values(contributorsByRound[roundId_]); - // address issuanceToken = address( - // IBondingCurveBase_v1( - // address(__Module_orchestrator.fundingManager()) - // ).getIssuanceToken() - // ); - //@note: This is for testing purpose, the above snippet should be used to fetch the token address, talk to Fabi! - address issuanceToken = bancorFM.getIssuanceToken(); + address issuanceToken = address( + IBondingCurveBase_v1( + address(__Module_orchestrator.fundingManager()) + ).getIssuanceToken() + ); for (uint i = 0; i < contributors.length; i++) { address contributor = contributors[i]; @@ -1171,13 +1163,11 @@ contract LM_PC_FundingPot_v1 is function _buyBondingCurveToken(uint64 roundId_) internal { uint totalContributions = _getTotalRoundContribution(roundId_); - // address issuanceToken = address( - // IBondingCurveBase_v1( - // address(__Module_orchestrator.fundingManager()) - // ).getIssuanceToken() - // ); - - address issuanceToken = bancorFM.getIssuanceToken(); + address issuanceToken = address( + IBondingCurveBase_v1( + address(__Module_orchestrator.fundingManager()) + ).getIssuanceToken() + ); uint balanceBefore = IERC20(issuanceToken).balanceOf(address(this)); IBondingCurveBase_v1(issuanceToken).buyFor( diff --git a/test/unit/modules/ModuleTest.sol b/test/unit/modules/ModuleTest.sol index e6a08338f..9b79dc468 100644 --- a/test/unit/modules/ModuleTest.sol +++ b/test/unit/modules/ModuleTest.sol @@ -41,7 +41,8 @@ abstract contract ModuleTest is Test { OrchestratorV1Mock _orchestrator; // Mocks - FundingManagerV1Mock _fundingManager; + //FundingManagerV1Mock _fundingManager; @note: using new custom funding manager for testing issuance token + FundingManagerV2Mock _fundingManager; AuthorizerV1Mock _authorizer; ERC20Mock _token = new ERC20Mock("Mock Token", "MOCK", 18); PaymentProcessorV1Mock _paymentProcessor = new PaymentProcessorV1Mock(); @@ -91,8 +92,11 @@ abstract contract ModuleTest is Test { address impl = address(new OrchestratorV1Mock(address(_forwarder))); _orchestrator = OrchestratorV1Mock(Clones.clone(impl)); - impl = address(new FundingManagerV1Mock()); - _fundingManager = FundingManagerV1Mock(Clones.clone(impl)); + // impl = address(new FundingManagerV1Mock()); + // _fundingManager = FundingManagerV1Mock(Clones.clone(impl)); + // @note: using new custom funding manager for testing issuance token + impl = address(new FundingManagerV2Mock()); + _fundingManager = FundingManagerV2Mock(Clones.clone(impl)); impl = address(new AuthorizerV1Mock()); _authorizer = AuthorizerV1Mock(Clones.clone(impl)); diff --git a/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol b/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol index fca2c69bf..184f8be63 100644 --- a/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol +++ b/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol @@ -22,6 +22,8 @@ import {ERC721Mock} from "test/utils/mocks/modules/logicModules/LM_PC_FundingPot_v2NFTMock.sol"; import {LM_PC_FundingPot_v1ERC20Mock} from "test/utils/mocks/modules/logicModules/LM_PC_FundingPot_v1ERC20Mock.sol"; +import {IBondingCurveBase_v1} from + "@fm/bondingCurve/interfaces/IBondingCurveBase_v1.sol"; // System under Test (SuT) import {LM_PC_FundingPot_v1_Exposed} from @@ -95,8 +97,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { } ERC721Mock mockNFTContract = new ERC721Mock("NFT Mock", "NFT"); - LM_PC_FundingPot_v1ERC20Mock issuanceERC20Token = - new LM_PC_FundingPot_v1ERC20Mock("ERC20 Mock", "ERC20"); + address issuanceERC20MockToken; // ------------------------------------------------------------------------- // Setup @@ -119,9 +120,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { _setUpOrchestrator(fundingPot); // Initiate the Logic Module with the metadata and config data - fundingPot.init( - _orchestrator, _METADATA, abi.encode(address(issuanceERC20Token)) - ); // @note: The address of issuancetoken is being passed for testing purpose as of now, We arent actually sure how to use the mock contract alongside the fundingpot contract. + fundingPot.init(_orchestrator, _METADATA, abi.encode("")); _authorizer.setIsAuthorized(address(this), true); @@ -149,6 +148,10 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { true, true ); + + issuanceERC20MockToken = IBondingCurveBase_v1( + address(_orchestrator.fundingManager()) + ).getIssuanceToken(); } // ------------------------------------------------------------------------- From f7f1910dc346af92ec899c30562d8d36b9658a27 Mon Sep 17 00:00:00 2001 From: Zuhaib Mohammed Date: Thu, 24 Apr 2025 10:53:07 +0530 Subject: [PATCH 073/130] fix: PR Review Fix --- src/modules/logicModule/LM_PC_FundingPot_v1.sol | 14 ++++++-------- .../interfaces/ILM_PC_FundingPot_v1.sol | 3 +++ 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/src/modules/logicModule/LM_PC_FundingPot_v1.sol b/src/modules/logicModule/LM_PC_FundingPot_v1.sol index b35cf778b..85346f3b7 100644 --- a/src/modules/logicModule/LM_PC_FundingPot_v1.sol +++ b/src/modules/logicModule/LM_PC_FundingPot_v1.sol @@ -1074,10 +1074,6 @@ contract LM_PC_FundingPot_v1 is if (contributorTotal == 0) continue; - // Calculate tokens for this contributor proportionally - uint contributorTokens = - (contributorTotal * tokensBought) / totalContributions; - for ( uint8 accessCriteriaId = 0; accessCriteriaId <= MAX_ACCESS_CRITERIA_ID; @@ -1112,19 +1108,19 @@ contract LM_PC_FundingPot_v1 is uint8 flagCount = 0; if (start > 0) { - flags |= bytes32(uint(1) << 1); // Flag 1 for start + flags |= bytes32(1 << FLAG_START); data[flagCount] = bytes32(start); flagCount++; } if (cliff > 0) { - flags |= bytes32(uint(1) << 2); // Flag 2 for cliff + flags |= bytes32(1 << FLAG_CLIFF); data[flagCount] = bytes32(cliff); flagCount++; } if (end > 0) { - flags |= bytes32(uint(1) << 3); // Flag 3 for end + flags |= bytes32(1 << FLAG_END); data[flagCount] = bytes32(end); flagCount++; } @@ -1162,7 +1158,9 @@ contract LM_PC_FundingPot_v1 is function _buyBondingCurveToken(uint64 roundId_) internal { uint totalContributions = _getTotalRoundContribution(roundId_); - + if (totalContributions == 0) { + revert Module__LM_PC_FundingPot__NoContributions(); + } address issuanceToken = address( IBondingCurveBase_v1( address(__Module_orchestrator.fundingManager()) diff --git a/src/modules/logicModule/interfaces/ILM_PC_FundingPot_v1.sol b/src/modules/logicModule/interfaces/ILM_PC_FundingPot_v1.sol index f2ef56168..fd7914d6d 100644 --- a/src/modules/logicModule/interfaces/ILM_PC_FundingPot_v1.sol +++ b/src/modules/logicModule/interfaces/ILM_PC_FundingPot_v1.sol @@ -283,6 +283,9 @@ interface ILM_PC_FundingPot_v1 is IERC20PaymentClientBase_v2 { /// @notice Hook execution failed. error Module__LM_PC_FundingPot__HookExecutionFailed(); + /// @notice No contributions were made to the round. + error Module__LM_PC_FundingPot__NoContributions(); + // ------------------------------------------------------------------------- // Public - Getters From 0351adf36ec498efafa525611cebafacc89d2386 Mon Sep 17 00:00:00 2001 From: Zuhaib Mohammed Date: Thu, 24 Apr 2025 12:58:36 +0530 Subject: [PATCH 074/130] fix: PR Review Fix: add batches for creating payment orders --- .../logicModule/LM_PC_FundingPot_v1.sol | 77 ++++++++- .../interfaces/ILM_PC_FundingPot_v1.sol | 25 +++ .../logicModule/LM_PC_FundingPot_v1.t.sol | 163 ++++++++++++++---- .../LM_PC_FundingPot_v1_Exposed.sol | 12 +- 4 files changed, 237 insertions(+), 40 deletions(-) diff --git a/src/modules/logicModule/LM_PC_FundingPot_v1.sol b/src/modules/logicModule/LM_PC_FundingPot_v1.sol index 85346f3b7..5b62315d5 100644 --- a/src/modules/logicModule/LM_PC_FundingPot_v1.sol +++ b/src/modules/logicModule/LM_PC_FundingPot_v1.sol @@ -150,6 +150,9 @@ contract LM_PC_FundingPot_v1 is /// @notice Storage gap for future upgrades. uint[50] private __gap; + // Add a mapping to track the next unprocessed index for each round + mapping(uint64 => uint) private roundIdToNextUnprocessedIndex; + // ------------------------------------------------------------------------- // Modifiers @@ -646,12 +649,55 @@ contract LM_PC_FundingPot_v1 is _buyBondingCurveToken(roundId_); - _createPaymentOrdersForContributors(roundId_); + // Payment orders will be created separately via processContributorBatch } else { revert Module__LM_PC_FundingPot__ClosureConditionsNotMet(); } } + /// @inheritdoc ILM_PC_FundingPot_v1 + function createPaymentOrdersForContributorsBatch( + uint64 roundId_, + uint batchSize_ + ) external { + Round storage round = rounds[roundId_]; + + // Check if round exists + if (round.roundEnd == 0 && round.roundCap == 0) { + revert Module__LM_PC_FundingPot__RoundNotCreated(); + } + + // Check if round is closed + if (!roundIdToClosedStatus[roundId_]) { + revert Module__LM_PC_FundingPot__RoundNotClosed(); + } + + address[] memory contributors = + EnumerableSet.values(contributorsByRound[roundId_]); + uint contributorCount = contributors.length; + + // Check batch size is not zero + if (batchSize_ == 0 || batchSize_ > contributorCount) { + revert Module__LM_PC_FundingPot__InvalidBatchParameters(); + } + + // If autoClosure is false, only admin can process contributors + if (!round.autoClosure) { + _checkRoleModifier(FUNDING_POT_ADMIN_ROLE, _msgSender()); + } + + uint startIndex = roundIdToNextUnprocessedIndex[roundId_]; + _createPaymentOrdersForContributors(roundId_, startIndex, batchSize_); + + // Update the next unprocessed index + uint endIndex = startIndex + batchSize_; + if (endIndex > contributorCount) { + endIndex = contributorCount; + } + + roundIdToNextUnprocessedIndex[roundId_] = endIndex; + } + // ------------------------------------------------------------------------- // Internal @@ -792,8 +838,6 @@ contract LM_PC_FundingPot_v1 is _closeRound(roundId_); _buyBondingCurveToken(roundId_); - - _createPaymentOrdersForContributors(roundId_); } } } @@ -1048,10 +1092,16 @@ contract LM_PC_FundingPot_v1 is ); } - /// @notice Creates payment orders for all contributors in a round based on their access criteria - /// @dev Loops through all contributors and creates payment orders with appropriate vesting schedules + /// @notice Creates payment orders for contributors in a round based on their access criteria + /// @dev Processes a batch of contributors to handle gas limit concerns /// @param roundId_ The ID of the round to create payment orders for - function _createPaymentOrdersForContributors(uint64 roundId_) internal { + /// @param startIndex_ The starting index in the contributors array + /// @param batchSize_ The number of contributors to process in this batch + function _createPaymentOrdersForContributors( + uint64 roundId_, + uint startIndex_, + uint batchSize_ + ) internal { Round storage round = rounds[roundId_]; uint totalContributions = roundIdToTotalContributions[roundId_]; uint tokensBought = roundTokensBought[roundId_]; @@ -1060,6 +1110,17 @@ contract LM_PC_FundingPot_v1 is address[] memory contributors = EnumerableSet.values(contributorsByRound[roundId_]); + uint contributorCount = contributors.length; + + if (startIndex_ >= contributorCount) { + revert Module__LM_PC_FundingPot__InvalidStartIndex(); + } + + // Calculate the end index (don't exceed array bounds) + uint endIndex = startIndex_ + batchSize_; + if (endIndex > contributorCount) { + endIndex = contributorCount; + } address issuanceToken = address( IBondingCurveBase_v1( @@ -1067,7 +1128,7 @@ contract LM_PC_FundingPot_v1 is ).getIssuanceToken() ); - for (uint i = 0; i < contributors.length; i++) { + for (uint i = startIndex_; i < endIndex; i++) { address contributor = contributors[i]; uint contributorTotal = roundIdToUserToContribution[roundId_][contributor]; @@ -1154,6 +1215,8 @@ contract LM_PC_FundingPot_v1 is ); } } + + emit ContributorBatchProcessed(roundId_, startIndex_, endIndex); } function _buyBondingCurveToken(uint64 roundId_) internal { diff --git a/src/modules/logicModule/interfaces/ILM_PC_FundingPot_v1.sol b/src/modules/logicModule/interfaces/ILM_PC_FundingPot_v1.sol index fd7914d6d..89fd16020 100644 --- a/src/modules/logicModule/interfaces/ILM_PC_FundingPot_v1.sol +++ b/src/modules/logicModule/interfaces/ILM_PC_FundingPot_v1.sol @@ -209,6 +209,14 @@ interface ILM_PC_FundingPot_v1 is IERC20PaymentClientBase_v2 { uint end_ ); + /// @notice Emitted when a contributor batch is processed. + /// @param roundId_ The ID of the round. + /// @param startIndex_ The starting index in the contributors array. + /// @param endIndex_ The ending index in the contributors array. + event ContributorBatchProcessed( + uint64 indexed roundId_, uint startIndex_, uint endIndex_ + ); + // ------------------------------------------------------------------------- // Errors @@ -286,6 +294,15 @@ interface ILM_PC_FundingPot_v1 is IERC20PaymentClientBase_v2 { /// @notice No contributions were made to the round. error Module__LM_PC_FundingPot__NoContributions(); + /// @notice Round is not closed. + error Module__LM_PC_FundingPot__RoundNotClosed(); + + /// @notice Invalid start index. + error Module__LM_PC_FundingPot__InvalidStartIndex(); + + /// @notice Invalid batch parameters. + error Module__LM_PC_FundingPot__InvalidBatchParameters(); + // ------------------------------------------------------------------------- // Public - Getters @@ -510,4 +527,12 @@ interface ILM_PC_FundingPot_v1 is IERC20PaymentClientBase_v2 { /// @notice Closes a round. /// @param roundId_ The ID of the round to close. function closeRound(uint64 roundId_) external; + + /// @notice Creates a batch of contributors for payment order creation + /// @param roundId_ The ID of the round to process contributors for + /// @param batchSize_ The number of contributors to process in this batch + function createPaymentOrdersForContributorsBatch( + uint64 roundId_, + uint batchSize_ + ) external; } diff --git a/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol b/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol index 184f8be63..eb941fa09 100644 --- a/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol +++ b/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol @@ -2033,12 +2033,6 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { // Verify round is closed assertEq(fundingPot.isRoundClosed(roundId), true); - - // Verify payment orders - IERC20PaymentClientBase_v2.PaymentOrder[] memory orders = - fundingPot.paymentOrders(); - assertEq(orders.length, 1); - assertEq(orders[0].amount, 1000); } function testCloseRound_worksGivenRoundHasEnded() public { @@ -2077,12 +2071,6 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { // Verify round is closed assertEq(fundingPot.isRoundClosed(roundId), true); - - // Verify payment orders - IERC20PaymentClientBase_v2.PaymentOrder[] memory orders = - fundingPot.paymentOrders(); - assertEq(orders.length, 1); - assertEq(orders[0].amount, 500); } function testCloseRound_worksGivenRoundCapHasBeenReached() public { @@ -2122,12 +2110,6 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { assertEq(fundingPot.isRoundClosed(roundId), false); fundingPot.closeRound(roundId); assertEq(fundingPot.isRoundClosed(roundId), true); - - // Get the payment orders and store them in a variable - IERC20PaymentClientBase_v2.PaymentOrder[] memory orders = - fundingPot.paymentOrders(); - - assertEq(orders.length, 1); } function testCloseRound_worksGivenRoundisAutoClosure() public { @@ -2165,12 +2147,6 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { ); assertEq(fundingPot.isRoundClosed(roundId), true); - - // Get the payment orders and store them in a variable - IERC20PaymentClientBase_v2.PaymentOrder[] memory orders = - fundingPot.paymentOrders(); - - assertEq(orders.length, 1); } function testCloseRound_worksWithMultipleContributors() public { @@ -2217,13 +2193,138 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { // Verify round is closed assertEq(fundingPot.isRoundClosed(roundId), true); + } + + //------------------------------------------------------------------------- + + /* Test createPaymentOrdersForContributorsBatch() + ├── Given round does not exist + │ └── When user attempts to create payment orders in batch + │ └── Then it should revert with Module__LM_PC_FundingPot__RoundNotCreated + │ + ├── Given round is not closed + │ └── When user attempts to create payment orders in batch + │ └── Then it should revert with Module__LM_PC_FundingPot__RoundNotClosed + │ + ├── Given start index is greater than the number of contributors + │ └── When user attempts to create payment orders in batch + │ └── Then it should revert with Module__LM_PC_FundingPot__InvalidBatchParameters + │ + ├── Given batch size is zero + │ └── When user attempts to create payment orders in batch + │ └── Then it should revert with Module__LM_PC_FundingPot__InvalidBatchParameters + │ + ├── Given user does not have FUNDING_POT_ADMIN_ROLE + │ └── Given the round is configured with autoClosure + │ └── When user attempts to create payment orders in batch + │ └── Then it should revert with Module__CallerNotAuthorized + │ + ├── Given a closed round with autoClosure + │ └── When user attempts to create payment orders in batch + │ └── Then it should not revert and payment orders should be created + │ └── And the payment orders should have correct token amounts + │ + ├── Given a closed round with manualClosure + │ └── When funding pot admin attempts to create payment orders in batch + │ └── Then it should not revert and payment orders should be created + │ └── And the payment orders should have correct token amounts + */ - // Verify payment orders - IERC20PaymentClientBase_v2.PaymentOrder[] memory orders = - fundingPot.paymentOrders(); - assertEq(orders.length, 3); + function testCreatePaymentOrdersForContributorsBatch_revertsGivenRoundDoesNotExist( + ) public { + uint64 nonExistentRoundId = 999; + + vm.expectRevert( + abi.encodeWithSelector( + ILM_PC_FundingPot_v1 + .Module__LM_PC_FundingPot__RoundNotCreated + .selector + ) + ); + fundingPot.createPaymentOrdersForContributorsBatch( + nonExistentRoundId, 1 + ); } + function testCreatePaymentOrdersForContributorsBatch_revertsGivenRoundIsNotClosed( + ) public { + testContributeToRound_worksGivenAllConditionsMet(); + uint64 roundId = fundingPot.getRoundCount(); + + vm.expectRevert( + abi.encodeWithSelector( + ILM_PC_FundingPot_v1 + .Module__LM_PC_FundingPot__RoundNotClosed + .selector + ) + ); + fundingPot.createPaymentOrdersForContributorsBatch(roundId, 1); + } + + function testCreatePaymentOrdersForContributorsBatch_revertsGivenBatchSizeIsGreaterThanContributorCount( + ) public { + testCloseRound_worksWithMultipleContributors(); + uint64 roundId = fundingPot.getRoundCount(); + + vm.expectRevert( + abi.encodeWithSelector( + ILM_PC_FundingPot_v1 + .Module__LM_PC_FundingPot__InvalidBatchParameters + .selector + ) + ); + fundingPot.createPaymentOrdersForContributorsBatch(roundId, 999); + } + + function testCreatePaymentOrdersForContributorsBatch_revertsGivenBatchSizeIsZero( + ) public { + testCloseRound_worksWithMultipleContributors(); + uint64 roundId = fundingPot.getRoundCount(); + + vm.expectRevert( + abi.encodeWithSelector( + ILM_PC_FundingPot_v1 + .Module__LM_PC_FundingPot__InvalidBatchParameters + .selector + ) + ); + fundingPot.createPaymentOrdersForContributorsBatch(roundId, 0); + } + + function testCreatePaymentOrdersForContributorsBatch_revertsGivenUserDoesNotHaveFundingPotAdminRole( + ) public { + testCloseRound_worksWithMultipleContributors(); + uint64 roundId = fundingPot.getRoundCount(); + + vm.startPrank(contributor1_); + vm.expectRevert( + abi.encodeWithSelector( + IModule_v1.Module__CallerNotAuthorized.selector, + fundingPot.FUNDING_POT_ADMIN_ROLE(), + contributor1_ + ) + ); + fundingPot.createPaymentOrdersForContributorsBatch(roundId, 1); + vm.stopPrank(); + } + + function testCreatePaymentOrdersForContributorsBatch_worksGivenRoundIsAutoClosure( + ) public { + testCloseRound_worksGivenRoundisAutoClosure(); + uint64 roundId = fundingPot.getRoundCount(); + + fundingPot.createPaymentOrdersForContributorsBatch(roundId, 1); + assertEq(fundingPot.paymentOrders().length, 1); + } + + function testCreatePaymentOrdersForContributorsBatch_worksGivenRoundIsManualClosure( + ) public { + testCloseRound_worksWithMultipleContributors(); + uint64 roundId = fundingPot.getRoundCount(); + + fundingPot.createPaymentOrdersForContributorsBatch(roundId, 3); + assertEq(fundingPot.paymentOrders().length, 3); + } // ------------------------------------------------------------------------- // Internal Functions @@ -2498,9 +2599,13 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { assertTrue(fundingPot.exposed_checkRoundClosureConditions(roundId)); + uint startIndex = 0; + uint batchSize = 1; fundingPot.exposed_closeRound(roundId); fundingPot.exposed_buyBondingCurveToken(roundId); - fundingPot.exposed_createPaymentOrdersForContributors(roundId); + fundingPot.exposed_createPaymentOrdersForContributors( + roundId, startIndex, batchSize + ); assertTrue(fundingPot.isRoundClosed(roundId)); } diff --git a/test/unit/modules/logicModule/LM_PC_FundingPot_v1_Exposed.sol b/test/unit/modules/logicModule/LM_PC_FundingPot_v1_Exposed.sol index 10ca25738..fadd242e6 100644 --- a/test/unit/modules/logicModule/LM_PC_FundingPot_v1_Exposed.sol +++ b/test/unit/modules/logicModule/LM_PC_FundingPot_v1_Exposed.sol @@ -150,9 +150,13 @@ contract LM_PC_FundingPot_v1_Exposed is LM_PC_FundingPot_v1 { /** * @notice Exposes the internal _createPaymentOrdersForContributors function for testing */ - function exposed_createPaymentOrdersForContributors(uint64 roundId_) - external - { - return _createPaymentOrdersForContributors(roundId_); + function exposed_createPaymentOrdersForContributors( + uint64 roundId_, + uint startIndex_, + uint batchSize_ + ) external { + return _createPaymentOrdersForContributors( + roundId_, startIndex_, batchSize_ + ); } } From 7315c01bc656227fcce8a1a18720f982795d8404 Mon Sep 17 00:00:00 2001 From: JeffreyJoel Date: Thu, 24 Apr 2025 08:59:19 +0100 Subject: [PATCH 075/130] fix: fix stack too deep error --- .../logicModule/LM_PC_FundingPot_v1.sol | 161 +++++++++++------- 1 file changed, 101 insertions(+), 60 deletions(-) diff --git a/src/modules/logicModule/LM_PC_FundingPot_v1.sol b/src/modules/logicModule/LM_PC_FundingPot_v1.sol index 5b62315d5..766805d9b 100644 --- a/src/modules/logicModule/LM_PC_FundingPot_v1.sol +++ b/src/modules/logicModule/LM_PC_FundingPot_v1.sol @@ -1102,7 +1102,6 @@ contract LM_PC_FundingPot_v1 is uint startIndex_, uint batchSize_ ) internal { - Round storage round = rounds[roundId_]; uint totalContributions = roundIdToTotalContributions[roundId_]; uint tokensBought = roundTokensBought[roundId_]; @@ -1145,73 +1144,16 @@ contract LM_PC_FundingPot_v1 is if (contributionByAccessCriteria == 0) continue; - AccessCriteriaPrivileges storage privileges = - roundItToAccessCriteriaIdToPrivileges[roundId_][accessCriteriaId]; - uint tokensForThisAccessCriteria = ( contributionByAccessCriteria * tokensBought ) / totalContributions; - uint start = privileges.overrideContributionSpan - ? privileges.start - : round.roundStart; - uint cliff = - privileges.overrideContributionSpan ? privileges.cliff : 0; - uint end = privileges.overrideContributionSpan - ? privileges.end - : round.roundEnd; - - if (start == 0) start = block.timestamp; - if (end == 0) end = block.timestamp; - - bytes32 flags = 0; - bytes32[] memory data = new bytes32[](3); // For start, cliff, and end - uint8 flagCount = 0; - - if (start > 0) { - flags |= bytes32(1 << FLAG_START); - data[flagCount] = bytes32(start); - flagCount++; - } - - if (cliff > 0) { - flags |= bytes32(1 << FLAG_CLIFF); - data[flagCount] = bytes32(cliff); - flagCount++; - } - - if (end > 0) { - flags |= bytes32(1 << FLAG_END); - data[flagCount] = bytes32(end); - flagCount++; - } - - bytes32[] memory finalData = new bytes32[](flagCount); - for (uint8 j = 0; j < flagCount; j++) { - finalData[j] = data[j]; - } - - IERC20PaymentClientBase_v2.PaymentOrder memory paymentOrder = - IERC20PaymentClientBase_v2.PaymentOrder({ - recipient: contributor, - paymentToken: issuanceToken, - amount: tokensForThisAccessCriteria, - originChainId: block.chainid, - targetChainId: block.chainid, - flags: flags, - data: finalData - }); - - _addPaymentOrder(paymentOrder); - - emit PaymentOrderCreated( + _createAndAddPaymentOrder( roundId_, contributor, accessCriteriaId, tokensForThisAccessCriteria, - start, - cliff, - end + issuanceToken ); } } @@ -1219,6 +1161,105 @@ contract LM_PC_FundingPot_v1 is emit ContributorBatchProcessed(roundId_, startIndex_, endIndex); } + /// @notice Creates time parameter data for a payment order + /// @dev Sets default values for start, cliff, and end if they are zero + /// @param start_ The start time of the payment order + /// @param cliff_ The cliff time of the payment order + /// @param end_ The end time of the payment order + /// @return flags The flags for the payment order + /// @return finalData The final data for the payment order + function _createTimeParameterData(uint start_, uint cliff_, uint end_) + internal + view + returns (bytes32 flags, bytes32[] memory finalData) + { + if (start_ == 0) start_ = block.timestamp; + if (end_ == 0) end_ = block.timestamp; + + bytes32 flags = 0; + bytes32[] memory data = new bytes32[](3); // For start, cliff, and end + uint8 flagCount = 0; + + if (start_ > 0) { + flags |= bytes32(uint(1) << 1); + data[flagCount] = bytes32(start_); + flagCount++; + } + + if (cliff_ > 0) { + flags |= bytes32(uint(1) << 2); + data[flagCount] = bytes32(cliff_); + flagCount++; + } + + if (end_ > 0) { + flags |= bytes32(uint(1) << 3); + data[flagCount] = bytes32(end_); + flagCount++; + } + + finalData = new bytes32[](flagCount); + for (uint8 j = 0; j < flagCount; j++) { + finalData[j] = data[j]; + } + + return (flags, finalData); + } + + /// @notice Creates and adds a payment order for a contributor + /// @dev Sets default values for start, cliff, and end if they are zero + /// @param roundId_ The ID of the round to create the payment order for + /// @param recipient_ The address of the recipient of the payment order + /// @param accessCriteriaId_ The ID of the specific access criteria + /// @param tokensAmount_ The amount of tokens for the payment order + /// @param issuanceToken_ The issuance token for the payment order + function _createAndAddPaymentOrder( + uint64 roundId_, + address recipient_, + uint8 accessCriteriaId_, + uint tokensAmount_, + address issuanceToken_ + ) internal { + Round storage round = rounds[roundId_]; + + AccessCriteriaPrivileges storage privileges = + roundItToAccessCriteriaIdToPrivileges[roundId_][accessCriteriaId_]; + + uint start = privileges.overrideContributionSpan + ? privileges.start + : round.roundStart; + uint cliff = privileges.overrideContributionSpan ? privileges.cliff : 0; + uint end = privileges.overrideContributionSpan + ? privileges.end + : round.roundEnd; + + (bytes32 flags, bytes32[] memory finalData) = + _createTimeParameterData(start, cliff, end); + + IERC20PaymentClientBase_v2.PaymentOrder memory paymentOrder = + IERC20PaymentClientBase_v2.PaymentOrder({ + recipient: recipient_, + paymentToken: issuanceToken_, + amount: tokensAmount_, + originChainId: block.chainid, + targetChainId: block.chainid, + flags: flags, + data: finalData + }); + + _addPaymentOrder(paymentOrder); + + emit PaymentOrderCreated( + roundId_, + recipient_, + accessCriteriaId_, + tokensAmount_, + start, + cliff, + end + ); + } + function _buyBondingCurveToken(uint64 roundId_) internal { uint totalContributions = _getTotalRoundContribution(roundId_); if (totalContributions == 0) { From 4dabde039822f92cd3473a9d7b905ab8231c917b Mon Sep 17 00:00:00 2001 From: Zuhaib Mohammed Date: Fri, 25 Apr 2025 21:23:42 +0530 Subject: [PATCH 076/130] PR Review Fix: format and code cleanup --- src/modules/logicModule/LM_PC_FundingPot_v1.sol | 12 ++++++------ .../fundingManager/FundingManagerV1Mock.sol | 14 ++++++++++++++ test/unit/modules/ModuleTest.sol | 10 +++------- .../modules/logicModule/LM_PC_FundingPot_v1.t.sol | 5 ----- 4 files changed, 23 insertions(+), 18 deletions(-) diff --git a/src/modules/logicModule/LM_PC_FundingPot_v1.sol b/src/modules/logicModule/LM_PC_FundingPot_v1.sol index 766805d9b..784188d48 100644 --- a/src/modules/logicModule/LM_PC_FundingPot_v1.sol +++ b/src/modules/logicModule/LM_PC_FundingPot_v1.sol @@ -16,8 +16,6 @@ import { } from "@lm/abstracts/ERC20PaymentClientBase_v2.sol"; import {IBondingCurveBase_v1} from "@fm/bondingCurve/interfaces/IBondingCurveBase_v1.sol"; -import {FM_BC_Bancor_Redeeming_VirtualSupply_v1} from - "src/modules/fundingManager/bondingCurve/FM_BC_Bancor_Redeeming_VirtualSupply_v1.sol"; // External import {IERC20} from "@oz/token/ERC20/IERC20.sol"; @@ -1270,11 +1268,13 @@ contract LM_PC_FundingPot_v1 is address(__Module_orchestrator.fundingManager()) ).getIssuanceToken() ); - - uint balanceBefore = IERC20(issuanceToken).balanceOf(address(this)); - IBondingCurveBase_v1(issuanceToken).buyFor( - address(this), totalContributions, 0 + // approve the fundingManager to spend the contribution token + IERC20(__Module_orchestrator.fundingManager().token()).approve( + address(__Module_orchestrator.fundingManager()), totalContributions ); + uint balanceBefore = IERC20(issuanceToken).balanceOf(address(this)); + IBondingCurveBase_v1(address(__Module_orchestrator.fundingManager())) + .buyFor(address(this), totalContributions, 1); uint balanceAfter = IERC20(issuanceToken).balanceOf(address(this)); uint tokensBought = balanceAfter - balanceBefore; diff --git a/test/mocks/modules/fundingManager/FundingManagerV1Mock.sol b/test/mocks/modules/fundingManager/FundingManagerV1Mock.sol index d2fc6bffc..292e86f2f 100644 --- a/test/mocks/modules/fundingManager/FundingManagerV1Mock.sol +++ b/test/mocks/modules/fundingManager/FundingManagerV1Mock.sol @@ -11,6 +11,7 @@ import { IOrchestrator_v1 } from "src/modules/base/Module_v1.sol"; import {IFundingManager_v1} from "@fm/IFundingManager_v1.sol"; +import {ERC20Issuance_v1} from "@ex/token/ERC20Issuance_v1.sol"; // External Libraries import {SafeERC20} from "@oz/token/ERC20/utils/SafeERC20.sol"; @@ -32,6 +33,7 @@ contract FundingManagerV1Mock is IFundingManager_v1, Module_v1 { // using SafeERC20 for IERC20; IERC20 private _token; + ERC20Issuance_v1 private _bondingToken; function init( IOrchestrator_v1 orchestrator_, @@ -39,6 +41,9 @@ contract FundingManagerV1Mock is IFundingManager_v1, Module_v1 { bytes memory ) public override(Module_v1) initializer { __Module_init(orchestrator_, metadata); + _bondingToken = new ERC20Issuance_v1( + "Bonding Token", "BOND", 18, type(uint).max - 1, address(this) + ); } function setToken(IERC20 newToken) public { @@ -73,4 +78,13 @@ contract FundingManagerV1Mock is IFundingManager_v1, Module_v1 { // _token.safeTransfer(to, amount); _token.transfer(to, amount); } + + function getIssuanceToken() public view returns (address) { + return address(_bondingToken); + } + + function buyFor(address to, uint amount, uint minTokens) public { + _token.transferFrom(_msgSender(), address(this), amount); + _bondingToken.mint(to, amount); + } } diff --git a/test/unit/modules/ModuleTest.sol b/test/unit/modules/ModuleTest.sol index 9b79dc468..e6a08338f 100644 --- a/test/unit/modules/ModuleTest.sol +++ b/test/unit/modules/ModuleTest.sol @@ -41,8 +41,7 @@ abstract contract ModuleTest is Test { OrchestratorV1Mock _orchestrator; // Mocks - //FundingManagerV1Mock _fundingManager; @note: using new custom funding manager for testing issuance token - FundingManagerV2Mock _fundingManager; + FundingManagerV1Mock _fundingManager; AuthorizerV1Mock _authorizer; ERC20Mock _token = new ERC20Mock("Mock Token", "MOCK", 18); PaymentProcessorV1Mock _paymentProcessor = new PaymentProcessorV1Mock(); @@ -92,11 +91,8 @@ abstract contract ModuleTest is Test { address impl = address(new OrchestratorV1Mock(address(_forwarder))); _orchestrator = OrchestratorV1Mock(Clones.clone(impl)); - // impl = address(new FundingManagerV1Mock()); - // _fundingManager = FundingManagerV1Mock(Clones.clone(impl)); - // @note: using new custom funding manager for testing issuance token - impl = address(new FundingManagerV2Mock()); - _fundingManager = FundingManagerV2Mock(Clones.clone(impl)); + impl = address(new FundingManagerV1Mock()); + _fundingManager = FundingManagerV1Mock(Clones.clone(impl)); impl = address(new AuthorizerV1Mock()); _authorizer = AuthorizerV1Mock(Clones.clone(impl)); diff --git a/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol b/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol index eb941fa09..e9cf8472f 100644 --- a/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol +++ b/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol @@ -97,7 +97,6 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { } ERC721Mock mockNFTContract = new ERC721Mock("NFT Mock", "NFT"); - address issuanceERC20MockToken; // ------------------------------------------------------------------------- // Setup @@ -148,10 +147,6 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { true, true ); - - issuanceERC20MockToken = IBondingCurveBase_v1( - address(_orchestrator.fundingManager()) - ).getIssuanceToken(); } // ------------------------------------------------------------------------- From 3459516aa925ae920244a5e4a1a3825ec07ec70c Mon Sep 17 00:00:00 2001 From: Leandro Faria Date: Mon, 28 Apr 2025 12:23:31 -0500 Subject: [PATCH 077/130] fix:gas/contract optimziations --- simulation.py | 150 +++++++++++++++++ .../logicModule/LM_PC_FundingPot_v1.sol | 93 ++++++----- .../interfaces/ILM_PC_FundingPot_v1.sol | 55 +++---- .../logicModule/LM_PC_FundingPot_v1.t.sol | 154 +++++++++--------- .../LM_PC_FundingPot_v1_Exposed.sol | 22 +-- 5 files changed, 310 insertions(+), 164 deletions(-) create mode 100644 simulation.py diff --git a/simulation.py b/simulation.py new file mode 100644 index 000000000..08365509b --- /dev/null +++ b/simulation.py @@ -0,0 +1,150 @@ +import numpy as np +import matplotlib.pyplot as plt +from dataclasses import dataclass +from typing import List, Tuple +import math + +@dataclass +class Epoch: + fixed_price: float + issuance_threshold: float + current_issuance: float = 0 + is_price_discovery: bool = False + discovery_volume: float = 0 + discovery_price: float = 0 + +class UniswapV4HookSimulation: + def __init__( + self, + initial_price: float, + issuance_threshold: float, + discovery_volume_limit: float, + price_volatility_limit: float + ): + self.epochs: List[Epoch] = [] + self.current_epoch = Epoch( + fixed_price=initial_price, + issuance_threshold=issuance_threshold + ) + self.epochs.append(self.current_epoch) + + self.discovery_volume_limit = discovery_volume_limit + self.price_volatility_limit = price_volatility_limit + self.total_collateral = 0 + self.price_history = [] + self.issuance_history = [] + + def calculate_required_collateral(self) -> float: + """Calculate the required collateral based on current issuance and price""" + if self.current_epoch.is_price_discovery: + # In price discovery, use the discovered price or floor price + price = max( + self.current_epoch.discovery_price, + self.current_epoch.fixed_price * (1 - self.price_volatility_limit) + ) + else: + price = self.current_epoch.fixed_price + + return self.current_epoch.current_issuance * price + + def verify_backing_requirement(self) -> bool: + """Verify if current collateral meets backing requirements""" + required_collateral = self.calculate_required_collateral() + return self.total_collateral >= required_collateral + + def simulate_mint(self, amount: float) -> Tuple[bool, str]: + """Simulate minting tokens""" + if not self.verify_backing_requirement(): + return False, "Insufficient backing" + + if self.current_epoch.is_price_discovery: + if self.current_epoch.discovery_volume >= self.discovery_volume_limit: + return False, "Discovery volume limit reached" + + # Simulate price impact in discovery phase + price_impact = amount * 0.001 # 0.1% price impact + self.current_epoch.discovery_price = self.current_epoch.fixed_price * (1 + price_impact) + self.current_epoch.discovery_volume += amount + + # Check if price discovery should end + if self.current_epoch.discovery_volume >= self.discovery_volume_limit: + self._end_price_discovery() + + else: + self.current_epoch.current_issuance += amount + + # Check if we should start price discovery + if self.current_epoch.current_issuance >= self.current_epoch.issuance_threshold: + self._start_price_discovery() + + self.total_collateral += amount * self.current_epoch.fixed_price + self.price_history.append(self.current_epoch.fixed_price) + self.issuance_history.append(self.current_epoch.current_issuance) + return True, "Success" + + def _start_price_discovery(self): + """Start price discovery phase""" + self.current_epoch.is_price_discovery = True + self.current_epoch.discovery_price = self.current_epoch.fixed_price + self.current_epoch.discovery_volume = 0 + + def _end_price_discovery(self): + """End price discovery phase and start new epoch""" + # Create new epoch with discovered price + new_price = self.current_epoch.discovery_price + new_epoch = Epoch( + fixed_price=new_price, + issuance_threshold=self.current_epoch.issuance_threshold + ) + self.epochs.append(new_epoch) + self.current_epoch = new_epoch + +def run_simulation(): + # Initialize simulation + sim = UniswapV4HookSimulation( + initial_price=1.0, + issuance_threshold=1000, + discovery_volume_limit=500, + price_volatility_limit=0.1 # 10% max price movement + ) + + # Simulate a series of mints + mint_amounts = np.random.normal(100, 20, 50) # Random mint amounts + + for amount in mint_amounts: + success, message = sim.simulate_mint(amount) + if not success: + print(f"Mint failed: {message}") + break + + # Plot results + plt.figure(figsize=(12, 6)) + + # Plot price history + plt.subplot(1, 2, 1) + plt.plot(sim.price_history) + plt.title('Token Price Over Time') + plt.xlabel('Transaction') + plt.ylabel('Price') + + # Plot issuance history + plt.subplot(1, 2, 2) + plt.plot(sim.issuance_history) + plt.title('Total Issuance Over Time') + plt.xlabel('Transaction') + plt.ylabel('Issuance') + + plt.tight_layout() + plt.show() + + # Print final state + print("\nFinal State:") + print(f"Total Epochs: {len(sim.epochs)}") + print(f"Current Price: {sim.current_epoch.fixed_price}") + print(f"Current Issuance: {sim.current_epoch.current_issuance}") + print(f"Total Collateral: {sim.total_collateral}") + print(f"Required Collateral: {sim.calculate_required_collateral()}") + print(f"Backing Requirement Met: {sim.verify_backing_requirement()}") + +if __name__ == "__main__": + run_simulation() \ No newline at end of file diff --git a/src/modules/logicModule/LM_PC_FundingPot_v1.sol b/src/modules/logicModule/LM_PC_FundingPot_v1.sol index 784188d48..8cffe0be5 100644 --- a/src/modules/logicModule/LM_PC_FundingPot_v1.sol +++ b/src/modules/logicModule/LM_PC_FundingPot_v1.sol @@ -114,36 +114,36 @@ contract LM_PC_FundingPot_v1 is // State /// @notice Stores all funding rounds by their unique ID. - mapping(uint64 => Round) private rounds; + mapping(uint32 => Round) private rounds; /// @notice Stores all access criteria privilages by their unique ID. mapping( - uint64 roundId + uint32 roundId => mapping(uint8 accessCriteriaId_ => AccessCriteriaPrivileges) ) private roundItToAccessCriteriaIdToPrivileges; /// @notice Maps round IDs to user addresses to contribution amounts - mapping(uint64 => mapping(address => uint)) private + mapping(uint32 => mapping(address => uint)) private roundIdToUserToContribution; /// @notice Maps round IDs to total contributions - mapping(uint64 => uint) private roundIdToTotalContributions; + mapping(uint32 => uint) private roundIdToTotalContributions; /// @notice Maps round IDs to closed status - mapping(uint64 => bool) private roundIdToClosedStatus; + mapping(uint32 => bool) private roundIdToClosedStatus; /// @notice Maps round IDs to bonding curve tokens bought - mapping(uint64 => uint) private roundTokensBought; + mapping(uint32 => uint) private roundTokensBought; /// @notice Maps round IDs to contributors recipients - mapping(uint64 => EnumerableSet.AddressSet) private contributorsByRound; + mapping(uint32 => EnumerableSet.AddressSet) private contributorsByRound; /// @notice Maps round IDs to user addresses to contribution amounts by access criteria mapping(uint64 => mapping(address => mapping(uint8 => uint))) private roundIdTouserContributionsByAccessCriteria; /// @notice The current round count. - uint64 private roundCount; + uint32 private roundCount; /// @notice Storage gap for future upgrades. uint[50] private __gap; @@ -182,7 +182,7 @@ contract LM_PC_FundingPot_v1 is // Public - Getters /// @inheritdoc ILM_PC_FundingPot_v1 - function getRoundGenericParameters(uint64 roundId_) + function getRoundGenericParameters(uint32 roundId_) external view returns ( @@ -208,7 +208,7 @@ contract LM_PC_FundingPot_v1 is } /// @inheritdoc ILM_PC_FundingPot_v1 - function getRoundAccessCriteria(uint64 roundId_, uint8 accessCriteriaId_) + function getRoundAccessCriteria(uint32 roundId_, uint8 accessCriteriaId_) external view returns ( @@ -249,7 +249,7 @@ contract LM_PC_FundingPot_v1 is /// @inheritdoc ILM_PC_FundingPot_v1 function getRoundAccessCriteriaPrivileges( - uint64 roundId_, + uint32 roundId_, uint8 accessCriteriaId__ ) external @@ -284,18 +284,18 @@ contract LM_PC_FundingPot_v1 is } /// @inheritdoc ILM_PC_FundingPot_v1 - function getRoundCount() external view returns (uint64) { + function getRoundCount() external view returns (uint32) { return roundCount; } /// @inheritdoc ILM_PC_FundingPot_v1 - function isRoundClosed(uint64 roundId_) external view returns (bool) { + function isRoundClosed(uint32 roundId_) external view returns (bool) { return roundIdToClosedStatus[roundId_]; } /// @inheritdoc ILM_PC_FundingPot_v1 function getUserEligibility( - uint64 roundId_, + uint32 roundId_, uint8 accessCriteriaId_, bytes32[] memory merkleProof_, address user_ @@ -361,10 +361,10 @@ contract LM_PC_FundingPot_v1 is bytes memory hookFunction_, bool autoClosure_, bool globalAccumulativeCaps_ - ) external onlyModuleRole(FUNDING_POT_ADMIN_ROLE) returns (uint64) { + ) external onlyModuleRole(FUNDING_POT_ADMIN_ROLE) returns (uint32) { roundCount++; - uint64 roundId = roundCount; + uint32 roundId = roundCount; Round storage round = rounds[roundId]; round.roundStart = roundStart_; @@ -388,12 +388,12 @@ contract LM_PC_FundingPot_v1 is globalAccumulativeCaps_ ); - return roundId; + return uint32(roundId); } /// @inheritdoc ILM_PC_FundingPot_v1 function editRound( - uint64 roundId_, + uint32 roundId_, uint roundStart_, uint roundEnd_, uint roundCap_, @@ -430,7 +430,7 @@ contract LM_PC_FundingPot_v1 is /// @inheritdoc ILM_PC_FundingPot_v1 function setAccessCriteriaForRound( - uint64 roundId_, + uint32 roundId_, uint8 accessCriteriaId_, address nftContract_, bytes32 merkleRoot_, @@ -474,7 +474,7 @@ contract LM_PC_FundingPot_v1 is /// @inheritdoc ILM_PC_FundingPot_v1 function editAccessCriteriaForRound( - uint64 roundId_, + uint32 roundId_, uint8 accessCriteriaId_, address nftContract_, bytes32 merkleRoot_, @@ -503,7 +503,7 @@ contract LM_PC_FundingPot_v1 is } function removeAllowlistedAddresses( - uint64 roundId_, + uint32 roundId_, uint8 accessCriteriaId_, address[] calldata addressesToRemove_ ) external onlyModuleRole(FUNDING_POT_ADMIN_ROLE) { @@ -526,7 +526,7 @@ contract LM_PC_FundingPot_v1 is /// @inheritdoc ILM_PC_FundingPot_v1 function setAccessCriteriaPrivileges( - uint64 roundId_, + uint32 roundId_, uint8 accessCriteriaId_, uint personalCap_, bool overrideContributionSpan_, @@ -565,7 +565,7 @@ contract LM_PC_FundingPot_v1 is /// @inheritdoc ILM_PC_FundingPot_v1 function contributeToRound( - uint64 roundId_, + uint32 roundId_, uint amount_, uint8 accessCriteriaId_, bytes32[] calldata merkleProof_ @@ -578,7 +578,7 @@ contract LM_PC_FundingPot_v1 is /// @inheritdoc ILM_PC_FundingPot_v1 function contributeToRound( - uint64 roundId_, + uint32 roundId_, uint amount_, uint8 accessCriteriaId_, bytes32[] memory merkleProof_, @@ -596,7 +596,7 @@ contract LM_PC_FundingPot_v1 is // Verify the user was eligible for this access criteria in the previous round bool isEligible = _checkAccessCriteriaEligibility( - roundCap.roundId, + uint32(roundCap.roundId), roundCap.accessCriteriaId, roundCap.merkleProof, _msgSender() @@ -607,8 +607,9 @@ contract LM_PC_FundingPot_v1 is roundItToAccessCriteriaIdToPrivileges[roundCap.roundId][roundCap .accessCriteriaId]; - uint userContribution = - _getUserContributionToRound(roundCap.roundId, _msgSender()); + uint userContribution = _getUserContributionToRound( + uint32(roundCap.roundId), _msgSender() + ); uint personalCap = privileges.personalCap; if (userContribution < personalCap) { @@ -627,7 +628,7 @@ contract LM_PC_FundingPot_v1 is } /// @inheritdoc ILM_PC_FundingPot_v1 - function closeRound(uint64 roundId_) + function closeRound(uint32 roundId_) external onlyModuleRole(FUNDING_POT_ADMIN_ROLE) { @@ -655,7 +656,7 @@ contract LM_PC_FundingPot_v1 is /// @inheritdoc ILM_PC_FundingPot_v1 function createPaymentOrdersForContributorsBatch( - uint64 roundId_, + uint32 roundId_, uint batchSize_ ) external { Round storage round = rounds[roundId_]; @@ -767,7 +768,7 @@ contract LM_PC_FundingPot_v1 is /// @param merkleProof_ The Merkle proof for validation if needed /// @param unspentPersonalCap_ The amount of unused capacity from previous rounds function _contributeToRound( - uint64 roundId_, + uint32 roundId_, uint amount_, uint8 accessCriteriaId_, bytes32[] memory merkleProof_, @@ -847,7 +848,7 @@ contract LM_PC_FundingPot_v1 is /// @param merkleProof_ Merkle proof for Merkle tree-based access (optional) /// @param user_ The address of the user to validate function _validateAccessCriteria( - uint64 roundId_, + uint32 roundId_, uint8 accessCriteriaId_, bytes32[] memory merkleProof_, address user_ @@ -882,7 +883,7 @@ contract LM_PC_FundingPot_v1 is /// @param canOverrideContributionSpan_ Whether the contribution span can be overridden /// @param unspentPersonalCap_ The amount of unused capacity from previous rounds function _validateAndAdjustCapsWithUnspentCap( - uint64 roundId_, + uint32 roundId_, uint amount_, uint8 accessCriteriaId__, bool canOverrideContributionSpan_, @@ -948,7 +949,7 @@ contract LM_PC_FundingPot_v1 is /// @param user_ The address of the user to validate /// @return isEligible True if the user meets the access criteria, false otherwise function _checkAccessCriteriaEligibility( - uint64 roundId_, + uint32 roundId_, uint8 accessCriteriaId_, bytes32[] memory merkleProof_, address user_ @@ -979,14 +980,14 @@ contract LM_PC_FundingPot_v1 is /// @notice Calculates unused capacity from previous rounds /// @param roundId_ The ID of the current round /// @return unusedCapacityFromPrevious The total unused capacity from previous rounds - function _calculateUnusedCapacityFromPreviousRounds(uint64 roundId_) + function _calculateUnusedCapacityFromPreviousRounds(uint32 roundId_) internal view returns (uint unusedCapacityFromPrevious) { unusedCapacityFromPrevious = 0; // Iterate through all previous rounds (1 to roundId_-1) - for (uint64 i = 1; i < roundId_; ++i) { + for (uint32 i = 1; i < roundId_; ++i) { Round storage prevRound = rounds[i]; if (!prevRound.globalAccumulativeCaps) continue; @@ -1003,7 +1004,7 @@ contract LM_PC_FundingPot_v1 is /// @dev Returns the accumulated contributions for the given round /// @param roundId_ The ID of the round to check contributions for /// @return The total contributions for the specified round - function _getTotalRoundContribution(uint64 roundId_) + function _getTotalRoundContribution(uint32 roundId_) internal view returns (uint) @@ -1016,7 +1017,7 @@ contract LM_PC_FundingPot_v1 is /// @param roundId_ The ID of the round to check contributions for /// @param user_ The address of the user /// @return The user's contribution amount for the specified round - function _getUserContributionToRound(uint64 roundId_, address user_) + function _getUserContributionToRound(uint32 roundId_, address user_) internal view returns (uint) @@ -1059,7 +1060,7 @@ contract LM_PC_FundingPot_v1 is bytes32 root_, bytes32[] memory merkleProof_, address user_, - uint64 roundId_ + uint32 roundId_ ) internal pure returns (bool) { bytes32 leaf = keccak256(abi.encodePacked(user_, roundId_)); @@ -1073,7 +1074,7 @@ contract LM_PC_FundingPot_v1 is /// @notice Handles round closure logic /// @dev Updates round status and executes hook if needed /// @param roundId_ The ID of the round to close - function _closeRound(uint64 roundId_) internal { + function _closeRound(uint32 roundId_) internal { Round storage round = rounds[roundId_]; roundIdToClosedStatus[roundId_] = true; @@ -1085,9 +1086,7 @@ contract LM_PC_FundingPot_v1 is } } - emit RoundClosed( - roundId_, block.timestamp, roundIdToTotalContributions[roundId_] - ); + emit RoundClosed(roundId_, roundIdToTotalContributions[roundId_]); } /// @notice Creates payment orders for contributors in a round based on their access criteria @@ -1096,7 +1095,7 @@ contract LM_PC_FundingPot_v1 is /// @param startIndex_ The starting index in the contributors array /// @param batchSize_ The number of contributors to process in this batch function _createPaymentOrdersForContributors( - uint64 roundId_, + uint32 roundId_, uint startIndex_, uint batchSize_ ) internal { @@ -1174,7 +1173,7 @@ contract LM_PC_FundingPot_v1 is if (start_ == 0) start_ = block.timestamp; if (end_ == 0) end_ = block.timestamp; - bytes32 flags = 0; + flags = 0; bytes32[] memory data = new bytes32[](3); // For start, cliff, and end uint8 flagCount = 0; @@ -1212,7 +1211,7 @@ contract LM_PC_FundingPot_v1 is /// @param tokensAmount_ The amount of tokens for the payment order /// @param issuanceToken_ The issuance token for the payment order function _createAndAddPaymentOrder( - uint64 roundId_, + uint32 roundId_, address recipient_, uint8 accessCriteriaId_, uint tokensAmount_, @@ -1258,7 +1257,7 @@ contract LM_PC_FundingPot_v1 is ); } - function _buyBondingCurveToken(uint64 roundId_) internal { + function _buyBondingCurveToken(uint32 roundId_) internal { uint totalContributions = _getTotalRoundContribution(roundId_); if (totalContributions == 0) { revert Module__LM_PC_FundingPot__NoContributions(); @@ -1284,7 +1283,7 @@ contract LM_PC_FundingPot_v1 is /// @notice Checks if a round has reached its cap or time limit /// @param roundId_ The ID of the round to check /// @return Boolean indicating if the round has reached its cap or time limit - function _checkRoundClosureConditions(uint64 roundId_) + function _checkRoundClosureConditions(uint32 roundId_) internal view returns (bool) diff --git a/src/modules/logicModule/interfaces/ILM_PC_FundingPot_v1.sol b/src/modules/logicModule/interfaces/ILM_PC_FundingPot_v1.sol index 89fd16020..2bd1db1aa 100644 --- a/src/modules/logicModule/interfaces/ILM_PC_FundingPot_v1.sol +++ b/src/modules/logicModule/interfaces/ILM_PC_FundingPot_v1.sol @@ -26,7 +26,7 @@ interface ILM_PC_FundingPot_v1 is IERC20PaymentClientBase_v2 { bytes hookFunction; bool autoClosure; bool globalAccumulativeCaps; - mapping(uint64 id => AccessCriteria) accessCriterias; + mapping(uint32 id => AccessCriteria) accessCriterias; } /// @notice Struct used to store information about a funding round's access criteria. @@ -60,7 +60,7 @@ interface ILM_PC_FundingPot_v1 is IERC20PaymentClientBase_v2 { /// @param accessCriteriaId The ID of the access criteria in that round /// @param merkleProof The Merkle proof needed to validate eligibility (if needed) struct UnspentPersonalRoundCap { - uint64 roundId; + uint32 roundId; uint8 accessCriteriaId; bytes32[] merkleProof; } @@ -142,13 +142,13 @@ interface ILM_PC_FundingPot_v1 is IERC20PaymentClientBase_v2 { /// @notice Emitted when access criteria is set for a round. /// @param roundId_ The unique identifier of the round. /// @param accessCriteriaId_ The identifier of the access criteria. - event AccessCriteriaSet(uint64 indexed roundId_, uint8 accessCriteriaId_); + event AccessCriteriaSet(uint32 indexed roundId_, uint8 accessCriteriaId_); /// @notice Emitted when access criteria is edited for a round. /// @param roundId_ The unique identifier of the round. /// @param accessCriteriaId_ The identifier of the access criteria. event AccessCriteriaEdited( - uint64 indexed roundId_, uint8 accessCriteriaId_ + uint32 indexed roundId_, uint8 accessCriteriaId_ ); /// @notice Emitted when access criteria privileges are set for a round. @@ -160,7 +160,7 @@ interface ILM_PC_FundingPot_v1 is IERC20PaymentClientBase_v2 { /// @param cliff_ The time in seconds from start time at which the unlock starts. /// @param end_ The end timestamp for when the linear vesting ends. event AccessCriteriaPrivilegesSet( - uint64 indexed roundId_, + uint32 indexed roundId_, uint8 accessCriteriaId_, uint personalCap_, bool overrideContributionSpan_, @@ -173,22 +173,19 @@ interface ILM_PC_FundingPot_v1 is IERC20PaymentClientBase_v2 { /// @param roundId_ The ID of the round. /// @param contributor_ The address of the contributor. /// @param amount_ The amount contributed. - event ContributionMade(uint64 roundId_, address contributor_, uint amount_); + event ContributionMade(uint32 roundId_, address contributor_, uint amount_); /// @notice Emitted when a round is closed. /// @param roundId_ The ID of the round. - /// @param timestamp_ The timestamp when the round was closed. /// @param totalContributions_ The total contributions collected in the round. - event RoundClosed( - uint64 roundId_, uint timestamp_, uint totalContributions_ - ); + event RoundClosed(uint32 roundId_, uint totalContributions_); /// @notice Emitted when addresses are removed from an access criteria's allowed list. /// @param roundId_ The ID of the round. /// @param accessCriteriaId_ The ID of the access criteria. /// @param addressesRemoved_ The addresses that were removed from the allowlist. event AllowlistedAddressesRemoved( - uint64 roundId_, uint8 accessCriteriaId_, address[] addressesRemoved_ + uint32 roundId_, uint8 accessCriteriaId_, address[] addressesRemoved_ ); /// @notice Emitted when a payment order is created. @@ -200,7 +197,7 @@ interface ILM_PC_FundingPot_v1 is IERC20PaymentClientBase_v2 { /// @param cliff_ The time in seconds from start time at which the unlock starts. /// @param end_ The end timestamp for when the linear vesting ends. event PaymentOrderCreated( - uint64 roundId_, + uint32 roundId_, address contributor_, uint8 accessCriteriaId_, uint tokensForThisAccessCriteria_, @@ -214,7 +211,7 @@ interface ILM_PC_FundingPot_v1 is IERC20PaymentClientBase_v2 { /// @param startIndex_ The starting index in the contributors array. /// @param endIndex_ The ending index in the contributors array. event ContributorBatchProcessed( - uint64 indexed roundId_, uint startIndex_, uint endIndex_ + uint32 indexed roundId_, uint startIndex_, uint endIndex_ ); // ------------------------------------------------------------------------- @@ -315,7 +312,7 @@ interface ILM_PC_FundingPot_v1 is IERC20PaymentClientBase_v2 { /// @return hookFunction_ The encoded function call for the hook. /// @return autoClosure_ Whether hook closure coincides with contribution span end. /// @return globalAccumulativeCaps_ Whether caps accumulate globally across rounds. - function getRoundGenericParameters(uint64 roundId_) + function getRoundGenericParameters(uint32 roundId_) external view returns ( @@ -335,7 +332,7 @@ interface ILM_PC_FundingPot_v1 is IERC20PaymentClientBase_v2 { /// @return nftContract_ The address of the NFT contract used for access control. /// @return merkleRoot_ The merkle root used for access verification. /// @return isList_ If the access criteria is a list, this will be true. - function getRoundAccessCriteria(uint64 roundId_, uint8 accessCriteriaId_) + function getRoundAccessCriteria(uint32 roundId_, uint8 accessCriteriaId_) external view returns ( @@ -354,7 +351,7 @@ interface ILM_PC_FundingPot_v1 is IERC20PaymentClientBase_v2 { /// @return cliff_ The cliff timestamp for the access criteria. /// @return end_ The end timestamp for the access criteria. function getRoundAccessCriteriaPrivileges( - uint64 roundId_, + uint32 roundId_, uint8 accessCriteriaId_ ) external @@ -369,12 +366,12 @@ interface ILM_PC_FundingPot_v1 is IERC20PaymentClientBase_v2 { /// @notice Retrieves the total number of funding rounds. /// @return roundCount_ The total number of funding rounds. - function getRoundCount() external view returns (uint64 roundCount_); + function getRoundCount() external view returns (uint32 roundCount_); /// @notice Retrieves the closed status of a round. /// @param roundId_ The ID of the round. /// @return The closed status of the round. - function isRoundClosed(uint64 roundId_) external view returns (bool); + function isRoundClosed(uint32 roundId_) external view returns (bool); /// @notice Gets eligibility information for a user in a specific round /// @param roundId_ The ID of the round to check eligibility for @@ -384,7 +381,7 @@ interface ILM_PC_FundingPot_v1 is IERC20PaymentClientBase_v2 { /// @return isEligible Whether the user is eligible for the round through any criteria /// @return remainingAmountAllowedToContribute The remaining contribution the user can make function getUserEligibility( - uint64 roundId_, + uint32 roundId_, uint8 accessCriteriaId_, bytes32[] memory merkleProof_, address user_ @@ -414,7 +411,7 @@ interface ILM_PC_FundingPot_v1 is IERC20PaymentClientBase_v2 { bytes memory hookFunction_, bool autoClosure_, bool globalAccumulativeCaps_ - ) external returns (uint64); + ) external returns (uint32); /// @notice Edits an existing funding round. /// @dev Only callable by funding pot admin and only before the round has started. @@ -427,7 +424,7 @@ interface ILM_PC_FundingPot_v1 is IERC20PaymentClientBase_v2 { /// @param autoClosure_ New closure mechanism setting. /// @param globalAccumulativeCaps_ New global accumulative caps setting. function editRound( - uint64 roundId_, + uint32 roundId_, uint roundStart_, uint roundEnd_, uint roundCap_, @@ -445,7 +442,7 @@ interface ILM_PC_FundingPot_v1 is IERC20PaymentClientBase_v2 { /// @param merkleRoot_ Merkle root for the access criteria. /// @param allowedAddresses_ List of explicitly allowed addresses. function setAccessCriteriaForRound( - uint64 roundId_, + uint32 roundId_, uint8 accessCriteriaId_, address nftContract_, bytes32 merkleRoot_, @@ -460,7 +457,7 @@ interface ILM_PC_FundingPot_v1 is IERC20PaymentClientBase_v2 { /// @param merkleRoot_ Merkle root for the access criteria. /// @param allowedAddresses_ List of explicitly allowed addresses. function editAccessCriteriaForRound( - uint64 roundId_, + uint32 roundId_, uint8 accessCriteriaId_, address nftContract_, bytes32 merkleRoot_, @@ -473,7 +470,7 @@ interface ILM_PC_FundingPot_v1 is IERC20PaymentClientBase_v2 { /// @param accessCriteriaId_ ID of the access criteria. /// @param addressesToRemove_ List of addresses to remove from the allowed list. function removeAllowlistedAddresses( - uint64 roundId_, + uint32 roundId_, uint8 accessCriteriaId_, address[] calldata addressesToRemove_ ) external; @@ -488,7 +485,7 @@ interface ILM_PC_FundingPot_v1 is IERC20PaymentClientBase_v2 { /// @param cliff_ Cliff timestamp for the access criteria. /// @param end_ End timestamp for the access criteria. function setAccessCriteriaPrivileges( - uint64 roundId_, + uint32 roundId_, uint8 accessCriteriaId_, uint personalCap_, bool overrideContributionSpan_, @@ -504,7 +501,7 @@ interface ILM_PC_FundingPot_v1 is IERC20PaymentClientBase_v2 { /// @param accessCriteriaId_ The identifier for the access criteria to validate eligibility. /// @param merkleProof_ The Merkle proof used to verify the contributor's eligibility. function contributeToRound( - uint64 roundId_, + uint32 roundId_, uint amount_, uint8 accessCriteriaId_, bytes32[] calldata merkleProof_ @@ -517,7 +514,7 @@ interface ILM_PC_FundingPot_v1 is IERC20PaymentClientBase_v2 { /// @param merkleProof_ The Merkle proof for validation if needed /// @param unspentPersonalRoundCaps_ Array of previous rounds and access criteria to calculate unused capacity from function contributeToRound( - uint64 roundId_, + uint32 roundId_, uint amount_, uint8 accessCriteriaId_, bytes32[] calldata merkleProof_, @@ -526,13 +523,13 @@ interface ILM_PC_FundingPot_v1 is IERC20PaymentClientBase_v2 { /// @notice Closes a round. /// @param roundId_ The ID of the round to close. - function closeRound(uint64 roundId_) external; + function closeRound(uint32 roundId_) external; /// @notice Creates a batch of contributors for payment order creation /// @param roundId_ The ID of the round to process contributors for /// @param batchSize_ The number of contributors to process in this batch function createPaymentOrdersForContributorsBatch( - uint64 roundId_, + uint32 roundId_, uint batchSize_ ) external; } diff --git a/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol b/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol index e9cf8472f..0c27b1175 100644 --- a/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol +++ b/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol @@ -65,21 +65,13 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { // ------------------------------------------------------------------------- // State - struct RoundParameters { - uint roundStart; - uint roundEnd; - uint roundCap; - address hookContract; - bytes hookFunction; - bool autoClosure; - bool globalAccumulativeCaps; - } + // SuT LM_PC_FundingPot_v1_Exposed fundingPot; // Storage variables to avoid stack too deep - uint64 private _testRoundId; + uint32 private _testRoundId; // Default round parameters for testing RoundParams private _defaultRoundParams; @@ -438,7 +430,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { public { testCreateRound(); - uint64 roundId = fundingPot.getRoundCount(); + uint32 roundId = fundingPot.getRoundCount(); RoundParams memory params = RoundParams({ roundStart: block.timestamp + 3 days, @@ -474,7 +466,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { function testEditRound_revertsGivenRoundIsNotCreated() public { testCreateRound(); - uint64 roundId = fundingPot.getRoundCount(); + uint32 roundId = fundingPot.getRoundCount(); RoundParams memory params = RoundParams({ roundStart: block.timestamp + 3 days, @@ -494,7 +486,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { ) ); fundingPot.editRound( - roundId + 1, + uint32(roundId + 1), params.roundStart, params.roundEnd, params.roundCap, @@ -507,7 +499,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { function testEditRound_revertsGivenRoundIsActive() public { testCreateRound(); - uint64 roundId = fundingPot.getRoundCount(); + uint32 roundId = fundingPot.getRoundCount(); RoundParams memory params; ( @@ -554,7 +546,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { public { testCreateRound(); - uint64 roundId = fundingPot.getRoundCount(); + uint32 roundId = fundingPot.getRoundCount(); _editedRoundParams; vm.assume(roundStart_ < block.timestamp); _editedRoundParams.roundStart = roundStart_; @@ -581,7 +573,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { function testEditRound_revertsGivenRoundEndTimeAndCapAreBothZero() public { testCreateRound(); - uint64 roundId = fundingPot.getRoundCount(); + uint32 roundId = fundingPot.getRoundCount(); RoundParams memory params = RoundParams({ roundStart: block.timestamp + 3 days, @@ -622,7 +614,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { && roundEnd_ < roundStart_ ); testCreateRound(); - uint64 roundId = fundingPot.getRoundCount(); + uint32 roundId = fundingPot.getRoundCount(); // Get the current round start time (uint currentRoundStart,,,,,,) = @@ -666,7 +658,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { public { testCreateRound(); - uint64 roundId = fundingPot.getRoundCount(); + uint32 roundId = fundingPot.getRoundCount(); RoundParams memory params = _helper_createEditRoundParams( block.timestamp + 3 days, @@ -702,7 +694,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { public { testCreateRound(); - uint64 roundId = fundingPot.getRoundCount(); + uint32 roundId = fundingPot.getRoundCount(); RoundParams memory params = _helper_createEditRoundParams( block.timestamp + 3 days, @@ -750,7 +742,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { function testEditRound() public { testCreateRound(); - uint64 lastRoundId = fundingPot.getRoundCount(); + uint32 lastRoundId = fundingPot.getRoundCount(); RoundParams memory params = _editedRoundParams; @@ -774,7 +766,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { bytes memory storedHookFunction, bool storedAutoClosure, bool storedGlobalAccumulativeCaps - ) = fundingPot.getRoundGenericParameters(lastRoundId); + ) = fundingPot.getRoundGenericParameters(uint32(lastRoundId)); // Compare with expected values assertEq(storedRoundStart, params.roundStart); @@ -825,7 +817,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { vm.assume(user_ != address(0) && user_ != address(this)); testCreateRound(); - uint64 roundId = fundingPot.getRoundCount(); + uint32 roundId = fundingPot.getRoundCount(); ( address nftContract, @@ -857,7 +849,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { ) public { vm.assume(accessCriteriaEnum >= 0 && accessCriteriaEnum <= 4); - uint64 roundId = fundingPot.getRoundCount(); + uint32 roundId = fundingPot.getRoundCount(); ( address nftContract, @@ -887,7 +879,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { vm.assume(accessCriteriaEnum >= 0 && accessCriteriaEnum <= 4); testCreateRound(); - uint64 roundId = fundingPot.getRoundCount(); + uint32 roundId = fundingPot.getRoundCount(); ( address nftContract, @@ -920,7 +912,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { uint8(ILM_PC_FundingPot_v1.AccessCriteriaType.NFT); testCreateRound(); - uint64 roundId = fundingPot.getRoundCount(); + uint32 roundId = fundingPot.getRoundCount(); ( address nftContract, @@ -951,7 +943,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { uint8(ILM_PC_FundingPot_v1.AccessCriteriaType.MERKLE); testCreateRound(); - uint64 roundId = fundingPot.getRoundCount(); + uint32 roundId = fundingPot.getRoundCount(); ( address nftContract, @@ -982,7 +974,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { uint8(ILM_PC_FundingPot_v1.AccessCriteriaType.LIST); testCreateRound(); - uint64 roundId = fundingPot.getRoundCount(); + uint32 roundId = fundingPot.getRoundCount(); ( address nftContract, @@ -1011,7 +1003,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { vm.assume(accessCriteriaEnum >= 0 && accessCriteriaEnum <= 4); testCreateRound(); - uint64 roundId = fundingPot.getRoundCount(); + uint32 roundId = fundingPot.getRoundCount(); ( address nftContract, @@ -1032,7 +1024,9 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { address retrievedNftContract, bytes32 retrievedMerkleRoot, bool hasAccess - ) = fundingPot.getRoundAccessCriteria(roundId, accessCriteriaEnum); + ) = fundingPot.getRoundAccessCriteria( + uint32(roundId), accessCriteriaEnum + ); assertEq(isOpen, accessCriteriaEnum == 1); assertEq(retrievedNftContract, nftContract); @@ -1076,7 +1070,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { vm.assume(user_ != address(0) && user_ != address(this)); _helper_setupRoundWithAccessCriteria(accessCriteriaEnum); - uint64 roundId = fundingPot.getRoundCount(); + uint32 roundId = fundingPot.getRoundCount(); ( address nftContract, @@ -1109,7 +1103,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { vm.assume(accessCriteriaEnum >= 0 && accessCriteriaEnum <= 4); _helper_setupRoundWithAccessCriteria(accessCriteriaEnum); - uint64 roundId = fundingPot.getRoundCount(); + uint32 roundId = fundingPot.getRoundCount(); uint8 accessCriteriaId = 10; // Invalid ID ( @@ -1134,7 +1128,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { uint8 accessCriteriaEnum ) public { vm.assume(accessCriteriaEnum >= 0 && accessCriteriaEnum <= 4); - uint64 roundId = fundingPot.getRoundCount(); + uint32 roundId = fundingPot.getRoundCount(); uint8 accessCriteriaId = 0; ( @@ -1156,7 +1150,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { // Set up a round with access criteria _helper_setupRoundWithAccessCriteria(accessCriteriaEnum); - uint64 roundId = fundingPot.getRoundCount(); + uint32 roundId = fundingPot.getRoundCount(); // Warp to make the round active (uint roundStart,,,,,,) = fundingPot.getRoundGenericParameters(roundId); @@ -1223,7 +1217,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { { testCreateRound(); - uint64 roundId = fundingPot.getRoundCount(); + uint32 roundId = fundingPot.getRoundCount(); uint8 accessId = 1; uint amount = 250; @@ -1263,7 +1257,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { { testCreateRound(); - uint64 roundId = fundingPot.getRoundCount(); + uint32 roundId = fundingPot.getRoundCount(); uint8 accessId = 0; uint amount = 250; @@ -1307,7 +1301,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { uint8 accessId = 2; _helper_setupRoundWithAccessCriteria(accessId); - uint64 roundId = fundingPot.getRoundCount(); + uint32 roundId = fundingPot.getRoundCount(); uint amount = 250; @@ -1338,7 +1332,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { ) public { testCreateRound(); - uint64 roundId = fundingPot.getRoundCount(); + uint32 roundId = fundingPot.getRoundCount(); uint8 accessId = 3; uint amount = 250; @@ -1378,7 +1372,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { ) public { testCreateRound(); - uint64 roundId = fundingPot.getRoundCount(); + uint32 roundId = fundingPot.getRoundCount(); uint8 accessId = 4; uint amount = 250; @@ -1420,7 +1414,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { ) public { testCreateRound(); - uint64 roundId = fundingPot.getRoundCount(); + uint32 roundId = fundingPot.getRoundCount(); uint8 accessId = 1; uint amount = 500; @@ -1510,7 +1504,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { function testContributeToRound_worksGivenAllConditionsMet() public { testCreateRound(); - uint64 roundId = fundingPot.getRoundCount(); + uint32 roundId = fundingPot.getRoundCount(); uint8 accessId = 1; uint amount = 250; @@ -1561,7 +1555,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { ); _helper_setupRoundWithAccessCriteria(accessCriteriaEnumOld); - uint64 roundId = fundingPot.getRoundCount(); + uint32 roundId = fundingPot.getRoundCount(); uint8 accessId = 0; uint amount = 201; @@ -1603,7 +1597,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { ) public { testCreateRound(); - uint64 roundId = fundingPot.getRoundCount(); + uint32 roundId = fundingPot.getRoundCount(); uint8 accessId = 1; uint firstAmount = 400; @@ -1656,7 +1650,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { { testCreateRound(); - uint64 roundId = fundingPot.getRoundCount(); + uint32 roundId = fundingPot.getRoundCount(); uint8 accessId = 1; uint amount = 250; @@ -1709,7 +1703,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { _defaultRoundParams.globalAccumulativeCaps ); - uint64 round1Id = fundingPot.getRoundCount(); + uint32 round1Id = fundingPot.getRoundCount(); uint8 accessCriteriaId = 1; ( @@ -1740,7 +1734,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { _defaultRoundParams.autoClosure, _defaultRoundParams.globalAccumulativeCaps ); - uint64 round2Id = fundingPot.getRoundCount(); + uint32 round2Id = fundingPot.getRoundCount(); fundingPot.setAccessCriteriaForRound( round2Id, accessCriteriaId, address(0), bytes32(0), allowedAddresses @@ -1807,7 +1801,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { _defaultRoundParams.autoClosure, _defaultRoundParams.globalAccumulativeCaps ); - uint64 round1Id = fundingPot.getRoundCount(); + uint32 round1Id = fundingPot.getRoundCount(); uint8 accessId = 0; ( @@ -1833,7 +1827,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { _defaultRoundParams.autoClosure, _defaultRoundParams.globalAccumulativeCaps ); - uint64 round2Id = fundingPot.getRoundCount(); + uint32 round2Id = fundingPot.getRoundCount(); fundingPot.setAccessCriteriaForRound( round2Id, accessId, nftContract, merkleRoot, allowedAddresses ); @@ -1952,7 +1946,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { vm.assume(user_ != address(0) && user_ != address(this)); testCreateRound(); - uint64 roundId = fundingPot.getRoundCount(); + uint32 roundId = fundingPot.getRoundCount(); vm.startPrank(user_); bytes32 roleId = _authorizer.generateRoleId( @@ -1968,7 +1962,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { } function testCloseRound_revertsGivenRoundDoesNotExist() public { - uint64 nonExistentRoundId = 999; + uint32 nonExistentRoundId = 999; vm.expectRevert( abi.encodeWithSelector( @@ -1983,7 +1977,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { function testCloseRound_revertsGivenRoundIsAlreadyClosed() public { testCloseRound_worksGivenRoundCapHasBeenReached(); // Try to close it again - uint64 roundId = fundingPot.getRoundCount(); + uint32 roundId = fundingPot.getRoundCount(); vm.expectRevert( abi.encodeWithSelector( @@ -1997,7 +1991,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { function testCloseRound_worksGivenRoundHasStartedButNotEnded() public { testCreateRound(); - uint64 roundId = fundingPot.getRoundCount(); + uint32 roundId = fundingPot.getRoundCount(); uint8 accessId = 1; ( @@ -2032,7 +2026,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { function testCloseRound_worksGivenRoundHasEnded() public { testCreateRound(); - uint64 roundId = fundingPot.getRoundCount(); + uint32 roundId = fundingPot.getRoundCount(); uint8 accessId = 1; ( @@ -2071,7 +2065,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { function testCloseRound_worksGivenRoundCapHasBeenReached() public { testCreateRound(); - uint64 roundId = fundingPot.getRoundCount(); + uint32 roundId = fundingPot.getRoundCount(); uint8 accessId = 2; uint amount = 1000; @@ -2110,7 +2104,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { function testCloseRound_worksGivenRoundisAutoClosure() public { testEditRound(); - uint64 roundId = fundingPot.getRoundCount(); + uint32 roundId = fundingPot.getRoundCount(); uint8 accessId = 2; uint amount = 2000; @@ -2146,7 +2140,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { function testCloseRound_worksWithMultipleContributors() public { testCreateRound(); - uint64 roundId = fundingPot.getRoundCount(); + uint32 roundId = fundingPot.getRoundCount(); // Set up access criteria uint8 accessId = 1; @@ -2227,7 +2221,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { function testCreatePaymentOrdersForContributorsBatch_revertsGivenRoundDoesNotExist( ) public { - uint64 nonExistentRoundId = 999; + uint32 nonExistentRoundId = 999; vm.expectRevert( abi.encodeWithSelector( @@ -2244,7 +2238,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { function testCreatePaymentOrdersForContributorsBatch_revertsGivenRoundIsNotClosed( ) public { testContributeToRound_worksGivenAllConditionsMet(); - uint64 roundId = fundingPot.getRoundCount(); + uint32 roundId = fundingPot.getRoundCount(); vm.expectRevert( abi.encodeWithSelector( @@ -2259,7 +2253,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { function testCreatePaymentOrdersForContributorsBatch_revertsGivenBatchSizeIsGreaterThanContributorCount( ) public { testCloseRound_worksWithMultipleContributors(); - uint64 roundId = fundingPot.getRoundCount(); + uint32 roundId = fundingPot.getRoundCount(); vm.expectRevert( abi.encodeWithSelector( @@ -2274,7 +2268,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { function testCreatePaymentOrdersForContributorsBatch_revertsGivenBatchSizeIsZero( ) public { testCloseRound_worksWithMultipleContributors(); - uint64 roundId = fundingPot.getRoundCount(); + uint32 roundId = fundingPot.getRoundCount(); vm.expectRevert( abi.encodeWithSelector( @@ -2289,7 +2283,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { function testCreatePaymentOrdersForContributorsBatch_revertsGivenUserDoesNotHaveFundingPotAdminRole( ) public { testCloseRound_worksWithMultipleContributors(); - uint64 roundId = fundingPot.getRoundCount(); + uint32 roundId = fundingPot.getRoundCount(); vm.startPrank(contributor1_); vm.expectRevert( @@ -2306,7 +2300,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { function testCreatePaymentOrdersForContributorsBatch_worksGivenRoundIsAutoClosure( ) public { testCloseRound_worksGivenRoundisAutoClosure(); - uint64 roundId = fundingPot.getRoundCount(); + uint32 roundId = fundingPot.getRoundCount(); fundingPot.createPaymentOrdersForContributorsBatch(roundId, 1); assertEq(fundingPot.paymentOrders().length, 1); @@ -2315,7 +2309,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { function testCreatePaymentOrdersForContributorsBatch_worksGivenRoundIsManualClosure( ) public { testCloseRound_worksWithMultipleContributors(); - uint64 roundId = fundingPot.getRoundCount(); + uint32 roundId = fundingPot.getRoundCount(); fundingPot.createPaymentOrdersForContributorsBatch(roundId, 3); assertEq(fundingPot.paymentOrders().length, 3); @@ -2324,7 +2318,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { // Internal Functions function testFuzz_validateAccessCriteria( - uint64 roundId_, + uint32 roundId_, uint8 accessId_, bytes32[] calldata merkleProof_ ) external view { @@ -2341,7 +2335,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { } function testFuzz_validateAndAdjustCapsWithUnspentCap( - uint64 roundId_, + uint32 roundId_, uint amount_, uint8 accessId_, bool canOverrideContributionSpan_, @@ -2397,7 +2391,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { params.roundEnd = block.timestamp + 2 days; params.roundCap = 1000; - uint64 roundId = fundingPot.createRound( + uint32 roundId = fundingPot.createRound( params.roundStart, params.roundEnd, params.roundCap, @@ -2445,7 +2439,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { params.roundEnd = block.timestamp + 2 days; params.roundCap = 1000; - uint64 roundId = fundingPot.createRound( + uint32 roundId = fundingPot.createRound( params.roundStart, params.roundEnd, params.roundCap, @@ -2468,7 +2462,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { params.roundEnd = block.timestamp + 2 days; params.roundCap = 1000; - uint64 roundId = fundingPot.createRound( + uint32 roundId = fundingPot.createRound( params.roundStart, params.roundEnd, params.roundCap, @@ -2488,7 +2482,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { params.roundEnd = 0; // No end time params.roundCap = 1000; - uint64 roundId = fundingPot.createRound( + uint32 roundId = fundingPot.createRound( params.roundStart, params.roundEnd, params.roundCap, @@ -2539,7 +2533,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { params.roundEnd = block.timestamp + 2 days; params.roundCap = 0; // No cap - uint64 roundId = fundingPot.createRound( + uint32 roundId = fundingPot.createRound( params.roundStart, params.roundEnd, params.roundCap, @@ -2550,18 +2544,22 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { ); // Should be false before end time - assertFalse(fundingPot.exposed_checkRoundClosureConditions(roundId)); + assertFalse( + fundingPot.exposed_checkRoundClosureConditions(uint32(roundId)) + ); // Should be true after end time vm.warp(params.roundEnd + 1); - assertTrue(fundingPot.exposed_checkRoundClosureConditions(roundId)); + assertTrue( + fundingPot.exposed_checkRoundClosureConditions(uint32(roundId)) + ); } // Test exposed internal function closeRound function test_closeRound_worksGivenCapReached() public { testCreateRound(); - uint64 roundId = fundingPot.getRoundCount(); + uint32 roundId = fundingPot.getRoundCount(); uint8 accessId = 2; uint amount = 1000; @@ -2592,14 +2590,16 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { roundId, amount, accessId, new bytes32[](0) ); - assertTrue(fundingPot.exposed_checkRoundClosureConditions(roundId)); + assertTrue( + fundingPot.exposed_checkRoundClosureConditions(uint32(roundId)) + ); uint startIndex = 0; uint batchSize = 1; - fundingPot.exposed_closeRound(roundId); - fundingPot.exposed_buyBondingCurveToken(roundId); + fundingPot.exposed_closeRound(uint32(roundId)); + fundingPot.exposed_buyBondingCurveToken(uint32(roundId)); fundingPot.exposed_createPaymentOrdersForContributors( - roundId, startIndex, batchSize + uint32(roundId), startIndex, batchSize ); assertTrue(fundingPot.isRoundClosed(roundId)); @@ -2684,7 +2684,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { function _helper_setupRoundWithAccessCriteria(uint8 accessCriteriaEnum) internal { - uint64 roundId = fundingPot.createRound( + uint32 roundId = fundingPot.createRound( _defaultRoundParams.roundStart, _defaultRoundParams.roundEnd, _defaultRoundParams.roundCap, diff --git a/test/unit/modules/logicModule/LM_PC_FundingPot_v1_Exposed.sol b/test/unit/modules/logicModule/LM_PC_FundingPot_v1_Exposed.sol index fadd242e6..036f5de01 100644 --- a/test/unit/modules/logicModule/LM_PC_FundingPot_v1_Exposed.sol +++ b/test/unit/modules/logicModule/LM_PC_FundingPot_v1_Exposed.sol @@ -13,7 +13,7 @@ contract LM_PC_FundingPot_v1_Exposed is LM_PC_FundingPot_v1 { /** * @notice Exposes the internal _getTotalRoundContribution function for testing */ - function exposed_getTotalRoundContributions(uint64 roundId_) + function exposed_getTotalRoundContributions(uint32 roundId_) external view returns (uint) @@ -24,7 +24,7 @@ contract LM_PC_FundingPot_v1_Exposed is LM_PC_FundingPot_v1 { /** * @notice Exposes the internal _getUserContributionToRound function for testing */ - function exposed_getUserContributionToRound(uint64 roundId_, address user_) + function exposed_getUserContributionToRound(uint32 roundId_, address user_) external view returns (uint) @@ -47,7 +47,7 @@ contract LM_PC_FundingPot_v1_Exposed is LM_PC_FundingPot_v1 { * @notice Exposes the internal _validateAndAdjustCapsWithUnspentCap function for testing */ function exposed_validateAndAdjustCapsWithUnspentCap( - uint64 roundId_, + uint32 roundId_, uint amount_, uint8 accessCriteriaId__, bool canOverrideContributionSpan_, @@ -66,7 +66,7 @@ contract LM_PC_FundingPot_v1_Exposed is LM_PC_FundingPot_v1 { * @notice Exposes the internal _validateAccessCriteria function for testing */ function exposed_validateAccessCriteria( - uint64 roundId_, + uint32 roundId_, uint8 accessId_, bytes32[] calldata merkleProof_, address user_ @@ -78,7 +78,7 @@ contract LM_PC_FundingPot_v1_Exposed is LM_PC_FundingPot_v1 { * @notice Exposes the internal _checkAccessCriteriaEligibility function for testing */ function exposed_checkAccessCriteriaEligibility( - uint64 roundId_, + uint32 roundId_, uint8 accessCriteriaId_, bytes32[] memory merkleProof_, address user_ @@ -106,7 +106,7 @@ contract LM_PC_FundingPot_v1_Exposed is LM_PC_FundingPot_v1 { bytes32 root_, bytes32[] memory merkleProof_, address user_, - uint64 roundId_ + uint32 roundId_ ) external pure returns (bool) { return _validateMerkleProof(root_, merkleProof_, user_, roundId_); } @@ -114,7 +114,7 @@ contract LM_PC_FundingPot_v1_Exposed is LM_PC_FundingPot_v1 { /** * @notice Exposes the internal _calculateUnusedCapacityFromPreviousRounds function for testing */ - function exposed_calculateUnusedCapacityFromPreviousRounds(uint64 roundId_) + function exposed_calculateUnusedCapacityFromPreviousRounds(uint32 roundId_) external view returns (uint) @@ -125,14 +125,14 @@ contract LM_PC_FundingPot_v1_Exposed is LM_PC_FundingPot_v1 { /** * @notice Exposes the internal _closeRound function for testing */ - function exposed_closeRound(uint64 roundId_) external { + function exposed_closeRound(uint32 roundId_) external { _closeRound(roundId_); } /** * @notice Exposes the internal _checkRoundClosureConditions function for testing */ - function exposed_checkRoundClosureConditions(uint64 roundId_) + function exposed_checkRoundClosureConditions(uint32 roundId_) external view returns (bool) @@ -143,7 +143,7 @@ contract LM_PC_FundingPot_v1_Exposed is LM_PC_FundingPot_v1 { /** * @notice Exposes the internal _buyBondingCurveToken function for testing */ - function exposed_buyBondingCurveToken(uint64 roundId_) external { + function exposed_buyBondingCurveToken(uint32 roundId_) external { return _buyBondingCurveToken(roundId_); } @@ -151,7 +151,7 @@ contract LM_PC_FundingPot_v1_Exposed is LM_PC_FundingPot_v1 { * @notice Exposes the internal _createPaymentOrdersForContributors function for testing */ function exposed_createPaymentOrdersForContributors( - uint64 roundId_, + uint32 roundId_, uint startIndex_, uint batchSize_ ) external { From 9ffc336b709dfe1011b097cef0286f09a8512fd0 Mon Sep 17 00:00:00 2001 From: JeffreyJoel Date: Mon, 28 Apr 2025 18:32:31 +0100 Subject: [PATCH 078/130] fix: PR feedback --- .../logicModule/LM_PC_FundingPot_v1.sol | 20 ++++++++----------- .../fundingManager/FundingManagerV1Mock.sol | 4 ++++ 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/modules/logicModule/LM_PC_FundingPot_v1.sol b/src/modules/logicModule/LM_PC_FundingPot_v1.sol index 8cffe0be5..bfa2c5914 100644 --- a/src/modules/logicModule/LM_PC_FundingPot_v1.sol +++ b/src/modules/logicModule/LM_PC_FundingPot_v1.sol @@ -648,7 +648,7 @@ contract LM_PC_FundingPot_v1 is _buyBondingCurveToken(roundId_); - // Payment orders will be created separately via processContributorBatch + // Payment orders will be created separately via createPaymentOrdersForContributorsBatch } else { revert Module__LM_PC_FundingPot__ClosureConditionsNotMet(); } @@ -1222,13 +1222,9 @@ contract LM_PC_FundingPot_v1 is AccessCriteriaPrivileges storage privileges = roundItToAccessCriteriaIdToPrivileges[roundId_][accessCriteriaId_]; - uint start = privileges.overrideContributionSpan - ? privileges.start - : round.roundStart; - uint cliff = privileges.overrideContributionSpan ? privileges.cliff : 0; - uint end = privileges.overrideContributionSpan - ? privileges.end - : round.roundEnd; + uint start = privileges.start; + uint cliff = privileges.cliff; + uint end = privileges.end; (bytes32 flags, bytes32[] memory finalData) = _createTimeParameterData(start, cliff, end); @@ -1271,13 +1267,13 @@ contract LM_PC_FundingPot_v1 is IERC20(__Module_orchestrator.fundingManager().token()).approve( address(__Module_orchestrator.fundingManager()), totalContributions ); - uint balanceBefore = IERC20(issuanceToken).balanceOf(address(this)); + uint minAmountOut = IBondingCurveBase_v1( + address(__Module_orchestrator.fundingManager()) + ).calculatePurchaseReturn(totalContributions); IBondingCurveBase_v1(address(__Module_orchestrator.fundingManager())) .buyFor(address(this), totalContributions, 1); - uint balanceAfter = IERC20(issuanceToken).balanceOf(address(this)); - uint tokensBought = balanceAfter - balanceBefore; - roundTokensBought[roundId_] = tokensBought; + roundTokensBought[roundId_] = minAmountOut; } /// @notice Checks if a round has reached its cap or time limit diff --git a/test/mocks/modules/fundingManager/FundingManagerV1Mock.sol b/test/mocks/modules/fundingManager/FundingManagerV1Mock.sol index 292e86f2f..7c6668b28 100644 --- a/test/mocks/modules/fundingManager/FundingManagerV1Mock.sol +++ b/test/mocks/modules/fundingManager/FundingManagerV1Mock.sol @@ -83,6 +83,10 @@ contract FundingManagerV1Mock is IFundingManager_v1, Module_v1 { return address(_bondingToken); } + function calculatePurchaseReturn(uint amount) public view returns (uint) { + return amount; + } + function buyFor(address to, uint amount, uint minTokens) public { _token.transferFrom(_msgSender(), address(this), amount); _bondingToken.mint(to, amount); From 531c29959d33b5525d9eb6b1f6e79e2c322d9e9a Mon Sep 17 00:00:00 2001 From: Zuhaib Mohammed Date: Tue, 29 Apr 2025 10:44:49 +0530 Subject: [PATCH 079/130] fix: PR Review --- src/modules/logicModule/LM_PC_FundingPot_v1.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/modules/logicModule/LM_PC_FundingPot_v1.sol b/src/modules/logicModule/LM_PC_FundingPot_v1.sol index bfa2c5914..8f659d16e 100644 --- a/src/modules/logicModule/LM_PC_FundingPot_v1.sol +++ b/src/modules/logicModule/LM_PC_FundingPot_v1.sol @@ -1271,7 +1271,7 @@ contract LM_PC_FundingPot_v1 is address(__Module_orchestrator.fundingManager()) ).calculatePurchaseReturn(totalContributions); IBondingCurveBase_v1(address(__Module_orchestrator.fundingManager())) - .buyFor(address(this), totalContributions, 1); + .buyFor(address(this), totalContributions, minAmountOut); roundTokensBought[roundId_] = minAmountOut; } From c272662f4f83569ed60e70e83a582d340c8f3b1b Mon Sep 17 00:00:00 2001 From: Fabian Scherer Date: Tue, 29 Apr 2025 21:40:54 +0200 Subject: [PATCH 080/130] chore: `contributeToRound` => `contributeToRoundFor` --- .../logicModule/LM_PC_FundingPot_v1.sol | 245 +++++++++--------- .../interfaces/ILM_PC_FundingPot_v1.sol | 64 ++--- .../logicModule/LM_PC_FundingPot_v1.t.sol | 153 ++++++----- .../LM_PC_FundingPot_v1_Exposed.sol | 2 + 4 files changed, 253 insertions(+), 211 deletions(-) diff --git a/src/modules/logicModule/LM_PC_FundingPot_v1.sol b/src/modules/logicModule/LM_PC_FundingPot_v1.sol index 8f659d16e..c3a90c73d 100644 --- a/src/modules/logicModule/LM_PC_FundingPot_v1.sol +++ b/src/modules/logicModule/LM_PC_FundingPot_v1.sol @@ -28,7 +28,7 @@ import "@oz/utils/cryptography/MerkleProof.sol"; import {EnumerableSet} from "@oz/utils/structs/EnumerableSet.sol"; /** - * @title Inverter Funding Pot Logic Module + * @title Inverter Funding Pot Logic Module. * * @notice A sophisticated funding management system that enables configurable fundraising rounds * with multiple access criteria, contribution limits, and automated distribution. @@ -36,37 +36,37 @@ import {EnumerableSet} from "@oz/utils/structs/EnumerableSet.sol"; * campaigns with granular access control and contribution management. * * @dev Implements a comprehensive funding system with the following features: - * - Round Configuration + * - Round Configuration. * Supports configurable start/end times, caps, and post-round hooks. * - * - Access Control + * - Access Control. * Multiple access criteria types: - * - Allowlist-based access - * - NFT ownership verification - * - Merkle proof validation - * - Open access + * - Allowlist-based access. + * - NFT ownership verification. + * - Merkle proof validation. + * - Open access. * - * - Contribution Management - * - Personal contribution caps - * - Round-level caps - * - Global accumulative caps across rounds - * - Configurable contribution time windows + * - Contribution Management. + * - Personal contribution caps. + * - Round-level caps. + * - Global accumulative caps across rounds. + * - Configurable contribution time windows. * - * - Automated Processing - * - Automatic round closure based on time or cap - * - Post-round hook execution - * - Payment order creation for contributors + * - Automated Processing. + * - Automatic round closure based on time or cap. + * - Post-round hook execution. + * - Payment order creation for contributors. * * @custom:security-contact security@inverter.network * In case of any concerns or findings, please refer to * our Security Policy at security.inverter.network or * email us directly! * - * @custom:version v1.0.0 + * @custom:version v1.0.0. * - * @custom:inverter-standard-version v0.1.0 + * @custom:inverter-standard-version v0.1.0. * - * @author 33Audits + * @author 33Audits. */ contract LM_PC_FundingPot_v1 is ILM_PC_FundingPot_v1, @@ -122,35 +122,35 @@ contract LM_PC_FundingPot_v1 is => mapping(uint8 accessCriteriaId_ => AccessCriteriaPrivileges) ) private roundItToAccessCriteriaIdToPrivileges; - /// @notice Maps round IDs to user addresses to contribution amounts + /// @notice Maps round IDs to user addresses to contribution amounts. mapping(uint32 => mapping(address => uint)) private roundIdToUserToContribution; - /// @notice Maps round IDs to total contributions + /// @notice Maps round IDs to total contributions. mapping(uint32 => uint) private roundIdToTotalContributions; - /// @notice Maps round IDs to closed status + /// @notice Maps round IDs to closed status. mapping(uint32 => bool) private roundIdToClosedStatus; - /// @notice Maps round IDs to bonding curve tokens bought + /// @notice Maps round IDs to bonding curve tokens bought. mapping(uint32 => uint) private roundTokensBought; - /// @notice Maps round IDs to contributors recipients + /// @notice Maps round IDs to contributors recipients. mapping(uint32 => EnumerableSet.AddressSet) private contributorsByRound; - /// @notice Maps round IDs to user addresses to contribution amounts by access criteria + /// @notice Maps round IDs to user addresses to contribution amounts by access criteria. mapping(uint64 => mapping(address => mapping(uint8 => uint))) private roundIdTouserContributionsByAccessCriteria; /// @notice The current round count. uint32 private roundCount; - /// @notice Storage gap for future upgrades. - uint[50] private __gap; - - // Add a mapping to track the next unprocessed index for each round + /// @notice Add a mapping to track the next unprocessed index for each round. mapping(uint64 => uint) private roundIdToNextUnprocessedIndex; + /// @notice Storage gap for future upgrades. + uint[50] private __gap; + // ------------------------------------------------------------------------- // Modifiers @@ -564,20 +564,22 @@ contract LM_PC_FundingPot_v1 is } /// @inheritdoc ILM_PC_FundingPot_v1 - function contributeToRound( + function contributeToRoundFor( + address user_, uint32 roundId_, uint amount_, uint8 accessCriteriaId_, bytes32[] calldata merkleProof_ ) external { // Call the internal function with no additional unspent personal cap - _contributeToRound( - roundId_, amount_, accessCriteriaId_, merkleProof_, 0 + _contributeToRoundFor( + user_, roundId_, amount_, accessCriteriaId_, merkleProof_, 0 ); } /// @inheritdoc ILM_PC_FundingPot_v1 - function contributeToRound( + function contributeToRoundFor( + address user_, uint32 roundId_, uint amount_, uint8 accessCriteriaId_, @@ -599,7 +601,7 @@ contract LM_PC_FundingPot_v1 is uint32(roundCap.roundId), roundCap.accessCriteriaId, roundCap.merkleProof, - _msgSender() + user_ ); if (isEligible) { @@ -607,9 +609,8 @@ contract LM_PC_FundingPot_v1 is roundItToAccessCriteriaIdToPrivileges[roundCap.roundId][roundCap .accessCriteriaId]; - uint userContribution = _getUserContributionToRound( - uint32(roundCap.roundId), _msgSender() - ); + uint userContribution = + _getUserContributionToRound(uint32(roundCap.roundId), user_); uint personalCap = privileges.personalCap; if (userContribution < personalCap) { @@ -618,7 +619,8 @@ contract LM_PC_FundingPot_v1 is } } - _contributeToRound( + _contributeToRoundFor( + user_, roundId_, amount_, accessCriteriaId_, @@ -761,13 +763,15 @@ contract LM_PC_FundingPot_v1 is return start_ + cliff_ <= end_; } - /// @notice Contributes to a round with unused capacity from previous rounds - /// @param roundId_ The ID of the round to contribute to - /// @param amount_ The amount to contribute - /// @param accessCriteriaId_ The ID of the access criteria to use for this contribution - /// @param merkleProof_ The Merkle proof for validation if needed - /// @param unspentPersonalCap_ The amount of unused capacity from previous rounds - function _contributeToRound( + /// @notice Contributes to a round with unused capacity from previous rounds. + /// @param roundId_ The ID of the round to contribute to. + /// @param user_ The address of the user to contribute for. + /// @param amount_ The amount to contribute. + /// @param accessCriteriaId_ The ID of the access criteria to use for this contribution. + /// @param merkleProof_ The Merkle proof for validation if needed. + /// @param unspentPersonalCap_ The amount of unused capacity from previous rounds. + function _contributeToRoundFor( + address user_, uint32 roundId_, uint amount_, uint8 accessCriteriaId_, @@ -794,7 +798,7 @@ contract LM_PC_FundingPot_v1 is } _validateAccessCriteria( - roundId_, accessCriteriaId_, merkleProof_, _msgSender() + roundId_, accessCriteriaId_, merkleProof_, user_ ); AccessCriteriaPrivileges storage privileges = @@ -810,6 +814,7 @@ contract LM_PC_FundingPot_v1 is // Calculate the adjusted amount considering caps uint adjustedAmount = _validateAndAdjustCapsWithUnspentCap( + user_, roundId_, amount_, accessCriteriaId_, @@ -817,18 +822,18 @@ contract LM_PC_FundingPot_v1 is unspentPersonalCap_ ); - roundIdToUserToContribution[roundId_][_msgSender()] += adjustedAmount; + roundIdToUserToContribution[roundId_][user_] += adjustedAmount; roundIdToTotalContributions[roundId_] += adjustedAmount; - roundIdTouserContributionsByAccessCriteria[roundId_][_msgSender()][accessCriteriaId_] + roundIdTouserContributionsByAccessCriteria[roundId_][user_][accessCriteriaId_] += adjustedAmount; __Module_orchestrator.fundingManager().token().safeTransferFrom( _msgSender(), address(this), adjustedAmount ); - EnumerableSet.add(contributorsByRound[roundId_], _msgSender()); + EnumerableSet.add(contributorsByRound[roundId_], user_); - emit ContributionMade(roundId_, _msgSender(), adjustedAmount); + emit ContributionMade(roundId_, user_, adjustedAmount); // contribution triggers automatic closure if (!roundIdToClosedStatus[roundId_] && round.autoClosure) { @@ -841,12 +846,12 @@ contract LM_PC_FundingPot_v1 is } } - /// @notice Validates access criteria for a specific round and access type - /// @dev Checks if a user meets the access requirements based on the round's access criteria - /// @param roundId_ The ID of the round being validated - /// @param accessCriteriaId_ The ID of the specific access criteria - /// @param merkleProof_ Merkle proof for Merkle tree-based access (optional) - /// @param user_ The address of the user to validate + /// @notice Validates access criteria for a specific round and access type. + /// @dev Checks if a user meets the access requirements based on the round's access criteria. + /// @param roundId_ The ID of the round being validated. + /// @param accessCriteriaId_ The ID of the specific access criteria. + /// @param merkleProof_ Merkle proof for Merkle tree-based access (optional). + /// @param user_ The address of the user to validate. function _validateAccessCriteria( uint32 roundId_, uint8 accessCriteriaId_, @@ -876,13 +881,15 @@ contract LM_PC_FundingPot_v1 is } } - /// @notice Validates and adjusts the contribution amount considering caps and unspent capacity - /// @param roundId_ The ID of the round to contribute to - /// @param amount_ The amount to contribute - /// @param accessCriteriaId__ The ID of the access criteria to use for this contribution - /// @param canOverrideContributionSpan_ Whether the contribution span can be overridden - /// @param unspentPersonalCap_ The amount of unused capacity from previous rounds + /// @notice Validates and adjusts the contribution amount considering caps and unspent capacity. + /// @param user_ The address of the user to contribute for. + /// @param roundId_ The ID of the round to contribute to. + /// @param amount_ The amount to contribute. + /// @param accessCriteriaId__ The ID of the access criteria to use for this contribution. + /// @param canOverrideContributionSpan_ Whether the contribution span can be overridden. + /// @param unspentPersonalCap_ The amount of unused capacity from previous rounds. function _validateAndAdjustCapsWithUnspentCap( + address user_, uint32 roundId_, uint amount_, uint8 accessCriteriaId__, @@ -918,7 +925,7 @@ contract LM_PC_FundingPot_v1 is // Check and adjust for personal cap uint userPreviousContribution = - _getUserContributionToRound(roundId_, _msgSender()); + _getUserContributionToRound(roundId_, user_); // Get the base personal cap for this round and criteria AccessCriteriaPrivileges storage privileges = @@ -941,13 +948,13 @@ contract LM_PC_FundingPot_v1 is return adjustedAmount; } - /// @notice Checks if a user meets the access criteria for a specific round and access type - /// @dev Returns true if the user meets the access criteria, reverts otherwise - /// @param roundId_ The ID of the round being validated - /// @param accessCriteriaId_ The ID of the specific access criteria - /// @param merkleProof_ Merkle proof for Merkle tree-based access (optional) - /// @param user_ The address of the user to validate - /// @return isEligible True if the user meets the access criteria, false otherwise + /// @notice Checks if a user meets the access criteria for a specific round and access type. + /// @dev Returns true if the user meets the access criteria, reverts otherwise. + /// @param roundId_ The ID of the round being validated. + /// @param accessCriteriaId_ The ID of the specific access criteria. + /// @param merkleProof_ Merkle proof for Merkle tree-based access (optional). + /// @param user_ The address of the user to validate. + /// @return isEligible True if the user meets the access criteria, false otherwise. function _checkAccessCriteriaEligibility( uint32 roundId_, uint8 accessCriteriaId_, @@ -977,9 +984,9 @@ contract LM_PC_FundingPot_v1 is return isEligible; } - /// @notice Calculates unused capacity from previous rounds - /// @param roundId_ The ID of the current round - /// @return unusedCapacityFromPrevious The total unused capacity from previous rounds + /// @notice Calculates unused capacity from previous rounds. + /// @param roundId_ The ID of the current round. + /// @return unusedCapacityFromPrevious The total unused capacity from previous rounds. function _calculateUnusedCapacityFromPreviousRounds(uint32 roundId_) internal view @@ -1000,10 +1007,10 @@ contract LM_PC_FundingPot_v1 is return unusedCapacityFromPrevious; } - /// @notice Retrieves the total contribution for a specific round - /// @dev Returns the accumulated contributions for the given round - /// @param roundId_ The ID of the round to check contributions for - /// @return The total contributions for the specified round + /// @notice Retrieves the total contribution for a specific round. + /// @dev Returns the accumulated contributions for the given round. + /// @param roundId_ The ID of the round to check contributions for. + /// @return The total contributions for the specified round. function _getTotalRoundContribution(uint32 roundId_) internal view @@ -1012,11 +1019,11 @@ contract LM_PC_FundingPot_v1 is return roundIdToTotalContributions[roundId_]; } - /// @notice Retrieves the contribution amount for a specific user in a round - /// @dev Returns the individual user's contribution for the given round - /// @param roundId_ The ID of the round to check contributions for - /// @param user_ The address of the user - /// @return The user's contribution amount for the specified round + /// @notice Retrieves the contribution amount for a specific user in a round. + /// @dev Returns the individual user's contribution for the given round. + /// @param roundId_ The ID of the round to check contributions for. + /// @param user_ The address of the user. + /// @return The user's contribution amount for the specified round. function _getUserContributionToRound(uint32 roundId_, address user_) internal view @@ -1025,11 +1032,11 @@ contract LM_PC_FundingPot_v1 is return roundIdToUserToContribution[roundId_][user_]; } - /// @notice Verifies NFT ownership for access control - /// @dev Safely checks the NFT balance of a user using a try-catch block - /// @param nftContract_ Address of the NFT contract - /// @param user_ Address of the user to check for NFT ownership - /// @return Boolean indicating whether the user owns an NFT + /// @notice Verifies NFT ownership for access control. + /// @dev Safely checks the NFT balance of a user using a try-catch block. + /// @param nftContract_ Address of the NFT contract. + /// @param user_ Address of the user to check for NFT ownership. + /// @return Boolean indicating whether the user owns an NFT. function _checkNftOwnership(address nftContract_, address user_) internal view @@ -1049,13 +1056,13 @@ contract LM_PC_FundingPot_v1 is } } - /// @notice Verifies a Merkle p roof for access control - /// @dev Validates that the user's address is part of the Merkle tree - /// @param root_ The Merkle root to validate against - /// @param user_ The address of the user to check - /// @param roundId_ The ID of the round to check - /// @param merkleProof_ The Merkle proof to verify - /// @return Boolean indicating whether the proof is valid + /// @notice Verifies a Merkle p roof for access control. + /// @dev Validates that the user's address is part of the Merkle tree. + /// @param root_ The Merkle root to validate against. + /// @param user_ The address of the user to check. + /// @param roundId_ The ID of the round to check. + /// @param merkleProof_ The Merkle proof to verify. + /// @return Boolean indicating whether the proof is valid. function _validateMerkleProof( bytes32 root_, bytes32[] memory merkleProof_, @@ -1071,9 +1078,9 @@ contract LM_PC_FundingPot_v1 is return true; } - /// @notice Handles round closure logic - /// @dev Updates round status and executes hook if needed - /// @param roundId_ The ID of the round to close + /// @notice Handles round closure logic. + /// @dev Updates round status and executes hook if needed. + /// @param roundId_ The ID of the round to close. function _closeRound(uint32 roundId_) internal { Round storage round = rounds[roundId_]; @@ -1089,11 +1096,11 @@ contract LM_PC_FundingPot_v1 is emit RoundClosed(roundId_, roundIdToTotalContributions[roundId_]); } - /// @notice Creates payment orders for contributors in a round based on their access criteria - /// @dev Processes a batch of contributors to handle gas limit concerns - /// @param roundId_ The ID of the round to create payment orders for - /// @param startIndex_ The starting index in the contributors array - /// @param batchSize_ The number of contributors to process in this batch + /// @notice Creates payment orders for contributors in a round based on their access criteria. + /// @dev Processes a batch of contributors to handle gas limit concerns. + /// @param roundId_ The ID of the round to create payment orders for. + /// @param startIndex_ The starting index in the contributors array. + /// @param batchSize_ The number of contributors to process in this batch. function _createPaymentOrdersForContributors( uint32 roundId_, uint startIndex_, @@ -1158,13 +1165,13 @@ contract LM_PC_FundingPot_v1 is emit ContributorBatchProcessed(roundId_, startIndex_, endIndex); } - /// @notice Creates time parameter data for a payment order - /// @dev Sets default values for start, cliff, and end if they are zero - /// @param start_ The start time of the payment order - /// @param cliff_ The cliff time of the payment order - /// @param end_ The end time of the payment order - /// @return flags The flags for the payment order - /// @return finalData The final data for the payment order + /// @notice Creates time parameter data for a payment order. + /// @dev Sets default values for start, cliff, and end if they are zero. + /// @param start_ The start time of the payment order. + /// @param cliff_ The cliff time of the payment order. + /// @param end_ The end time of the payment order. + /// @return flags The flags for the payment order. + /// @return finalData The final data for the payment order. function _createTimeParameterData(uint start_, uint cliff_, uint end_) internal view @@ -1203,13 +1210,13 @@ contract LM_PC_FundingPot_v1 is return (flags, finalData); } - /// @notice Creates and adds a payment order for a contributor - /// @dev Sets default values for start, cliff, and end if they are zero - /// @param roundId_ The ID of the round to create the payment order for - /// @param recipient_ The address of the recipient of the payment order - /// @param accessCriteriaId_ The ID of the specific access criteria - /// @param tokensAmount_ The amount of tokens for the payment order - /// @param issuanceToken_ The issuance token for the payment order + /// @notice Creates and adds a payment order for a contributor. + /// @dev Sets default values for start, cliff, and end if they are zero. + /// @param roundId_ The ID of the round to create the payment order for. + /// @param recipient_ The address of the recipient of the payment order. + /// @param accessCriteriaId_ The ID of the specific access criteria. + /// @param tokensAmount_ The amount of tokens for the payment order. + /// @param issuanceToken_ The issuance token for the payment order. function _createAndAddPaymentOrder( uint32 roundId_, address recipient_, @@ -1276,9 +1283,9 @@ contract LM_PC_FundingPot_v1 is roundTokensBought[roundId_] = minAmountOut; } - /// @notice Checks if a round has reached its cap or time limit - /// @param roundId_ The ID of the round to check - /// @return Boolean indicating if the round has reached its cap or time limit + /// @notice Checks if a round has reached its cap or time limit. + /// @param roundId_ The ID of the round to check. + /// @return Boolean indicating if the round has reached its cap or time limit. function _checkRoundClosureConditions(uint32 roundId_) internal view diff --git a/src/modules/logicModule/interfaces/ILM_PC_FundingPot_v1.sol b/src/modules/logicModule/interfaces/ILM_PC_FundingPot_v1.sol index 2bd1db1aa..0cc141d51 100644 --- a/src/modules/logicModule/interfaces/ILM_PC_FundingPot_v1.sol +++ b/src/modules/logicModule/interfaces/ILM_PC_FundingPot_v1.sol @@ -55,23 +55,23 @@ interface ILM_PC_FundingPot_v1 is IERC20PaymentClientBase_v2 { uint end; } - /// @notice Struct used to specify previous round's access criteria for carry-over capacity - /// @param roundId The ID of the previous round - /// @param accessCriteriaId The ID of the access criteria in that round - /// @param merkleProof The Merkle proof needed to validate eligibility (if needed) + /// @notice Struct used to specify previous round's access criteria for carry-over capacity. + /// @param roundId The ID of the previous round. + /// @param accessCriteriaId The ID of the access criteria in that round. + /// @param merkleProof The Merkle proof needed to validate eligibility (if needed). struct UnspentPersonalRoundCap { uint32 roundId; uint8 accessCriteriaId; bytes32[] merkleProof; } - /// @notice Struct to represent a user's complete eligibility information for a round - /// @param isEligible Whether the user is eligible for the round through any criteria - /// @param isNftHolder Whether the user is eligible through NFT holding - /// @param isInMerkleTree Whether the user is eligible through Merkle proof - /// @param isInAllowlist Whether the user is eligible through allowlist - /// @param highestPersonalCap The highest personal cap the user can access - /// @param canOverrideContributionSpan Whether the user has any criteria that can override contribution span + /// @notice Struct to represent a user's complete eligibility information for a round. + /// @param isEligible Whether the user is eligible for the round through any criteria. + /// @param isNftHolder Whether the user is eligible through NFT holding. + /// @param isInMerkleTree Whether the user is eligible through Merkle proof. + /// @param isInAllowlist Whether the user is eligible through allowlist. + /// @param highestPersonalCap The highest personal cap the user can access. + /// @param canOverrideContributionSpan Whether the user has any criteria that can override contribution span. struct RoundUserEligibility { bool isEligible; bool isNftHolder; @@ -91,7 +91,6 @@ interface ILM_PC_FundingPot_v1 is IERC20PaymentClientBase_v2 { NFT, // 2 MERKLE, // 3 LIST // 4 - } // ------------------------------------------------------------------------- @@ -249,6 +248,7 @@ interface ILM_PC_FundingPot_v1 is IERC20PaymentClientBase_v2 { /// @notice Invalid access criteria ID. error Module__LM_PC_FundingPot__InvalidAccessCriteriaId(); + /// @notice Cannot set Privileges for open access criteria. error Module__LM_PC_FundingPot__CannotSetPrivilegesForOpenAccessCriteria(); @@ -373,13 +373,13 @@ interface ILM_PC_FundingPot_v1 is IERC20PaymentClientBase_v2 { /// @return The closed status of the round. function isRoundClosed(uint32 roundId_) external view returns (bool); - /// @notice Gets eligibility information for a user in a specific round - /// @param roundId_ The ID of the round to check eligibility for - /// @param accessCriteriaId_ The ID of the access criteria to check eligibility for - /// @param merkleProof_ The Merkle proof for validation if needed - /// @param user_ The address of the user to check - /// @return isEligible Whether the user is eligible for the round through any criteria - /// @return remainingAmountAllowedToContribute The remaining contribution the user can make + /// @notice Gets eligibility information for a user in a specific round. + /// @param roundId_ The ID of the round to check eligibility for. + /// @param accessCriteriaId_ The ID of the access criteria to check eligibility for. + /// @param merkleProof_ The Merkle proof for validation if needed. + /// @param user_ The address of the user to check. + /// @return isEligible Whether the user is eligible for the round through any criteria. + /// @return remainingAmountAllowedToContribute The remaining contribution the user can make. function getUserEligibility( uint32 roundId_, uint8 accessCriteriaId_, @@ -496,24 +496,28 @@ interface ILM_PC_FundingPot_v1 is IERC20PaymentClientBase_v2 { /// @notice Allows a user to contribute to a specific funding round. /// @dev Verifies the contribution eligibility based on the provided Merkle proof. + /// @param user_ The address of the user to contribute for. /// @param roundId_ The unique identifier of the funding round. /// @param amount_ The amount of tokens being contributed. /// @param accessCriteriaId_ The identifier for the access criteria to validate eligibility. /// @param merkleProof_ The Merkle proof used to verify the contributor's eligibility. - function contributeToRound( + function contributeToRoundFor( + address user_, uint32 roundId_, uint amount_, uint8 accessCriteriaId_, bytes32[] calldata merkleProof_ ) external; - /// @notice Allows a user to contribute to a round with unused capacity from previous rounds - /// @param roundId_ The ID of the round to contribute to - /// @param amount_ The amount to contribute - /// @param accessCriteriaId_ The ID of the access criteria to use for this contribution - /// @param merkleProof_ The Merkle proof for validation if needed - /// @param unspentPersonalRoundCaps_ Array of previous rounds and access criteria to calculate unused capacity from - function contributeToRound( + /// @notice Allows a user to contribute to a round with unused capacity from previous rounds. + /// @param user_ The address of the user to contribute for. + /// @param roundId_ The ID of the round to contribute to. + /// @param amount_ The amount to contribute. + /// @param accessCriteriaId_ The ID of the access criteria to use for this contribution. + /// @param merkleProof_ The Merkle proof for validation if needed. + /// @param unspentPersonalRoundCaps_ Array of previous rounds and access criteria to calculate unused capacity from. + function contributeToRoundFor( + address user_, uint32 roundId_, uint amount_, uint8 accessCriteriaId_, @@ -525,9 +529,9 @@ interface ILM_PC_FundingPot_v1 is IERC20PaymentClientBase_v2 { /// @param roundId_ The ID of the round to close. function closeRound(uint32 roundId_) external; - /// @notice Creates a batch of contributors for payment order creation - /// @param roundId_ The ID of the round to process contributors for - /// @param batchSize_ The number of contributors to process in this batch + /// @notice Creates a batch of contributors for payment order creation. + /// @param roundId_ The ID of the round to process contributors for. + /// @param batchSize_ The number of contributors to process in this batch. function createPaymentOrdersForContributorsBatch( uint32 roundId_, uint batchSize_ diff --git a/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol b/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol index 0c27b1175..c86e9d269 100644 --- a/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol +++ b/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol @@ -1178,7 +1178,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { ); } - /* Test: contributeToRound() unhappy paths + /* Test: contributeToRoundFor() unhappy paths ├── Given the round has not started yet │ └── When the user contributes to the round │ └── Then the transaction should revert @@ -1212,9 +1212,8 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { │ └── Then the transaction should revert */ - function testContributeToRound_revertsGivenContributionIsBeforeRoundStart() - public - { + function testcontributeToRoundFor_revertsGivenContributionIsBeforeRoundStart( + ) public { testCreateRound(); uint32 roundId = fundingPot.getRoundCount(); @@ -1247,12 +1246,12 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { ); vm.prank(contributor1_); - fundingPot.contributeToRound( - roundId, amount, accessId, new bytes32[](0) + fundingPot.contributeToRoundFor( + contributor1_, roundId, amount, accessId, new bytes32[](0) ); } - function testContributeToRound_revertsGivenContributionIsAfterRoundEnd() + function testcontributeToRoundFor_revertsGivenContributionIsAfterRoundEnd() public { testCreateRound(); @@ -1290,12 +1289,12 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { ); vm.prank(contributor1_); - fundingPot.contributeToRound( - roundId, amount, accessId, new bytes32[](0) + fundingPot.contributeToRoundFor( + contributor1_, roundId, amount, accessId, new bytes32[](0) ); } - function testContributeToRound_revertsGivenNFTAccessCriteriaIsNotMet() + function testcontributeToRoundFor_revertsGivenNFTAccessCriteriaIsNotMet() public { uint8 accessId = 2; @@ -1323,12 +1322,12 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { ); vm.prank(contributor1_); - fundingPot.contributeToRound( - roundId, amount, accessId, new bytes32[](0) + fundingPot.contributeToRoundFor( + contributor1_, roundId, amount, accessId, new bytes32[](0) ); } - function testContributeToRound_revertsGivenMerkleRootAccessCriteriaIsNotMet( + function testcontributeToRoundFor_revertsGivenMerkleRootAccessCriteriaIsNotMet( ) public { testCreateRound(); @@ -1365,10 +1364,12 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { ); vm.prank(contributor1_); - fundingPot.contributeToRound(roundId, amount, accessId, PROOF); + fundingPot.contributeToRoundFor( + contributor1_, roundId, amount, accessId, PROOF + ); } - function testContributeToRound_revertsGivenAllowedListAccessCriteriaIsNotMet( + function testcontributeToRoundFor_revertsGivenAllowedListAccessCriteriaIsNotMet( ) public { testCreateRound(); @@ -1405,12 +1406,12 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { ); vm.prank(contributor1_); - fundingPot.contributeToRound( - roundId, amount, accessId, new bytes32[](0) + fundingPot.contributeToRoundFor( + contributor1_, roundId, amount, accessId, new bytes32[](0) ); } - function testContributeToRound_revertsGivenPreviousContributionExceedsPersonalCap( + function testcontributeToRoundFor_revertsGivenPreviousContributionExceedsPersonalCap( ) public { testCreateRound(); @@ -1441,8 +1442,8 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { _token.approve(address(fundingPot), 1000); vm.prank(contributor1_); - fundingPot.contributeToRound( - roundId, amount, accessId, new bytes32[](0) + fundingPot.contributeToRoundFor( + contributor1_, roundId, amount, accessId, new bytes32[](0) ); // Attempt to contribute beyond personal cap @@ -1455,10 +1456,12 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { ); vm.prank(contributor1_); - fundingPot.contributeToRound(roundId, 251, accessId, new bytes32[](0)); + fundingPot.contributeToRoundFor( + contributor1_, roundId, 251, accessId, new bytes32[](0) + ); } - /* Test: contributeToRound() happy paths + /* Test: contributeToRoundFor() happy paths ├── Given a round has been configured with generic round configuration and access criteria │ And the round has started │ And the user fulfills the access criteria @@ -1501,7 +1504,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { │ And the funds are transferred into the funding pot │ */ - function testContributeToRound_worksGivenAllConditionsMet() public { + function testcontributeToRoundFor_worksGivenAllConditionsMet() public { testCreateRound(); uint32 roundId = fundingPot.getRoundCount(); @@ -1530,8 +1533,8 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { _token.approve(address(fundingPot), 500); vm.prank(contributor1_); - fundingPot.contributeToRound( - roundId, amount, accessId, new bytes32[](0) + fundingPot.contributeToRoundFor( + contributor1_, roundId, amount, accessId, new bytes32[](0) ); uint totalContributions = @@ -1544,7 +1547,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { assertEq(personalContributions, amount); } - function testContributeToRound_worksGivenUserCurrentContributionExceedsTheRoundCap( + function testcontributeToRoundFor_worksGivenUserCurrentContributionExceedsTheRoundCap( uint8 accessCriteriaEnumOld, uint8 accessCriteriaEnumNew ) public { @@ -1580,8 +1583,8 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { _token.approve(address(fundingPot), amount); vm.prank(contributor1_); - fundingPot.contributeToRound( - roundId, amount, accessId, new bytes32[](0) + fundingPot.contributeToRoundFor( + contributor1_, roundId, amount, accessId, new bytes32[](0) ); // only the amount that does not exceed the roundcap is contributed @@ -1593,7 +1596,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { ); } - function testContributeToRound_worksGivenContributionPartiallyExceedingPersonalCap( + function testcontributeToRoundFor_worksGivenContributionPartiallyExceedingPersonalCap( ) public { testCreateRound(); @@ -1627,15 +1630,15 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { // First contribution vm.prank(contributor1_); - fundingPot.contributeToRound( - roundId, firstAmount, accessId, new bytes32[](0) + fundingPot.contributeToRoundFor( + contributor1_, roundId, firstAmount, accessId, new bytes32[](0) ); uint secondAmount = 200; vm.prank(contributor1_); - fundingPot.contributeToRound( - roundId, secondAmount, accessId, new bytes32[](0) + fundingPot.contributeToRoundFor( + contributor1_, roundId, secondAmount, accessId, new bytes32[](0) ); uint totalContribution = fundingPot.exposed_getUserContributionToRound( @@ -1645,7 +1648,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { assertEq(totalContribution, personalCap); } - function testContributeToRound_worksGivenUserCanOverrideTimeConstraints() + function testcontributeToRoundFor_worksGivenUserCanOverrideTimeConstraints() public { testCreateRound(); @@ -1681,8 +1684,8 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { // This should succeed despite being after round end, due to override privilege vm.prank(contributor1_); - fundingPot.contributeToRound( - roundId, amount, accessId, new bytes32[](0) + fundingPot.contributeToRoundFor( + contributor1_, roundId, amount, accessId, new bytes32[](0) ); // Verify the contribution was recorded @@ -1691,7 +1694,9 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { assertEq(totalContribution, amount); } - function testContributeToRound_worksGivenPersonalCapAccumulation() public { + function testcontributeToRoundFor_worksGivenPersonalCapAccumulation() + public + { _defaultRoundParams.globalAccumulativeCaps = true; fundingPot.createRound( _defaultRoundParams.roundStart, @@ -1748,8 +1753,8 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { vm.warp(_defaultRoundParams.roundStart + 1); vm.startPrank(contributor1_); _token.approve(address(fundingPot), 1000); - fundingPot.contributeToRound( - round1Id, 200, accessCriteriaId, new bytes32[](0) + fundingPot.contributeToRoundFor( + contributor1_, round1Id, 200, accessCriteriaId, new bytes32[](0) ); // Warp to round 2 @@ -1765,8 +1770,13 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { }); // Contribute to round 2 with unspent capacity from round 1 - fundingPot.contributeToRound( - round2Id, 700, accessCriteriaId, new bytes32[](0), unspentCaps + fundingPot.contributeToRoundFor( + contributor1_, + round2Id, + 700, + accessCriteriaId, + new bytes32[](0), + unspentCaps ); vm.stopPrank(); @@ -1786,7 +1796,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { ); } - function testContributeToRound_worksGivenTotalRoundCapAccumulation() + function testcontributeToRoundFor_worksGivenTotalRoundCapAccumulation() public { _defaultRoundParams.globalAccumulativeCaps = true; // global accumulative caps enabled @@ -1840,12 +1850,16 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { vm.startPrank(contributor1_); _token.approve(address(fundingPot), 300); - fundingPot.contributeToRound(round1Id, 300, accessId, new bytes32[](0)); + fundingPot.contributeToRoundFor( + contributor1_, round1Id, 300, accessId, new bytes32[](0) + ); vm.stopPrank(); vm.startPrank(contributor2_); _token.approve(address(fundingPot), 200); - fundingPot.contributeToRound(round1Id, 200, accessId, new bytes32[](0)); + fundingPot.contributeToRoundFor( + contributor2_, round1Id, 200, accessId, new bytes32[](0) + ); vm.stopPrank(); // Move to Round 2 @@ -1855,12 +1869,16 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { vm.startPrank(contributor2_); _token.approve(address(fundingPot), 400); - fundingPot.contributeToRound(round2Id, 400, accessId, new bytes32[](0)); + fundingPot.contributeToRoundFor( + contributor2_, round2Id, 400, accessId, new bytes32[](0) + ); vm.stopPrank(); vm.startPrank(contributor3_); _token.approve(address(fundingPot), 300); - fundingPot.contributeToRound(round2Id, 300, accessId, new bytes32[](0)); + fundingPot.contributeToRoundFor( + contributor3_, round2Id, 300, accessId, new bytes32[](0) + ); vm.stopPrank(); // Verify Round 1 contributions @@ -2014,7 +2032,9 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { // Make a contribution vm.startPrank(contributor1_); _token.approve(address(fundingPot), 1000); - fundingPot.contributeToRound(roundId, 1000, accessId, new bytes32[](0)); + fundingPot.contributeToRoundFor( + contributor1_, roundId, 1000, accessId, new bytes32[](0) + ); vm.stopPrank(); // Close the round @@ -2049,7 +2069,9 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { vm.startPrank(contributor1_); _token.approve(address(fundingPot), 500); - fundingPot.contributeToRound(roundId, 500, accessId, new bytes32[](0)); + fundingPot.contributeToRoundFor( + contributor1_, roundId, 500, accessId, new bytes32[](0) + ); vm.stopPrank(); // Warp to after round end @@ -2092,8 +2114,8 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { _token.approve(address(fundingPot), 1000); vm.prank(contributor1_); - fundingPot.contributeToRound( - roundId, amount, accessId, new bytes32[](0) + fundingPot.contributeToRoundFor( + contributor1_, roundId, amount, accessId, new bytes32[](0) ); assertEq(fundingPot.isRoundClosed(roundId), false); @@ -2131,8 +2153,8 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { _token.approve(address(fundingPot), 2000); vm.prank(contributor1_); - fundingPot.contributeToRound( - roundId, amount, accessId, new bytes32[](0) + fundingPot.contributeToRoundFor( + contributor1_, roundId, amount, accessId, new bytes32[](0) ); assertEq(fundingPot.isRoundClosed(roundId), true); @@ -2164,17 +2186,23 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { // Multiple contributors vm.startPrank(contributor1_); _token.approve(address(fundingPot), 500); - fundingPot.contributeToRound(roundId, 500, accessId, new bytes32[](0)); + fundingPot.contributeToRoundFor( + contributor1_, roundId, 500, accessId, new bytes32[](0) + ); vm.stopPrank(); vm.startPrank(contributor2_); _token.approve(address(fundingPot), 200); - fundingPot.contributeToRound(roundId, 200, accessId, new bytes32[](0)); + fundingPot.contributeToRoundFor( + contributor2_, roundId, 200, accessId, new bytes32[](0) + ); vm.stopPrank(); vm.startPrank(contributor3_); _token.approve(address(fundingPot), 300); - fundingPot.contributeToRound(roundId, 300, accessId, new bytes32[](0)); + fundingPot.contributeToRoundFor( + contributor3_, roundId, 300, accessId, new bytes32[](0) + ); vm.stopPrank(); // Close the round @@ -2237,7 +2265,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { function testCreatePaymentOrdersForContributorsBatch_revertsGivenRoundIsNotClosed( ) public { - testContributeToRound_worksGivenAllConditionsMet(); + testcontributeToRoundFor_worksGivenAllConditionsMet(); uint32 roundId = fundingPot.getRoundCount(); vm.expectRevert( @@ -2347,6 +2375,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { vm.assume(unspentPersonalCap_ >= 0); try fundingPot.exposed_validateAndAdjustCapsWithUnspentCap( + contributor1_, roundId_, amount_, accessId_, @@ -2425,8 +2454,8 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { vm.warp(params.roundStart + 1); vm.startPrank(contributor1_); _token.approve(address(fundingPot), params.roundCap); - fundingPot.contributeToRound( - roundId, params.roundCap, accessId, new bytes32[](0) + fundingPot.contributeToRoundFor( + contributor1_, roundId, params.roundCap, accessId, new bytes32[](0) ); vm.stopPrank(); @@ -2519,8 +2548,8 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { vm.warp(params.roundStart + 1); vm.startPrank(contributor1_); _token.approve(address(fundingPot), params.roundCap); - fundingPot.contributeToRound( - roundId, params.roundCap, accessId, new bytes32[](0) + fundingPot.contributeToRoundFor( + contributor1_, roundId, params.roundCap, accessId, new bytes32[](0) ); vm.stopPrank(); @@ -2586,8 +2615,8 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { _token.approve(address(fundingPot), 1000); vm.prank(contributor1_); - fundingPot.contributeToRound( - roundId, amount, accessId, new bytes32[](0) + fundingPot.contributeToRoundFor( + contributor1_, roundId, amount, accessId, new bytes32[](0) ); assertTrue( diff --git a/test/unit/modules/logicModule/LM_PC_FundingPot_v1_Exposed.sol b/test/unit/modules/logicModule/LM_PC_FundingPot_v1_Exposed.sol index 036f5de01..1e2f754a7 100644 --- a/test/unit/modules/logicModule/LM_PC_FundingPot_v1_Exposed.sol +++ b/test/unit/modules/logicModule/LM_PC_FundingPot_v1_Exposed.sol @@ -47,6 +47,7 @@ contract LM_PC_FundingPot_v1_Exposed is LM_PC_FundingPot_v1 { * @notice Exposes the internal _validateAndAdjustCapsWithUnspentCap function for testing */ function exposed_validateAndAdjustCapsWithUnspentCap( + address user_, uint32 roundId_, uint amount_, uint8 accessCriteriaId__, @@ -54,6 +55,7 @@ contract LM_PC_FundingPot_v1_Exposed is LM_PC_FundingPot_v1 { uint unspentPersonalCap_ ) external view returns (uint) { return _validateAndAdjustCapsWithUnspentCap( + user_, roundId_, amount_, accessCriteriaId__, From dad6eb31f0db59d023fae9dba12fa988401492c6 Mon Sep 17 00:00:00 2001 From: Fabian Scherer Date: Tue, 29 Apr 2025 21:55:46 +0200 Subject: [PATCH 081/130] fix: consistent uint64 for roundId --- src/modules/logicModule/LM_PC_FundingPot_v1.sol | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/modules/logicModule/LM_PC_FundingPot_v1.sol b/src/modules/logicModule/LM_PC_FundingPot_v1.sol index c3a90c73d..8b5b68b22 100644 --- a/src/modules/logicModule/LM_PC_FundingPot_v1.sol +++ b/src/modules/logicModule/LM_PC_FundingPot_v1.sol @@ -113,6 +113,9 @@ contract LM_PC_FundingPot_v1 is // ------------------------------------------------------------------------- // State + /// @notice The current round count. + uint32 private roundCount; + /// @notice Stores all funding rounds by their unique ID. mapping(uint32 => Round) private rounds; @@ -139,14 +142,11 @@ contract LM_PC_FundingPot_v1 is mapping(uint32 => EnumerableSet.AddressSet) private contributorsByRound; /// @notice Maps round IDs to user addresses to contribution amounts by access criteria. - mapping(uint64 => mapping(address => mapping(uint8 => uint))) private + mapping(uint32 => mapping(address => mapping(uint8 => uint))) private roundIdTouserContributionsByAccessCriteria; - /// @notice The current round count. - uint32 private roundCount; - /// @notice Add a mapping to track the next unprocessed index for each round. - mapping(uint64 => uint) private roundIdToNextUnprocessedIndex; + mapping(uint32 => uint) private roundIdToNextUnprocessedIndex; /// @notice Storage gap for future upgrades. uint[50] private __gap; From d375cc38ce24bc39345be2f8e414475607eff60a Mon Sep 17 00:00:00 2001 From: Fabian Scherer Date: Tue, 29 Apr 2025 22:25:07 +0200 Subject: [PATCH 082/130] chore: formatting --- src/modules/logicModule/LM_PC_FundingPot_v1.sol | 2 +- src/modules/logicModule/interfaces/ILM_PC_FundingPot_v1.sol | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/modules/logicModule/LM_PC_FundingPot_v1.sol b/src/modules/logicModule/LM_PC_FundingPot_v1.sol index 8b5b68b22..b302000ce 100644 --- a/src/modules/logicModule/LM_PC_FundingPot_v1.sol +++ b/src/modules/logicModule/LM_PC_FundingPot_v1.sol @@ -150,7 +150,7 @@ contract LM_PC_FundingPot_v1 is /// @notice Storage gap for future upgrades. uint[50] private __gap; - + // ------------------------------------------------------------------------- // Modifiers diff --git a/src/modules/logicModule/interfaces/ILM_PC_FundingPot_v1.sol b/src/modules/logicModule/interfaces/ILM_PC_FundingPot_v1.sol index 0cc141d51..76ca4ad48 100644 --- a/src/modules/logicModule/interfaces/ILM_PC_FundingPot_v1.sol +++ b/src/modules/logicModule/interfaces/ILM_PC_FundingPot_v1.sol @@ -91,6 +91,7 @@ interface ILM_PC_FundingPot_v1 is IERC20PaymentClientBase_v2 { NFT, // 2 MERKLE, // 3 LIST // 4 + } // ------------------------------------------------------------------------- From d28a72f363a162b98102433202bd8e93d4aaaf24 Mon Sep 17 00:00:00 2001 From: Zuhaib Mohammed Date: Wed, 30 Apr 2025 09:39:53 +0530 Subject: [PATCH 083/130] fix: remove mock contract --- .../LM_PC_FundingPot_v1ERC20Mock.sol | 20 ------------------- .../logicModule/LM_PC_FundingPot_v1.t.sol | 2 -- 2 files changed, 22 deletions(-) delete mode 100644 test/mocks/modules/logicModule/LM_PC_FundingPot_v1ERC20Mock.sol diff --git a/test/mocks/modules/logicModule/LM_PC_FundingPot_v1ERC20Mock.sol b/test/mocks/modules/logicModule/LM_PC_FundingPot_v1ERC20Mock.sol deleted file mode 100644 index b1df65d8f..000000000 --- a/test/mocks/modules/logicModule/LM_PC_FundingPot_v1ERC20Mock.sol +++ /dev/null @@ -1,20 +0,0 @@ -// SPDX-License-Identifier: LGPL-3.0-only -pragma solidity ^0.8.0; - -import {ERC20} from "@oz/token/ERC20/ERC20.sol"; - -contract LM_PC_FundingPot_v1ERC20Mock is ERC20 { - constructor(string memory name, string memory symbol) ERC20(name, symbol) {} - - function buyFor(address to, uint value, uint minTokens) public { - _mint(to, value); - } - - function token() public view returns (address) { - return address(this); - } - - function getIssuanceToken() public view returns (address) { - return address(this); - } -} diff --git a/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol b/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol index c86e9d269..de36e2e53 100644 --- a/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol +++ b/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol @@ -20,8 +20,6 @@ import { } from "test/utils/mocks/modules/paymentClient/ERC20PaymentClientBaseV2Mock.sol"; import {ERC721Mock} from "test/utils/mocks/modules/logicModules/LM_PC_FundingPot_v2NFTMock.sol"; -import {LM_PC_FundingPot_v1ERC20Mock} from - "test/utils/mocks/modules/logicModules/LM_PC_FundingPot_v1ERC20Mock.sol"; import {IBondingCurveBase_v1} from "@fm/bondingCurve/interfaces/IBondingCurveBase_v1.sol"; From f5547f313a4eefbbd5db963301ef5eabc5f18a55 Mon Sep 17 00:00:00 2001 From: Zuhaib Mohammed Date: Wed, 30 Apr 2025 10:04:01 +0530 Subject: [PATCH 084/130] fix: remove file --- simulation.py | 150 -------------------------------------------------- 1 file changed, 150 deletions(-) delete mode 100644 simulation.py diff --git a/simulation.py b/simulation.py deleted file mode 100644 index 08365509b..000000000 --- a/simulation.py +++ /dev/null @@ -1,150 +0,0 @@ -import numpy as np -import matplotlib.pyplot as plt -from dataclasses import dataclass -from typing import List, Tuple -import math - -@dataclass -class Epoch: - fixed_price: float - issuance_threshold: float - current_issuance: float = 0 - is_price_discovery: bool = False - discovery_volume: float = 0 - discovery_price: float = 0 - -class UniswapV4HookSimulation: - def __init__( - self, - initial_price: float, - issuance_threshold: float, - discovery_volume_limit: float, - price_volatility_limit: float - ): - self.epochs: List[Epoch] = [] - self.current_epoch = Epoch( - fixed_price=initial_price, - issuance_threshold=issuance_threshold - ) - self.epochs.append(self.current_epoch) - - self.discovery_volume_limit = discovery_volume_limit - self.price_volatility_limit = price_volatility_limit - self.total_collateral = 0 - self.price_history = [] - self.issuance_history = [] - - def calculate_required_collateral(self) -> float: - """Calculate the required collateral based on current issuance and price""" - if self.current_epoch.is_price_discovery: - # In price discovery, use the discovered price or floor price - price = max( - self.current_epoch.discovery_price, - self.current_epoch.fixed_price * (1 - self.price_volatility_limit) - ) - else: - price = self.current_epoch.fixed_price - - return self.current_epoch.current_issuance * price - - def verify_backing_requirement(self) -> bool: - """Verify if current collateral meets backing requirements""" - required_collateral = self.calculate_required_collateral() - return self.total_collateral >= required_collateral - - def simulate_mint(self, amount: float) -> Tuple[bool, str]: - """Simulate minting tokens""" - if not self.verify_backing_requirement(): - return False, "Insufficient backing" - - if self.current_epoch.is_price_discovery: - if self.current_epoch.discovery_volume >= self.discovery_volume_limit: - return False, "Discovery volume limit reached" - - # Simulate price impact in discovery phase - price_impact = amount * 0.001 # 0.1% price impact - self.current_epoch.discovery_price = self.current_epoch.fixed_price * (1 + price_impact) - self.current_epoch.discovery_volume += amount - - # Check if price discovery should end - if self.current_epoch.discovery_volume >= self.discovery_volume_limit: - self._end_price_discovery() - - else: - self.current_epoch.current_issuance += amount - - # Check if we should start price discovery - if self.current_epoch.current_issuance >= self.current_epoch.issuance_threshold: - self._start_price_discovery() - - self.total_collateral += amount * self.current_epoch.fixed_price - self.price_history.append(self.current_epoch.fixed_price) - self.issuance_history.append(self.current_epoch.current_issuance) - return True, "Success" - - def _start_price_discovery(self): - """Start price discovery phase""" - self.current_epoch.is_price_discovery = True - self.current_epoch.discovery_price = self.current_epoch.fixed_price - self.current_epoch.discovery_volume = 0 - - def _end_price_discovery(self): - """End price discovery phase and start new epoch""" - # Create new epoch with discovered price - new_price = self.current_epoch.discovery_price - new_epoch = Epoch( - fixed_price=new_price, - issuance_threshold=self.current_epoch.issuance_threshold - ) - self.epochs.append(new_epoch) - self.current_epoch = new_epoch - -def run_simulation(): - # Initialize simulation - sim = UniswapV4HookSimulation( - initial_price=1.0, - issuance_threshold=1000, - discovery_volume_limit=500, - price_volatility_limit=0.1 # 10% max price movement - ) - - # Simulate a series of mints - mint_amounts = np.random.normal(100, 20, 50) # Random mint amounts - - for amount in mint_amounts: - success, message = sim.simulate_mint(amount) - if not success: - print(f"Mint failed: {message}") - break - - # Plot results - plt.figure(figsize=(12, 6)) - - # Plot price history - plt.subplot(1, 2, 1) - plt.plot(sim.price_history) - plt.title('Token Price Over Time') - plt.xlabel('Transaction') - plt.ylabel('Price') - - # Plot issuance history - plt.subplot(1, 2, 2) - plt.plot(sim.issuance_history) - plt.title('Total Issuance Over Time') - plt.xlabel('Transaction') - plt.ylabel('Issuance') - - plt.tight_layout() - plt.show() - - # Print final state - print("\nFinal State:") - print(f"Total Epochs: {len(sim.epochs)}") - print(f"Current Price: {sim.current_epoch.fixed_price}") - print(f"Current Issuance: {sim.current_epoch.current_issuance}") - print(f"Total Collateral: {sim.total_collateral}") - print(f"Required Collateral: {sim.calculate_required_collateral()}") - print(f"Backing Requirement Met: {sim.verify_backing_requirement()}") - -if __name__ == "__main__": - run_simulation() \ No newline at end of file From 5f42fbb9f2f5819a20f4d3b9538e2c31eb2ce5d6 Mon Sep 17 00:00:00 2001 From: JeffreyJoel Date: Wed, 30 Apr 2025 11:11:51 +0100 Subject: [PATCH 085/130] chore: remove unused variables --- src/modules/logicModule/LM_PC_FundingPot_v1.sol | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/modules/logicModule/LM_PC_FundingPot_v1.sol b/src/modules/logicModule/LM_PC_FundingPot_v1.sol index b302000ce..f8b825576 100644 --- a/src/modules/logicModule/LM_PC_FundingPot_v1.sol +++ b/src/modules/logicModule/LM_PC_FundingPot_v1.sol @@ -1224,8 +1224,6 @@ contract LM_PC_FundingPot_v1 is uint tokensAmount_, address issuanceToken_ ) internal { - Round storage round = rounds[roundId_]; - AccessCriteriaPrivileges storage privileges = roundItToAccessCriteriaIdToPrivileges[roundId_][accessCriteriaId_]; @@ -1265,11 +1263,6 @@ contract LM_PC_FundingPot_v1 is if (totalContributions == 0) { revert Module__LM_PC_FundingPot__NoContributions(); } - address issuanceToken = address( - IBondingCurveBase_v1( - address(__Module_orchestrator.fundingManager()) - ).getIssuanceToken() - ); // approve the fundingManager to spend the contribution token IERC20(__Module_orchestrator.fundingManager().token()).approve( address(__Module_orchestrator.fundingManager()), totalContributions From 584f9e0b56905df63ba7b0819d6517a386894e80 Mon Sep 17 00:00:00 2001 From: Zuhaib Mohammed Date: Sat, 26 Apr 2025 21:41:40 +0530 Subject: [PATCH 086/130] fix: e2e test debug --- .../logicModule/LM_PC_FundingPot_v1.sol | 7 +- test/e2e/E2EModuleRegistry.sol | 44 +++ test/e2e/logicModule/FundingPotE2E.t.sol | 307 ++++++++++++++++++ .../logicModule/LM_PC_FundingPot_v1.t.sol | 5 +- 4 files changed, 361 insertions(+), 2 deletions(-) create mode 100644 test/e2e/logicModule/FundingPotE2E.t.sol diff --git a/src/modules/logicModule/LM_PC_FundingPot_v1.sol b/src/modules/logicModule/LM_PC_FundingPot_v1.sol index f8b825576..99bf43139 100644 --- a/src/modules/logicModule/LM_PC_FundingPot_v1.sol +++ b/src/modules/logicModule/LM_PC_FundingPot_v1.sol @@ -684,7 +684,12 @@ contract LM_PC_FundingPot_v1 is // If autoClosure is false, only admin can process contributors if (!round.autoClosure) { - _checkRoleModifier(FUNDING_POT_ADMIN_ROLE, _msgSender()); + _checkRoleModifier( + __Module_orchestrator.authorizer().generateRoleId( + address(this), FUNDING_POT_ADMIN_ROLE + ), + _msgSender() + ); } uint startIndex = roundIdToNextUnprocessedIndex[roundId_]; diff --git a/test/e2e/E2EModuleRegistry.sol b/test/e2e/E2EModuleRegistry.sol index 85d7a62d1..58e8567fe 100644 --- a/test/e2e/E2EModuleRegistry.sol +++ b/test/e2e/E2EModuleRegistry.sol @@ -27,6 +27,7 @@ import {LM_PC_Bounties_v2} from "@lm/LM_PC_Bounties_v2.sol"; import {LM_PC_RecurringPayments_v2} from "@lm/LM_PC_RecurringPayments_v2.sol"; import {LM_PC_PaymentRouter_v2} from "@lm/LM_PC_PaymentRouter_v2.sol"; import {LM_PC_Staking_v2} from "@lm/LM_PC_Staking_v2.sol"; +import {LM_PC_FundingPot_v1} from "@lm/LM_PC_FundingPot_v1.sol"; import {LM_PC_KPIRewarder_v2} from "@lm/LM_PC_KPIRewarder_v2.sol"; import {AUT_Roles_v1} from "@aut/role/AUT_Roles_v1.sol"; import {AUT_TokenGated_Roles_v1} from "@aut/role/AUT_TokenGated_Roles_v1.sol"; @@ -808,6 +809,49 @@ contract E2EModuleRegistry is Test { ); } + // LM_PC_FundingPot_v1 + LM_PC_FundingPot_v1 LM_PC_FundingPot_v1Impl; + + InverterBeacon_v1 LM_PC_FundingPot_v1Beacon; + + IModule_v1.Metadata LM_PC_FundingPot_v1Metadata = IModule_v1.Metadata( + 1, + 0, + 0, + "https://github.com/InverterNetwork/contracts", + "LM_PC_FundingPot_v1" + ); + + /* + IOrchestratorFactory_v1.ModuleConfig LM_PC_FundingPot_v1FactoryConfig = + IOrchestratorFactory_v1.ModuleConfig( + LM_PC_FundingPot_v1Metadata, + bytes(address(contributionToken)) + ); + */ + + function setUpLM_PC_FundingPot_v1() internal { + // Deploy module implementations. + LM_PC_FundingPot_v1Impl = new LM_PC_FundingPot_v1(); + + // Deploy module beacons. + LM_PC_FundingPot_v1Beacon = new InverterBeacon_v1( + moduleFactory.reverter(), + DEFAULT_BEACON_OWNER, + LM_PC_FundingPot_v1Metadata.majorVersion, + address(LM_PC_FundingPot_v1Impl), + LM_PC_FundingPot_v1Metadata.minorVersion, + LM_PC_FundingPot_v1Metadata.patchVersion + ); + + // Register modules at moduleFactory. + vm.prank(teamMultisig); + gov.registerMetadataInModuleFactory( + LM_PC_FundingPot_v1Metadata, + IInverterBeacon_v1(LM_PC_FundingPot_v1Beacon) + ); + } + // LM_PC_KPIRewarder_v2 LM_PC_KPIRewarder_v2 LM_PC_KPIRewarder_v2Impl; diff --git a/test/e2e/logicModule/FundingPotE2E.t.sol b/test/e2e/logicModule/FundingPotE2E.t.sol new file mode 100644 index 000000000..d3f40c943 --- /dev/null +++ b/test/e2e/logicModule/FundingPotE2E.t.sol @@ -0,0 +1,307 @@ +// SPDX-License-Identifier: LGPL-3.0-only +pragma solidity ^0.8.0; + +// Internal Dependencies +import { + E2ETest, + IOrchestratorFactory_v1, + IOrchestrator_v1 +} from "test/e2e/E2ETest.sol"; + +// SuT +import { + LM_PC_FundingPot_v1, + ILM_PC_FundingPot_v1 +} from "@lm/LM_PC_FundingPot_v1.sol"; +import {IERC20PaymentClientBase_v2} from + "test/utils/mocks/modules/paymentClient/ERC20PaymentClientBaseV2Mock.sol"; +import { + FM_BC_Bancor_Redeeming_VirtualSupply_v1, + IFM_BC_Bancor_Redeeming_VirtualSupply_v1 +} from + "test/modules/fundingManager/bondingCurve/FM_BC_Bancor_Redeeming_VirtualSupply_v1.t.sol"; +import {PP_Streaming_v2} from "src/modules/paymentProcessor/PP_Streaming_v2.sol"; +import { + LM_PC_Bounties_v2, ILM_PC_Bounties_v2 +} from "@lm/LM_PC_Bounties_v2.sol"; +import {IBondingCurveBase_v1} from + "@fm/bondingCurve/interfaces/IBondingCurveBase_v1.sol"; + +import {ERC165Upgradeable} from + "@oz-up/utils/introspection/ERC165Upgradeable.sol"; +import {ERC20Mock} from "test/utils/mocks/ERC20Mock.sol"; +import {SafeERC20} from "@oz/token/ERC20/utils/SafeERC20.sol"; +import {ERC20Issuance_v1} from "@ex/token/ERC20Issuance_v1.sol"; +import {console2} from "forge-std/console2.sol"; + +contract FundingPotE2E is E2ETest { + // Module Configurations for the current E2E test. Should be filled during setUp() call. + IOrchestratorFactory_v1.ModuleConfig[] moduleConfigurations; + + // Let's create a list of contributors + address contributor1 = makeAddr("contributor 1"); + address contributor2 = makeAddr("contributor 2"); + address contributor3 = makeAddr("contributor 3"); + ERC20Issuance_v1 issuanceToken; + LM_PC_Bounties_v2 bountyManager; + IOrchestrator_v1 orchestrator; + IFM_BC_Bancor_Redeeming_VirtualSupply_v1 bondingCurveFundingManager; + PP_Streaming_v2 paymentProcessor; + LM_PC_FundingPot_v1 fundingPot; + + // Constants + uint constant _SENTINEL = type(uint).max; + //ERC20Mock contributionToken = new ERC20Mock("Contribution Mock", "C_MOCK"); + ERC20Mock contributionToken; + + function setUp() public override { + // vm.label({ + // account: address(contributionToken), + // newLabel: ERC20Mock(address(contributionToken)).symbol() + // }); + // Setup common E2E framework + super.setUp(); + + // Set Up individual Modules the E2E test is going to use and store their configurations: + // NOTE: It's important to store the module configurations in order, since _create_E2E_Orchestrator() will copy from the array. + // The order should be: + // moduleConfigurations[0] => FundingManager + // moduleConfigurations[1] => Authorizer + // moduleConfigurations[2] => PaymentProcessor + // moduleConfigurations[3:] => Additional Logic Modules + + issuanceToken = new ERC20Issuance_v1( + "Bonding Curve Token", "BCT", 18, type(uint).max - 1, address(this) + ); + + IFM_BC_Bancor_Redeeming_VirtualSupply_v1.BondingCurveProperties memory + bc_properties = IFM_BC_Bancor_Redeeming_VirtualSupply_v1 + .BondingCurveProperties({ + formula: address(formula), + reserveRatioForBuying: 333_333, + reserveRatioForSelling: 333_333, + buyFee: 0, + sellFee: 0, + buyIsOpen: true, + sellIsOpen: true, + initialIssuanceSupply: 10, + initialCollateralSupply: 30 + }); + + // FundingManager + moduleConfigurations.push( + IOrchestratorFactory_v1.ModuleConfig( + bancorVirtualSupplyBondingCurveFundingManagerMetadata, + abi.encode(address(issuanceToken), bc_properties, token) + ) + ); + + // Authorizer + setUpRoleAuthorizer(); + moduleConfigurations.push( + IOrchestratorFactory_v1.ModuleConfig( + roleAuthorizerMetadata, abi.encode(address(this)) + ) + ); + + // PaymentProcessor + setUpStreamingPaymentProcessor(); + moduleConfigurations.push( + IOrchestratorFactory_v1.ModuleConfig( + streamingPaymentProcessorMetadata, + abi.encode(10, 0, 30) // defaultStart, defaultCliff, defaultEnd + ) + ); + + // Additional Logic Modules + setUpLM_PC_FundingPot_v1(); + moduleConfigurations.push( + IOrchestratorFactory_v1.ModuleConfig( + LM_PC_FundingPot_v1Metadata, abi.encode(contributionToken) + ) + ); + setUpBancorVirtualSupplyBondingCurveFundingManager(); + + // BancorFormula 'formula' is instantiated in the E2EModuleRegistry + } + + function init() private { + //-------------------------------------------------------------------------- + // Orchestrator_v1 Initialization + //-------------------------------------------------------------------------- + IOrchestratorFactory_v1.WorkflowConfig memory workflowConfig = + IOrchestratorFactory_v1.WorkflowConfig({ + independentUpdates: false, + independentUpdateAdmin: address(0) + }); + + orchestrator = + _create_E2E_Orchestrator(workflowConfig, moduleConfigurations); + + contributionToken = + ERC20Mock(address(orchestrator.fundingManager().token())); + // Get the Bancor bonding curve funding manager + bondingCurveFundingManager = IFM_BC_Bancor_Redeeming_VirtualSupply_v1( + address(orchestrator.fundingManager()) + ); + + // Get the streaming payment processor + paymentProcessor = + PP_Streaming_v2(address(orchestrator.paymentProcessor())); + + // Get the funding pot + address[] memory modulesList = orchestrator.listModules(); + for (uint i; i < modulesList.length; ++i) { + if ( + ERC165Upgradeable(modulesList[i]).supportsInterface( + type(ILM_PC_FundingPot_v1).interfaceId + ) + ) { + fundingPot = LM_PC_FundingPot_v1(modulesList[i]); + break; + } + } + + // Set up the bonding curve + issuanceToken.setMinter(address(bondingCurveFundingManager), true); + } + + function test_e2e_FundingPotLifecycle() public { + init(); + + // 2. Grant FUNDING_POT_ADMIN_ROLE to this contract for configuring rounds + fundingPot.grantModuleRole( + fundingPot.FUNDING_POT_ADMIN_ROLE(), address(this) + ); + + // Configure rounds + // Round 1 + uint64 round1Id = fundingPot.createRound( + block.timestamp + 1 days, // start + block.timestamp + 30 days, // end + 1000e18, // cap + address(0), // no hook + bytes(""), // no hook function + false, // auto closure + false // no global caps + ); + + // Round 2 + uint64 round2Id = fundingPot.createRound( + block.timestamp + 1, // start + block.timestamp + 60 days, // end + 750e18, // cap + address(0), // no hook + bytes(""), // no hook function + true, // auto closure + false // no global caps + ); + + // Add access criteria to round 1 + address[] memory allowedAddresses = new address[](2); + allowedAddresses[0] = contributor1; + allowedAddresses[1] = contributor2; + + fundingPot.setAccessCriteriaForRound( + round1Id, + uint8(ILM_PC_FundingPot_v1.AccessCriteriaType.LIST), + address(0), + bytes32(0), + allowedAddresses + ); + + // Add access criteria to round 2 + allowedAddresses = new address[](1); + allowedAddresses[0] = contributor3; + + fundingPot.setAccessCriteriaForRound( + round2Id, + uint8(ILM_PC_FundingPot_v1.AccessCriteriaType.LIST), + address(0), + bytes32(0), + allowedAddresses + ); + + fundingPot.setAccessCriteriaPrivileges( + round1Id, + 0, // accessCriteriaId + 500e18, // personalCap + true, // overrideContributionSpan + block.timestamp, // start + 0, // cliff + block.timestamp + 60 days // end + ); + + fundingPot.setAccessCriteriaPrivileges( + round2Id, + 0, // accessCriteriaId + 750e18, // personalCap + true, // overrideContributionSpan + block.timestamp, // start + 0, // cliff + block.timestamp + 60 days // end + ); + + vm.warp(block.timestamp + 1 days); + + // Fund contributors + contributionToken.mint(contributor1, 500e18); + contributionToken.mint(contributor2, 500e18); + contributionToken.mint(contributor3, 1000e18); + + vm.startPrank(contributor1); + contributionToken.approve(address(fundingPot), 500e18); + fundingPot.contributeToRound(round1Id, 500e18, 0, new bytes32[](0)); + vm.stopPrank(); + + vm.startPrank(contributor2); + contributionToken.approve(address(fundingPot), 500e18); + fundingPot.contributeToRound(round1Id, 500e18, 0, new bytes32[](0)); + vm.stopPrank(); + + vm.startPrank(contributor3); + contributionToken.approve(address(fundingPot), 750e18); + fundingPot.contributeToRound(round2Id, 750e18, 0, new bytes32[](0)); + vm.stopPrank(); + + // Fast forward to after rounds end + vm.warp(block.timestamp + 32 days); + + fundingPot.closeRound(round1Id); + assertEq(fundingPot.isRoundClosed(round1Id), true); + assertEq(fundingPot.isRoundClosed(round2Id), true); + assertEq(contributionToken.balanceOf(address(fundingPot)), 0); + assertGt(issuanceToken.balanceOf(address(fundingPot)), 0); + + fundingPot.createPaymentOrdersForContributorsBatch(round1Id, 2); + fundingPot.createPaymentOrdersForContributorsBatch(round2Id, 1); + + IERC20PaymentClientBase_v2.PaymentOrder[] memory orders = + fundingPot.paymentOrders(); + + console2.log("orders: ", orders.length); + + vm.prank(address(fundingPot)); + paymentProcessor.processPayments( + IERC20PaymentClientBase_v2(address(fundingPot)) + ); + //console2.log("orders: ", orders.length); + // @note: Lee I can view the orders created here! + + // /// Assert a payment order was created + // PP_Streaming_v2.Stream[] memory streams = paymentProcessor + // .viewAllPaymentOrders(address(fundingPot), contributor1); + // assertEq(streams.length, 1); + + // uint totalContributions = 1500e18; // 300 + 200 + 1000 + // // Verify tokens were minted from curve + + address issueToken = ( + IBondingCurveBase_v1(address(orchestrator.fundingManager())) + .getIssuanceToken() + ); + console2.log("Contributor 1: ", issuanceToken.balanceOf(contributor1)); + console2.log("Contributor 2: ", issuanceToken.balanceOf(contributor2)); + console2.log("Contributor 3: ", issuanceToken.balanceOf(contributor3)); + } +} diff --git a/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol b/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol index de36e2e53..1d3032bae 100644 --- a/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol +++ b/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol @@ -2312,10 +2312,13 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { uint32 roundId = fundingPot.getRoundCount(); vm.startPrank(contributor1_); + bytes32 roleId = _authorizer.generateRoleId( + address(fundingPot), fundingPot.FUNDING_POT_ADMIN_ROLE() + ); vm.expectRevert( abi.encodeWithSelector( IModule_v1.Module__CallerNotAuthorized.selector, - fundingPot.FUNDING_POT_ADMIN_ROLE(), + roleId, contributor1_ ) ); From 9d6f1e394250f3c76ec6789ab3af06c56bae8530 Mon Sep 17 00:00:00 2001 From: Zuhaib Mohammed Date: Mon, 28 Apr 2025 14:10:54 +0530 Subject: [PATCH 087/130] test: Funding Pot E2E test --- test/e2e/logicModule/FundingPotE2E.t.sol | 55 +++++++++++------------- 1 file changed, 26 insertions(+), 29 deletions(-) diff --git a/test/e2e/logicModule/FundingPotE2E.t.sol b/test/e2e/logicModule/FundingPotE2E.t.sol index d3f40c943..de86f3adc 100644 --- a/test/e2e/logicModule/FundingPotE2E.t.sol +++ b/test/e2e/logicModule/FundingPotE2E.t.sol @@ -174,7 +174,7 @@ contract FundingPotE2E is E2ETest { fundingPot.FUNDING_POT_ADMIN_ROLE(), address(this) ); - // Configure rounds + // 3. Configure rounds // Round 1 uint64 round1Id = fundingPot.createRound( block.timestamp + 1 days, // start @@ -197,6 +197,7 @@ contract FundingPotE2E is E2ETest { false // no global caps ); + // 4. Set access criteria for the rounds // Add access criteria to round 1 address[] memory allowedAddresses = new address[](2); allowedAddresses[0] = contributor1; @@ -222,6 +223,7 @@ contract FundingPotE2E is E2ETest { allowedAddresses ); + // 5. Set access criteria privileges for the rounds fundingPot.setAccessCriteriaPrivileges( round1Id, 0, // accessCriteriaId @@ -244,19 +246,19 @@ contract FundingPotE2E is E2ETest { vm.warp(block.timestamp + 1 days); - // Fund contributors + // 6. Fund contributors and contribute to rounds contributionToken.mint(contributor1, 500e18); contributionToken.mint(contributor2, 500e18); contributionToken.mint(contributor3, 1000e18); vm.startPrank(contributor1); - contributionToken.approve(address(fundingPot), 500e18); - fundingPot.contributeToRound(round1Id, 500e18, 0, new bytes32[](0)); + contributionToken.approve(address(fundingPot), 400e18); + fundingPot.contributeToRound(round1Id, 400e18, 0, new bytes32[](0)); vm.stopPrank(); vm.startPrank(contributor2); - contributionToken.approve(address(fundingPot), 500e18); - fundingPot.contributeToRound(round1Id, 500e18, 0, new bytes32[](0)); + contributionToken.approve(address(fundingPot), 600e18); + fundingPot.contributeToRound(round1Id, 600e18, 0, new bytes32[](0)); vm.stopPrank(); vm.startPrank(contributor3); @@ -264,44 +266,39 @@ contract FundingPotE2E is E2ETest { fundingPot.contributeToRound(round2Id, 750e18, 0, new bytes32[](0)); vm.stopPrank(); - // Fast forward to after rounds end - vm.warp(block.timestamp + 32 days); + // 7. Fast forward to after rounds end + vm.warp(block.timestamp + 50 days); + // 8. Close rounds fundingPot.closeRound(round1Id); assertEq(fundingPot.isRoundClosed(round1Id), true); - assertEq(fundingPot.isRoundClosed(round2Id), true); + assertEq(fundingPot.isRoundClosed(round2Id), true); // round2 is auto closed assertEq(contributionToken.balanceOf(address(fundingPot)), 0); assertGt(issuanceToken.balanceOf(address(fundingPot)), 0); + // 9. Create payment orders for contributors fundingPot.createPaymentOrdersForContributorsBatch(round1Id, 2); fundingPot.createPaymentOrdersForContributorsBatch(round2Id, 1); - IERC20PaymentClientBase_v2.PaymentOrder[] memory orders = - fundingPot.paymentOrders(); - - console2.log("orders: ", orders.length); - + // 10. Process payments vm.prank(address(fundingPot)); paymentProcessor.processPayments( IERC20PaymentClientBase_v2(address(fundingPot)) ); - //console2.log("orders: ", orders.length); - // @note: Lee I can view the orders created here! - // /// Assert a payment order was created - // PP_Streaming_v2.Stream[] memory streams = paymentProcessor - // .viewAllPaymentOrders(address(fundingPot), contributor1); - // assertEq(streams.length, 1); + // 11. Claim payments + vm.prank(contributor1); + paymentProcessor.claimAll(address(fundingPot)); - // uint totalContributions = 1500e18; // 300 + 200 + 1000 - // // Verify tokens were minted from curve + vm.prank(contributor2); + paymentProcessor.claimAll(address(fundingPot)); - address issueToken = ( - IBondingCurveBase_v1(address(orchestrator.fundingManager())) - .getIssuanceToken() - ); - console2.log("Contributor 1: ", issuanceToken.balanceOf(contributor1)); - console2.log("Contributor 2: ", issuanceToken.balanceOf(contributor2)); - console2.log("Contributor 3: ", issuanceToken.balanceOf(contributor3)); + vm.prank(contributor3); + paymentProcessor.claimAll(address(fundingPot)); + + // 12. Assert that contributors received their tokens + assertGt(issuanceToken.balanceOf(contributor1), 0); + assertGt(issuanceToken.balanceOf(contributor2), 0); + assertGt(issuanceToken.balanceOf(contributor3), 0); } } From 19007daaf2b1cb0d7994b021bfc284d48df4a17c Mon Sep 17 00:00:00 2001 From: Zuhaib Mohammed Date: Mon, 28 Apr 2025 18:14:50 +0530 Subject: [PATCH 088/130] fix: minor bug fixes --- src/modules/logicModule/LM_PC_FundingPot_v1.sol | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/modules/logicModule/LM_PC_FundingPot_v1.sol b/src/modules/logicModule/LM_PC_FundingPot_v1.sol index 99bf43139..3e9eeab25 100644 --- a/src/modules/logicModule/LM_PC_FundingPot_v1.sol +++ b/src/modules/logicModule/LM_PC_FundingPot_v1.sol @@ -123,7 +123,7 @@ contract LM_PC_FundingPot_v1 is mapping( uint32 roundId => mapping(uint8 accessCriteriaId_ => AccessCriteriaPrivileges) - ) private roundItToAccessCriteriaIdToPrivileges; + ) private roundIdToAccessCriteriaIdToPrivileges; /// @notice Maps round IDs to user addresses to contribution amounts. mapping(uint32 => mapping(address => uint)) private @@ -272,7 +272,7 @@ contract LM_PC_FundingPot_v1 is // Store the privileges in a local variable to reduce stack usage. AccessCriteriaPrivileges storage privileges = - roundItToAccessCriteriaIdToPrivileges[roundId_][accessCriteriaId__]; + roundIdToAccessCriteriaIdToPrivileges[roundId_][accessCriteriaId__]; return ( privileges.personalCap, @@ -327,7 +327,7 @@ contract LM_PC_FundingPot_v1 is if (isEligible) { AccessCriteriaPrivileges storage privileges = - roundItToAccessCriteriaIdToPrivileges[roundId_][accessCriteriaId_]; + roundIdToAccessCriteriaIdToPrivileges[roundId_][accessCriteriaId_]; uint userPersonalCap = privileges.personalCap; uint userContribution = _getUserContributionToRound(roundId_, user_); @@ -543,7 +543,7 @@ contract LM_PC_FundingPot_v1 is } AccessCriteriaPrivileges storage accessCriteriaPrivileges = - roundItToAccessCriteriaIdToPrivileges[roundId_][accessCriteriaId_]; + roundIdToAccessCriteriaIdToPrivileges[roundId_][accessCriteriaId_]; accessCriteriaPrivileges.personalCap = personalCap_; accessCriteriaPrivileges.overrideContributionSpan = @@ -606,7 +606,7 @@ contract LM_PC_FundingPot_v1 is if (isEligible) { AccessCriteriaPrivileges storage privileges = - roundItToAccessCriteriaIdToPrivileges[roundCap.roundId][roundCap + roundIdToAccessCriteriaIdToPrivileges[roundCap.roundId][roundCap .accessCriteriaId]; uint userContribution = @@ -807,7 +807,7 @@ contract LM_PC_FundingPot_v1 is ); AccessCriteriaPrivileges storage privileges = - roundItToAccessCriteriaIdToPrivileges[roundId_][accessCriteriaId_]; + roundIdToAccessCriteriaIdToPrivileges[roundId_][accessCriteriaId_]; bool canOverrideContributionSpan = privileges.overrideContributionSpan; if ( @@ -934,7 +934,7 @@ contract LM_PC_FundingPot_v1 is // Get the base personal cap for this round and criteria AccessCriteriaPrivileges storage privileges = - roundItToAccessCriteriaIdToPrivileges[roundId_][accessCriteriaId__]; + roundIdToAccessCriteriaIdToPrivileges[roundId_][accessCriteriaId__]; uint userPersonalCap = privileges.personalCap; // Add unspent capacity if global accumulative caps are enabled @@ -1230,7 +1230,7 @@ contract LM_PC_FundingPot_v1 is address issuanceToken_ ) internal { AccessCriteriaPrivileges storage privileges = - roundItToAccessCriteriaIdToPrivileges[roundId_][accessCriteriaId_]; + roundIdToAccessCriteriaIdToPrivileges[roundId_][accessCriteriaId_]; uint start = privileges.start; uint cliff = privileges.cliff; From e2bd75cfe637b6f60fc354506c7c531f291bea30 Mon Sep 17 00:00:00 2001 From: Zuhaib Mohammed Date: Tue, 29 Apr 2025 10:48:01 +0530 Subject: [PATCH 089/130] chore: rebase --- test/e2e/logicModule/FundingPotE2E.t.sol | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/e2e/logicModule/FundingPotE2E.t.sol b/test/e2e/logicModule/FundingPotE2E.t.sol index de86f3adc..a50af32de 100644 --- a/test/e2e/logicModule/FundingPotE2E.t.sol +++ b/test/e2e/logicModule/FundingPotE2E.t.sol @@ -176,7 +176,7 @@ contract FundingPotE2E is E2ETest { // 3. Configure rounds // Round 1 - uint64 round1Id = fundingPot.createRound( + uint32 round1Id = fundingPot.createRound( block.timestamp + 1 days, // start block.timestamp + 30 days, // end 1000e18, // cap @@ -187,7 +187,7 @@ contract FundingPotE2E is E2ETest { ); // Round 2 - uint64 round2Id = fundingPot.createRound( + uint32 round2Id = fundingPot.createRound( block.timestamp + 1, // start block.timestamp + 60 days, // end 750e18, // cap From 57515014dd5b142ba41484d76a411ecdff988c16 Mon Sep 17 00:00:00 2001 From: JeffreyJoel Date: Wed, 9 Apr 2025 20:20:53 +0100 Subject: [PATCH 090/130] test: add e2e test --- test/e2e/logicModule/FundingPotE2E.t.sol | 1 + 1 file changed, 1 insertion(+) diff --git a/test/e2e/logicModule/FundingPotE2E.t.sol b/test/e2e/logicModule/FundingPotE2E.t.sol index a50af32de..c32973d74 100644 --- a/test/e2e/logicModule/FundingPotE2E.t.sol +++ b/test/e2e/logicModule/FundingPotE2E.t.sol @@ -27,6 +27,7 @@ import { import {IBondingCurveBase_v1} from "@fm/bondingCurve/interfaces/IBondingCurveBase_v1.sol"; +import {FM_DepositVault_v1} from "@fm/depositVault/FM_DepositVault_v1.sol"; import {ERC165Upgradeable} from "@oz-up/utils/introspection/ERC165Upgradeable.sol"; import {ERC20Mock} from "test/utils/mocks/ERC20Mock.sol"; From cf62849e487b705d0a2056ee3a17a304a1070855 Mon Sep 17 00:00:00 2001 From: Leandro Faria Date: Wed, 9 Apr 2025 18:21:18 -0500 Subject: [PATCH 091/130] fix: add lifecycle test and fix set up --- test/e2e/logicModule/FundingPotE2E.t.sol | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/test/e2e/logicModule/FundingPotE2E.t.sol b/test/e2e/logicModule/FundingPotE2E.t.sol index c32973d74..76b4c73bc 100644 --- a/test/e2e/logicModule/FundingPotE2E.t.sol +++ b/test/e2e/logicModule/FundingPotE2E.t.sol @@ -33,7 +33,6 @@ import {ERC165Upgradeable} from import {ERC20Mock} from "test/utils/mocks/ERC20Mock.sol"; import {SafeERC20} from "@oz/token/ERC20/utils/SafeERC20.sol"; import {ERC20Issuance_v1} from "@ex/token/ERC20Issuance_v1.sol"; -import {console2} from "forge-std/console2.sol"; contract FundingPotE2E is E2ETest { // Module Configurations for the current E2E test. Should be filled during setUp() call. @@ -126,7 +125,7 @@ contract FundingPotE2E is E2ETest { // BancorFormula 'formula' is instantiated in the E2EModuleRegistry } - function init() private { + function test_e2e_FundingPotLifecycle() public { //-------------------------------------------------------------------------- // Orchestrator_v1 Initialization //-------------------------------------------------------------------------- @@ -150,7 +149,11 @@ contract FundingPotE2E is E2ETest { paymentProcessor = PP_Streaming_v2(address(orchestrator.paymentProcessor())); - // Get the funding pot + // Define payment processor + PP_Streaming_v2 paymentProcessor = + PP_Streaming_v2(address(orchestrator.paymentProcessor())); + + // Get modules list address[] memory modulesList = orchestrator.listModules(); for (uint i; i < modulesList.length; ++i) { if ( @@ -163,12 +166,12 @@ contract FundingPotE2E is E2ETest { } } - // Set up the bonding curve - issuanceToken.setMinter(address(bondingCurveFundingManager), true); - } + FM_BC_Bancor_Redeeming_VirtualSupply_v1 fundingManager = + FM_BC_Bancor_Redeeming_VirtualSupply_v1( + address(orchestrator.fundingManager()) + ); - function test_e2e_FundingPotLifecycle() public { - init(); + issuanceToken.setMinter(address(fundingManager), true); // 2. Grant FUNDING_POT_ADMIN_ROLE to this contract for configuring rounds fundingPot.grantModuleRole( From bcfc6b620fecba9302dc50b4424359237f93721c Mon Sep 17 00:00:00 2001 From: Leandro Faria Date: Mon, 14 Apr 2025 09:44:38 -0500 Subject: [PATCH 092/130] fix: add notes to e2e --- test/e2e/logicModule/FundingPotE2E.t.sol | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/test/e2e/logicModule/FundingPotE2E.t.sol b/test/e2e/logicModule/FundingPotE2E.t.sol index 76b4c73bc..2282d8f2f 100644 --- a/test/e2e/logicModule/FundingPotE2E.t.sol +++ b/test/e2e/logicModule/FundingPotE2E.t.sol @@ -24,8 +24,6 @@ import {PP_Streaming_v2} from "src/modules/paymentProcessor/PP_Streaming_v2.sol" import { LM_PC_Bounties_v2, ILM_PC_Bounties_v2 } from "@lm/LM_PC_Bounties_v2.sol"; -import {IBondingCurveBase_v1} from - "@fm/bondingCurve/interfaces/IBondingCurveBase_v1.sol"; import {FM_DepositVault_v1} from "@fm/depositVault/FM_DepositVault_v1.sol"; import {ERC165Upgradeable} from @@ -125,7 +123,7 @@ contract FundingPotE2E is E2ETest { // BancorFormula 'formula' is instantiated in the E2EModuleRegistry } - function test_e2e_FundingPotLifecycle() public { + function init() private { //-------------------------------------------------------------------------- // Orchestrator_v1 Initialization //-------------------------------------------------------------------------- @@ -153,7 +151,7 @@ contract FundingPotE2E is E2ETest { PP_Streaming_v2 paymentProcessor = PP_Streaming_v2(address(orchestrator.paymentProcessor())); - // Get modules list + // Get the funding pot address[] memory modulesList = orchestrator.listModules(); for (uint i; i < modulesList.length; ++i) { if ( @@ -166,12 +164,12 @@ contract FundingPotE2E is E2ETest { } } - FM_BC_Bancor_Redeeming_VirtualSupply_v1 fundingManager = - FM_BC_Bancor_Redeeming_VirtualSupply_v1( - address(orchestrator.fundingManager()) - ); + // Set up the bonding curve + issuanceToken.setMinter(address(bondingCurveFundingManager), true); + } - issuanceToken.setMinter(address(fundingManager), true); + function test_e2e_FundingPotLifecycle() public { + init(); // 2. Grant FUNDING_POT_ADMIN_ROLE to this contract for configuring rounds fundingPot.grantModuleRole( From 9b62a156e1936cee47bcf55616276bc278f3e543 Mon Sep 17 00:00:00 2001 From: Zuhaib Mohammed Date: Tue, 22 Apr 2025 16:14:34 +0530 Subject: [PATCH 093/130] fix: compile issues --- test/e2e/logicModule/FundingPotE2E.t.sol | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/test/e2e/logicModule/FundingPotE2E.t.sol b/test/e2e/logicModule/FundingPotE2E.t.sol index 2282d8f2f..ba7110b78 100644 --- a/test/e2e/logicModule/FundingPotE2E.t.sol +++ b/test/e2e/logicModule/FundingPotE2E.t.sol @@ -211,6 +211,20 @@ contract FundingPotE2E is E2ETest { address(0), bytes32(0), allowedAddresses +<<<<<<< HEAD +======= + ); + + // Round 2 + uint64 round2Id = fundingPot.createRound( + block.timestamp + 1, // start + block.timestamp + 60 days, // end + 2e18, // cap + address(0), // no hook + bytes(""), // no hook function + false, // auto closure + false // no global caps +>>>>>>> 85ef0139 (fix: compile issues) ); // Add access criteria to round 2 @@ -229,7 +243,11 @@ contract FundingPotE2E is E2ETest { fundingPot.setAccessCriteriaPrivileges( round1Id, 0, // accessCriteriaId +<<<<<<< HEAD 500e18, // personalCap +======= + 1_000_000_000_000_000_000, // personalCap +>>>>>>> 85ef0139 (fix: compile issues) true, // overrideContributionSpan block.timestamp, // start 0, // cliff @@ -239,7 +257,11 @@ contract FundingPotE2E is E2ETest { fundingPot.setAccessCriteriaPrivileges( round2Id, 0, // accessCriteriaId +<<<<<<< HEAD 750e18, // personalCap +======= + 1_000_000_000_000_000_000, // personalCap +>>>>>>> 85ef0139 (fix: compile issues) true, // overrideContributionSpan block.timestamp, // start 0, // cliff From 27466cf98816e8655cf67ffd5f221f8d76f2d546 Mon Sep 17 00:00:00 2001 From: Zuhaib Mohammed Date: Tue, 22 Apr 2025 23:51:33 +0530 Subject: [PATCH 094/130] chore: e2e test closeround --- test/e2e/logicModule/FundingPotE2E.t.sol | 24 ++---------------------- 1 file changed, 2 insertions(+), 22 deletions(-) diff --git a/test/e2e/logicModule/FundingPotE2E.t.sol b/test/e2e/logicModule/FundingPotE2E.t.sol index ba7110b78..115927724 100644 --- a/test/e2e/logicModule/FundingPotE2E.t.sol +++ b/test/e2e/logicModule/FundingPotE2E.t.sol @@ -32,6 +32,8 @@ import {ERC20Mock} from "test/utils/mocks/ERC20Mock.sol"; import {SafeERC20} from "@oz/token/ERC20/utils/SafeERC20.sol"; import {ERC20Issuance_v1} from "@ex/token/ERC20Issuance_v1.sol"; +import {console2} from "forge-std/console2.sol"; + contract FundingPotE2E is E2ETest { // Module Configurations for the current E2E test. Should be filled during setUp() call. IOrchestratorFactory_v1.ModuleConfig[] moduleConfigurations; @@ -211,20 +213,6 @@ contract FundingPotE2E is E2ETest { address(0), bytes32(0), allowedAddresses -<<<<<<< HEAD -======= - ); - - // Round 2 - uint64 round2Id = fundingPot.createRound( - block.timestamp + 1, // start - block.timestamp + 60 days, // end - 2e18, // cap - address(0), // no hook - bytes(""), // no hook function - false, // auto closure - false // no global caps ->>>>>>> 85ef0139 (fix: compile issues) ); // Add access criteria to round 2 @@ -243,11 +231,7 @@ contract FundingPotE2E is E2ETest { fundingPot.setAccessCriteriaPrivileges( round1Id, 0, // accessCriteriaId -<<<<<<< HEAD 500e18, // personalCap -======= - 1_000_000_000_000_000_000, // personalCap ->>>>>>> 85ef0139 (fix: compile issues) true, // overrideContributionSpan block.timestamp, // start 0, // cliff @@ -257,11 +241,7 @@ contract FundingPotE2E is E2ETest { fundingPot.setAccessCriteriaPrivileges( round2Id, 0, // accessCriteriaId -<<<<<<< HEAD 750e18, // personalCap -======= - 1_000_000_000_000_000_000, // personalCap ->>>>>>> 85ef0139 (fix: compile issues) true, // overrideContributionSpan block.timestamp, // start 0, // cliff From 367e68507da0812983a2da3d8f34f9c68c313a3b Mon Sep 17 00:00:00 2001 From: JeffreyJoel Date: Mon, 21 Apr 2025 06:01:08 +0100 Subject: [PATCH 095/130] test: improve test coverage --- .../logicModule/LM_PC_FundingPot_v1.sol | 4 + .../logicModule/LM_PC_FundingPot_v1Mock.sol | 55 ++ .../LM_PC_FundingPot_v2NFTMock.sol | 15 + .../logicModule/LM_PC_FundingPot_v1.t.sol | 807 +++++++++++++++--- .../LM_PC_FundingPot_v1_Exposed.sol | 11 +- 5 files changed, 782 insertions(+), 110 deletions(-) create mode 100644 test/mocks/modules/logicModule/LM_PC_FundingPot_v1Mock.sol diff --git a/src/modules/logicModule/LM_PC_FundingPot_v1.sol b/src/modules/logicModule/LM_PC_FundingPot_v1.sol index 3e9eeab25..d1e2b1024 100644 --- a/src/modules/logicModule/LM_PC_FundingPot_v1.sol +++ b/src/modules/logicModule/LM_PC_FundingPot_v1.sol @@ -438,6 +438,10 @@ contract LM_PC_FundingPot_v1 is ) external onlyModuleRole(FUNDING_POT_ADMIN_ROLE) { Round storage round = rounds[roundId_]; + if (accessCriteriaId_ > MAX_ACCESS_CRITERIA_ID) { + revert Module__LM_PC_FundingPot__InvalidAccessCriteriaId(); + } + _validateEditRoundParameters(round); if ( diff --git a/test/mocks/modules/logicModule/LM_PC_FundingPot_v1Mock.sol b/test/mocks/modules/logicModule/LM_PC_FundingPot_v1Mock.sol new file mode 100644 index 000000000..3d7ac2042 --- /dev/null +++ b/test/mocks/modules/logicModule/LM_PC_FundingPot_v1Mock.sol @@ -0,0 +1,55 @@ +// SPDX-License-Identifier: LGPL-3.0-only +pragma solidity 0.8.23; + +// External Dependencies +import "@oz/token/ERC721/ERC721.sol"; +import "@oz/token/ERC721/extensions/ERC721URIStorage.sol"; +import "@oz/access/Ownable.sol"; + +contract ERC721Mock is ERC721URIStorage, Ownable { + uint private _nextTokenId; + string public baseURI; + + constructor(string memory name, string memory symbol) + ERC721(name, symbol) + Ownable(msg.sender) + {} + + function _baseURI() internal view override returns (string memory) { + return baseURI; + } + + function setBaseURI(string memory newBaseURI) public onlyOwner { + baseURI = newBaseURI; + } + + function mint(address to) public onlyOwner returns (uint) { + uint tokenId = _nextTokenId; + _safeMint(to, tokenId); + _nextTokenId++; + + return tokenId; + } + + function setTokenURI(uint tokenId, string memory tokenURI) + public + onlyOwner + { + _setTokenURI(tokenId, tokenURI); + } +} + +// Mock contracts for testing hooks +contract MockHookContract { + bool public hookExecuted; + + function executeHook() external { + hookExecuted = true; + } +} + +contract MockFailingHookContract { + function executeHook() external pure { + revert("Hook execution failed"); + } +} diff --git a/test/mocks/modules/logicModule/LM_PC_FundingPot_v2NFTMock.sol b/test/mocks/modules/logicModule/LM_PC_FundingPot_v2NFTMock.sol index c7513d5cf..3d7ac2042 100644 --- a/test/mocks/modules/logicModule/LM_PC_FundingPot_v2NFTMock.sol +++ b/test/mocks/modules/logicModule/LM_PC_FundingPot_v2NFTMock.sol @@ -38,3 +38,18 @@ contract ERC721Mock is ERC721URIStorage, Ownable { _setTokenURI(tokenId, tokenURI); } } + +// Mock contracts for testing hooks +contract MockHookContract { + bool public hookExecuted; + + function executeHook() external { + hookExecuted = true; + } +} + +contract MockFailingHookContract { + function executeHook() external pure { + revert("Hook execution failed"); + } +} diff --git a/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol b/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol index 1d3032bae..ae4794b65 100644 --- a/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol +++ b/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol @@ -18,10 +18,13 @@ import { ERC20PaymentClientBaseV2Mock, ERC20Mock } from "test/utils/mocks/modules/paymentClient/ERC20PaymentClientBaseV2Mock.sol"; -import {ERC721Mock} from - "test/utils/mocks/modules/logicModules/LM_PC_FundingPot_v2NFTMock.sol"; import {IBondingCurveBase_v1} from "@fm/bondingCurve/interfaces/IBondingCurveBase_v1.sol"; +import { + ERC721Mock, + MockHookContract, + MockFailingHookContract +} from "test/utils/mocks/modules/logicModules/LM_PC_FundingPot_v1Mock.sol"; // System under Test (SuT) import {LM_PC_FundingPot_v1_Exposed} from @@ -53,14 +56,6 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { address contributor2_; address contributor3_; - bytes32 PROOF_ONE = - 0x0fd7c981d39bece61f7499702bf59b3114a90e66b51ba2c53abdf7b62986c00a; - bytes32 PROOF_TWO = - 0xe5ebd1e1b5a5478a944ecab36a9a954ac3b6b8216875f6524caa7a1d87096576; - bytes32[] PROOF = [PROOF_ONE, PROOF_TWO]; - bytes32 ROOT = - 0xaa5d581231e596618465a56aa0f5870ba6e20785fe436d5bfb82b08662ccc7c4; - // ------------------------------------------------------------------------- // State @@ -68,9 +63,6 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { LM_PC_FundingPot_v1_Exposed fundingPot; - // Storage variables to avoid stack too deep - uint32 private _testRoundId; - // Default round parameters for testing RoundParams private _defaultRoundParams; RoundParams private _editedRoundParams; @@ -362,7 +354,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { params.globalAccumulativeCaps ); - _testRoundId = fundingPot.getRoundCount(); + uint32 roundId = fundingPot.getRoundCount(); // Retrieve the stored parameters ( @@ -373,7 +365,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { bytes memory storedHookFunction, bool storedAutoClosure, bool storedGlobalAccumulativeCaps - ) = fundingPot.getRoundGenericParameters(_testRoundId); + ) = fundingPot.getRoundGenericParameters(roundId); // Compare with expected values assertEq(storedRoundStart, params.roundStart); @@ -422,11 +414,19 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { └── Given all valid parameters are provided └── When user attempts to edit the round └── Then all round details should be successfully updated + ├── roundStart should be updated to the new value + ├── roundEnd should be updated to the new value + ├── roundCap should be updated to the new value + ├── hookContract should be updated to the new value + ├── hookFunction should be updated to the new value + ├── autoClosure should be updated to the new value + └── globalAccumulativeCaps should be updated to the new value */ function testEditRound_revertsGivenUserIsNotFundingPotAdmin(address user_) public { + vm.assume(user_ != address(0) && user_ != address(this)); testCreateRound(); uint32 roundId = fundingPot.getRoundCount(); @@ -785,23 +785,27 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { ├── Given round does not exist │ └── When user attempts to set access criteria │ └── Then it should revert - │ + │ ├── Given round is active │ └── When user attempts to set access criteria │ └── Then it should revert - │ + │ + ├── Given AccessCriteriaId is greater than MAX_ACCESS_CRITERIA_ID + │ └── When user attempts to set access criteria + │ └── Then it should revert + │ ├── Given AccessCriteriaId is NFT and nftContract is 0x0 │ └── When user attempts to set access criteria │ └── Then it should revert - │ + │ ├── Given AccessCriteriaId is MERKLE and merkleRoot is 0x0 │ └── When user attempts to set access criteria │ └── Then it should revert - │ + │ ├── Given AccessCriteriaId is LIST and allowedAddresses is empty │ └── When user attempts to set access criteria │ └── Then it should revert - │ + │ └── Given all the valid parameters are provided └── When user attempts to set access criteria └── Then it should not revert @@ -821,7 +825,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { address nftContract, bytes32 merkleRoot, address[] memory allowedAddresses - ) = _helper_createAccessCriteria(accessCriteriaEnum_); + ) = _helper_createAccessCriteria(accessCriteriaEnum_, roundId); vm.startPrank(user_); bytes32 roleId = _authorizer.generateRoleId( @@ -853,7 +857,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { address nftContract, bytes32 merkleRoot, address[] memory allowedAddresses - ) = _helper_createAccessCriteria(accessCriteriaEnum); + ) = _helper_createAccessCriteria(accessCriteriaEnum, roundId); vm.expectRevert( abi.encodeWithSelector( @@ -883,7 +887,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { address nftContract, bytes32 merkleRoot, address[] memory allowedAddresses - ) = _helper_createAccessCriteria(accessCriteriaEnum); + ) = _helper_createAccessCriteria(accessCriteriaEnum, roundId); (uint roundStart,,,,,,) = fundingPot.getRoundGenericParameters(roundId); vm.warp(roundStart + 1); @@ -904,6 +908,39 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { ); } + function testFuzzSetAccessCriteria_revertsGivenAccessCriteriaIdIsGreaterThanMaxAccessCriteriaId( + uint8 accessCriteriaEnum + ) public { + vm.assume(accessCriteriaEnum > 4); + + testCreateRound(); + uint32 roundId = fundingPot.getRoundCount(); + + ( + address nftContract, + bytes32 merkleRoot, + address[] memory allowedAddresses + ) = _helper_createAccessCriteria(accessCriteriaEnum, roundId); + + (uint roundStart,,,,,,) = fundingPot.getRoundGenericParameters(roundId); + vm.warp(roundStart + 1); + + vm.expectRevert( + abi.encodeWithSelector( + ILM_PC_FundingPot_v1 + .Module__LM_PC_FundingPot__InvalidAccessCriteriaId + .selector + ) + ); + fundingPot.setAccessCriteriaForRound( + roundId, + accessCriteriaEnum, + nftContract, + merkleRoot, + allowedAddresses + ); + } + function testSetAccessCriteria_revertsGivenAccessCriteriaIdIsNFTAndNftContractIsZero( ) public { uint8 accessCriteriaEnum = @@ -916,7 +953,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { address nftContract, bytes32 merkleRoot, address[] memory allowedAddresses - ) = _helper_createAccessCriteria(accessCriteriaEnum); + ) = _helper_createAccessCriteria(accessCriteriaEnum, roundId); nftContract = address(0); vm.expectRevert( @@ -947,7 +984,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { address nftContract, bytes32 merkleRoot, address[] memory allowedAddresses - ) = _helper_createAccessCriteria(accessCriteriaEnum); + ) = _helper_createAccessCriteria(accessCriteriaEnum, roundId); merkleRoot = bytes32(uint(0x0)); vm.expectRevert( @@ -978,7 +1015,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { address nftContract, bytes32 merkleRoot, address[] memory allowedAddresses - ) = _helper_createAccessCriteria(accessCriteriaEnum); + ) = _helper_createAccessCriteria(accessCriteriaEnum, roundId); allowedAddresses = new address[](0); vm.expectRevert( @@ -1007,7 +1044,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { address nftContract, bytes32 merkleRoot, address[] memory allowedAddresses - ) = _helper_createAccessCriteria(accessCriteriaEnum); + ) = _helper_createAccessCriteria(accessCriteriaEnum, roundId); fundingPot.setAccessCriteriaForRound( roundId, @@ -1074,7 +1111,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { address nftContract, bytes32 merkleRoot, address[] memory allowedAddresses - ) = _helper_createAccessCriteria(accessCriteriaEnum); + ) = _helper_createAccessCriteria(accessCriteriaEnum, roundId); vm.startPrank(user_); bytes32 roleId = _authorizer.generateRoleId( @@ -1108,7 +1145,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { address nftContract, bytes32 merkleRoot, address[] memory allowedAddresses - ) = _helper_createAccessCriteria(accessCriteriaEnum); + ) = _helper_createAccessCriteria(accessCriteriaEnum, roundId); vm.expectRevert( abi.encodeWithSelector( @@ -1133,7 +1170,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { address nftContract, bytes32 merkleRoot, address[] memory allowedAddresses - ) = _helper_createAccessCriteria(accessCriteriaEnum); + ) = _helper_createAccessCriteria(accessCriteriaEnum, roundId); vm.expectRevert(); fundingPot.editAccessCriteriaForRound( @@ -1159,7 +1196,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { address nftContract, bytes32 merkleRoot, address[] memory allowedAddresses - ) = _helper_createAccessCriteria((accessCriteriaEnum + 1) % 5); // Use a different access criteria type + ) = _helper_createAccessCriteria((accessCriteriaEnum + 1) % 5, roundId); // Use a different access criteria type // Expect revert when trying to edit access criteria for an active round vm.expectRevert( @@ -1176,6 +1213,208 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { ); } + /* Test removeAllowlistedAddresses + ├── Given user does not have FUNDING_POT_ADMIN_ROLE + │ └── When user attempts to remove allowlisted addresses + │ └── Then it should revert + │ + ├── Given the round does not exist + │ └── When user attempts to remove allowlisted addresses + │ └── Then it should revert + │ + ├── Given the round has already started + │ └── When user attempts to remove allowlisted addresses + │ └── Then it should revert + │ + └── Given a valid round with LIST access criteria + └── When admin removes allowlisted addresses + └── Then the addresses should be removed from the allowlist + */ + function testRemoveAllowlistedAddresses() public { + testCreateRound(); + uint32 roundId = fundingPot.getRoundCount(); + + uint8 accessId = uint8(ILM_PC_FundingPot_v1.AccessCriteriaType.LIST); + ( + address nftContract, + bytes32 merkleRoot, + address[] memory allowedAddresses + ) = _helper_createAccessCriteria(accessId, roundId); + + fundingPot.setAccessCriteriaForRound( + roundId, accessId, nftContract, merkleRoot, allowedAddresses + ); + + address[] memory addressesToRemove = new address[](2); + addressesToRemove[0] = address(0x2); + addressesToRemove[1] = contributor2_; + + fundingPot.removeAllowlistedAddresses( + roundId, accessId, addressesToRemove + ); + + bool hasAccess = fundingPot.exposed_checkAccessCriteriaEligibility( + roundId, accessId, new bytes32[](0), contributor2_ + ); + + assertFalse(hasAccess); + + bool otherAddressesHaveAccess = fundingPot + .exposed_checkAccessCriteriaEligibility( + roundId, accessId, new bytes32[](0), address(0x3) + ); + + assertTrue(otherAddressesHaveAccess); + } + + /* Test: setAccessCriteriaPrivileges() + ├── Given user does not have FUNDING_POT_ADMIN_ROLE + │ └── When user attempts to set access criteria privileges + │ └── Then it should revert + │ + └── Given user has FUNDING_POT_ADMIN_ROLE + └── Given all valid parameters are provided + └── When user attempts to set access criteria privileges + ├── Then it should not revert + └── Then the access criteria privileges should be updated + */ + + function testFuzzSetAccessCriteriaPrivileges_revertsGivenUserDoesNotHaveFundingPotAdminRole( + uint8 accessCriteriaEnum, + address user_ + ) public { + vm.assume(accessCriteriaEnum >= 0 && accessCriteriaEnum <= 4); + vm.assume(user_ != address(0) && user_ != address(this)); + + uint32 roundId = fundingPot.createRound( + _defaultRoundParams.roundStart, + _defaultRoundParams.roundEnd, + _defaultRoundParams.roundCap, + _defaultRoundParams.hookContract, + _defaultRoundParams.hookFunction, + _defaultRoundParams.autoClosure, + _defaultRoundParams.globalAccumulativeCaps + ); + + ( + address nftContract, + bytes32 merkleRoot, + address[] memory allowedAddresses + ) = _helper_createAccessCriteria(accessCriteriaEnum, roundId); + + fundingPot.setAccessCriteriaForRound( + roundId, + accessCriteriaEnum, + nftContract, + merkleRoot, + allowedAddresses + ); + + vm.startPrank(user_); + bytes32 roleId = _authorizer.generateRoleId( + address(fundingPot), fundingPot.FUNDING_POT_ADMIN_ROLE() + ); + vm.expectRevert( + abi.encodeWithSelector( + IModule_v1.Module__CallerNotAuthorized.selector, roleId, user_ + ) + ); + + fundingPot.setAccessCriteriaPrivileges( + roundId, accessCriteriaEnum, 1000, false, 0, 0, 0 + ); + vm.stopPrank(); + } + + function testFuzzSetAccessCriteriaPrivileges_worksGivenAllConditionsMet( + uint8 accessCriteriaEnum + ) public { + vm.assume(accessCriteriaEnum > 0 && accessCriteriaEnum <= 4); + uint32 roundId = fundingPot.createRound( + _defaultRoundParams.roundStart, + _defaultRoundParams.roundEnd, + _defaultRoundParams.roundCap, + _defaultRoundParams.hookContract, + _defaultRoundParams.hookFunction, + _defaultRoundParams.autoClosure, + _defaultRoundParams.globalAccumulativeCaps + ); + + ( + address nftContract, + bytes32 merkleRoot, + address[] memory allowedAddresses + ) = _helper_createAccessCriteria(accessCriteriaEnum, roundId); + + fundingPot.setAccessCriteriaForRound( + roundId, + accessCriteriaEnum, + nftContract, + merkleRoot, + allowedAddresses + ); + + fundingPot.setAccessCriteriaPrivileges( + roundId, accessCriteriaEnum, 1000, false, 0, 0, 0 + ); + + ( + uint personalCap, + bool overrideContributionSpan, + uint start, + uint cliff, + uint end + ) = fundingPot.getRoundAccessCriteriaPrivileges( + roundId, accessCriteriaEnum + ); + + assertEq(personalCap, 1000); + assertEq(overrideContributionSpan, false); + assertEq(start, 0); + assertEq(cliff, 0); + assertEq(end, 0); + } + + /* Test: getRoundAccessCriteriaPrivileges() + ├── Given the access criteria does not exist + │ └── When user attempts to get access criteria privileges + │ └── Then it should return default values + + */ + function testFuzzGetRoundAccessCriteriaPrivileges_returnsDefaultValuesGivenInvalidAccessCriteriaId( + uint8 accessCriteriaEnum + ) public { + vm.assume(accessCriteriaEnum > 4); + + RoundParams memory params = _defaultRoundParams; + + uint32 roundId = fundingPot.createRound( + params.roundStart, + params.roundEnd, + params.roundCap, + params.hookContract, + params.hookFunction, + params.autoClosure, + params.globalAccumulativeCaps + ); + + ( + uint personalCap, + bool overrideContributionSpan, + uint start, + uint cliff, + uint end + ) = fundingPot.getRoundAccessCriteriaPrivileges( + roundId, accessCriteriaEnum + ); + + assertEq(personalCap, 0); + assertFalse(overrideContributionSpan); + assertEq(start, 0); + assertEq(cliff, 0); + assertEq(end, 0); + } + /* Test: contributeToRoundFor() unhappy paths ├── Given the round has not started yet │ └── When the user contributes to the round @@ -1205,15 +1444,27 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { │ │ └── When the user contributes to the round │ │ └── Then the transaction should revert │ │ + │ ├── Given the user tries to contribute with a zero amount + │ │ └── When the user contributes to the round + │ │ └── Then the transaction should revert + │ │ │ └── Given a user has already contributed up to their personal cap │ └── When the user attempts to contribute again │ └── Then the transaction should revert + │ + └── Given the round contribution cap is reached + └── When the user attempts to contribute + └── Then the transaction should revert */ function testcontributeToRoundFor_revertsGivenContributionIsBeforeRoundStart( ) public { testCreateRound(); + vm.assume(amount > 0 && amount <= 500); + vm.assume(roundStart > block.timestamp && roundStart < roundEnd); + vm.assume(roundCap > 0); + uint32 roundId = fundingPot.getRoundCount(); uint8 accessId = 1; uint amount = 250; @@ -1222,7 +1473,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { address nftContract, bytes32 merkleRoot, address[] memory allowedAddresses - ) = _helper_createAccessCriteria(accessId); + ) = _helper_createAccessCriteria(accessId, roundId); fundingPot.setAccessCriteriaForRound( roundId, accessId, nftContract, merkleRoot, allowedAddresses @@ -1233,7 +1484,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { // Approve vm.prank(contributor1_); - _token.approve(address(fundingPot), 500); + _token.approve(address(fundingPot), amount); vm.expectRevert( abi.encodeWithSelector( @@ -1262,7 +1513,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { address nftContract, bytes32 merkleRoot, address[] memory allowedAddresses - ) = _helper_createAccessCriteria(accessId); + ) = _helper_createAccessCriteria(accessId, roundId); fundingPot.setAccessCriteriaForRound( roundId, accessId, nftContract, merkleRoot, allowedAddresses @@ -1295,7 +1546,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { function testcontributeToRoundFor_revertsGivenNFTAccessCriteriaIsNotMet() public { - uint8 accessId = 2; + uint8 accessId = uint8(ILM_PC_FundingPot_v1.AccessCriteriaType.NFT); _helper_setupRoundWithAccessCriteria(accessId); uint32 roundId = fundingPot.getRoundCount(); @@ -1333,11 +1584,15 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { uint8 accessId = 3; uint amount = 250; + (,,,, bytes32[] memory proofB) = _helper_generateMerkleTreeForTwoLeaves( + contributor1_, contributor2_, roundId + ); + ( address nftContract, bytes32 merkleRoot, address[] memory allowedAddresses - ) = _helper_createAccessCriteria(accessId); + ) = _helper_createAccessCriteria(accessId, roundId); fundingPot.setAccessCriteriaForRound( roundId, accessId, nftContract, merkleRoot, allowedAddresses @@ -1350,7 +1605,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { vm.warp(roundStart + 1); // Approve - vm.prank(contributor1_); + vm.prank(contributor3_); _token.approve(address(fundingPot), amount); vm.expectRevert( @@ -1379,7 +1634,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { address nftContract, bytes32 merkleRoot, address[] memory allowedAddresses - ) = _helper_createAccessCriteria(accessId); + ) = _helper_createAccessCriteria(accessId, roundId); fundingPot.setAccessCriteriaForRound( roundId, accessId, nftContract, merkleRoot, allowedAddresses @@ -1421,7 +1676,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { address nftContract, bytes32 merkleRoot, address[] memory allowedAddresses - ) = _helper_createAccessCriteria(accessId); + ) = _helper_createAccessCriteria(accessId, roundId); fundingPot.setAccessCriteriaForRound( roundId, accessId, nftContract, merkleRoot, allowedAddresses @@ -1469,6 +1724,25 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { │ └── When the user contributes to the round │ └── Then the funds are transferred to the funding pot │ And the contribution is recorded + │ + ├── Given the access criteria is NFT + │ And the user fulfills the access criteria + │ └── When the user contributes to the round + │ └── Then the funds are transferred to the funding pot + │ And the contribution is recorded + │ + ├── Given the access criteria is MERKLE + │ And the user fulfills the access criteria + │ └── When the user contributes to the round + │ └── Then the funds are transferred to the funding pot + │ And the contribution is recorded + │ + ├── Given the access criteria is LIST + │ And the user fulfills the access criteria + │ └── When the user contributes to the round + │ └── Then the funds are transferred to the funding pot + │ And the contribution is recorded + │ ├── Given the round contribution cap is not reached │ └── When the user contributes to the round so that it exceeds the round contribution cap │ └── Then only the valid contribution amount is transferred to the funding pot @@ -1505,30 +1779,41 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { function testcontributeToRoundFor_worksGivenAllConditionsMet() public { testCreateRound(); + uint8 accessId = uint8(ILM_PC_FundingPot_v1.AccessCriteriaType.NFT); + uint32 roundId = fundingPot.getRoundCount(); - uint8 accessId = 1; uint amount = 250; ( address nftContract, bytes32 merkleRoot, address[] memory allowedAddresses - ) = _helper_createAccessCriteria(accessId); + ) = _helper_createAccessCriteria(accessId, roundId); fundingPot.setAccessCriteriaForRound( roundId, accessId, nftContract, merkleRoot, allowedAddresses ); fundingPot.setAccessCriteriaPrivileges( - roundId, accessId, 500, false, 0, 0, 0 + roundId, accessId, 1000, false, 0, 0, 0 ); mockNFTContract.mint(contributor1_); - (uint roundStart,,,,,,) = fundingPot.getRoundGenericParameters(roundId); - vm.warp(roundStart + 1); + (bool isEligible, uint remainingAmountAllowedToContribute) = fundingPot + .getUserEligibility(roundId, accessId, new bytes32[](0), contributor1_); + + assertTrue(isEligible); + assertEq(remainingAmountAllowedToContribute, 1000); + + vm.warp(_defaultRoundParams.roundStart + 1); // Approve vm.prank(contributor1_); - _token.approve(address(fundingPot), 500); + _token.approve(address(fundingPot), amount); + + vm.expectEmit(true, false, false, true); + emit ILM_PC_FundingPot_v1.ContributionMade( + roundId, contributor1_, amount + ); vm.prank(contributor1_); fundingPot.contributeToRoundFor( @@ -1555,43 +1840,185 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { && accessCriteriaEnumNew >= 0 && accessCriteriaEnumNew <= 4 ); - _helper_setupRoundWithAccessCriteria(accessCriteriaEnumOld); + _helper_setupRoundWithAccessCriteria(accessId); uint32 roundId = fundingPot.getRoundCount(); - uint8 accessId = 0; uint amount = 201; ( address nftContract, bytes32 merkleRoot, address[] memory allowedAddresses - ) = _helper_createAccessCriteria(accessId); + ) = _helper_createAccessCriteria(accessId, roundId); fundingPot.setAccessCriteriaForRound( roundId, accessId, nftContract, merkleRoot, allowedAddresses ); + + mockNFTContract.mint(contributor1_); + fundingPot.setAccessCriteriaPrivileges( - roundId, accessId, 200, false, 0, 0, 0 + roundId, accessId, 500, false, 0, 0, 0 ); - (uint roundStart,,,,,,) = fundingPot.getRoundGenericParameters(roundId); - vm.warp(roundStart + 1); + vm.warp(_defaultRoundParams.roundStart + 1); - // Approve - vm.prank(contributor1_); - _token.approve(address(fundingPot), amount); + vm.startPrank(contributor1_); + _token.approve(address(fundingPot), 250); + + vm.expectEmit(true, false, false, true); + emit ILM_PC_FundingPot_v1.ContributionMade(roundId, contributor1_, 250); + + fundingPot.contributeToRound(roundId, 250, accessId, new bytes32[](0)); + vm.stopPrank(); + + uint userContribution = fundingPot.exposed_getUserContributionToRound( + roundId, contributor1_ + ); + assertEq(userContribution, 250); + + uint totalContributions = + fundingPot.exposed_getTotalRoundContributions(roundId); + assertEq(totalContributions, 250); + } + + function testContributeToRound_worksGivenMerkleAccessCriteriaMet() public { + uint8 accessId = uint8(ILM_PC_FundingPot_v1.AccessCriteriaType.MERKLE); + + uint32 roundId = fundingPot.createRound( + _defaultRoundParams.roundStart, + _defaultRoundParams.roundEnd, + _defaultRoundParams.roundCap, + _defaultRoundParams.hookContract, + _defaultRoundParams.hookFunction, + _defaultRoundParams.autoClosure, + _defaultRoundParams.globalAccumulativeCaps + ); + + (,,,, bytes32[] memory proofB) = _helper_generateMerkleTreeForTwoLeaves( + contributor1_, contributor2_, roundId + ); + + ( + address nftContract, + bytes32 merkleRoot, + address[] memory allowedAddresses + ) = _helper_createAccessCriteria(accessId, roundId); + + fundingPot.setAccessCriteriaForRound( + roundId, accessId, nftContract, merkleRoot, allowedAddresses + ); + + fundingPot.setAccessCriteriaPrivileges( + roundId, accessId, 500, false, 0, 0, 0 + ); + + vm.warp(_defaultRoundParams.roundStart + 1); + + uint contributionAmount = 250; + vm.startPrank(contributor2_); + _token.approve(address(fundingPot), contributionAmount); + + vm.expectEmit(true, false, false, true); + emit ILM_PC_FundingPot_v1.ContributionMade( + roundId, contributor2_, contributionAmount + ); vm.prank(contributor1_); fundingPot.contributeToRoundFor( contributor1_, roundId, amount, accessId, new bytes32[](0) ); - // only the amount that does not exceed the roundcap is contributed - assertEq( - fundingPot.exposed_getUserContributionToRound( - roundId, contributor1_ - ), - 200 + ( + address nftContract, + bytes32 merkleRoot, + address[] memory allowedAddresses + ) = _helper_createAccessCriteria(accessId, roundId); + + fundingPot.setAccessCriteriaForRound( + roundId, accessId, nftContract, merkleRoot, allowedAddresses ); + + fundingPot.setAccessCriteriaPrivileges( + roundId, accessId, 500, false, 0, 0, 0 + ); + + vm.warp(_defaultRoundParams.roundStart + 1); + + vm.startPrank(contributor2_); + _token.approve(address(fundingPot), 250); + + vm.expectEmit(true, false, false, true); + emit ILM_PC_FundingPot_v1.ContributionMade(roundId, contributor2_, 250); + + fundingPot.contributeToRound(roundId, 250, accessId, new bytes32[](0)); + vm.stopPrank(); + + uint userContribution = fundingPot.exposed_getUserContributionToRound( + roundId, contributor2_ + ); + assertEq(userContribution, 250); + + uint totalContributions = + fundingPot.exposed_getTotalRoundContributions(roundId); + assertEq(totalContributions, 250); + } + + function testContributeToRound_worksGivenUserCurrentContributionExceedsTheRoundCap( + ) public { + _defaultRoundParams.roundCap = 150; + _defaultRoundParams.autoClosure = true; + + uint8 accessId = uint8(ILM_PC_FundingPot_v1.AccessCriteriaType.NFT); + + uint32 roundId = fundingPot.createRound( + _defaultRoundParams.roundStart, + _defaultRoundParams.roundEnd, + _defaultRoundParams.roundCap, + _defaultRoundParams.hookContract, + _defaultRoundParams.hookFunction, + _defaultRoundParams.autoClosure, + _defaultRoundParams.globalAccumulativeCaps + ); + + ( + address nftContract, + bytes32 merkleRoot, + address[] memory allowedAddresses + ) = _helper_createAccessCriteria(accessId, roundId); + + fundingPot.setAccessCriteriaForRound( + roundId, accessId, nftContract, merkleRoot, allowedAddresses + ); + + uint personalCap = 200; + fundingPot.setAccessCriteriaPrivileges( + roundId, accessId, personalCap, false, 0, 0, 0 + ); + + mockNFTContract.mint(contributor1_); + mockNFTContract.mint(contributor2_); + + vm.warp(_defaultRoundParams.roundStart); + + vm.startPrank(contributor1_); + _token.approve(address(fundingPot), 100); + fundingPot.contributeToRound(roundId, 100, accessId, new bytes32[](0)); + vm.stopPrank(); + + vm.startPrank(contributor2_); + _token.approve(address(fundingPot), 100); + fundingPot.contributeToRound(roundId, 100, accessId, new bytes32[](0)); + vm.stopPrank(); + + uint contribution = fundingPot.exposed_getUserContributionToRound( + roundId, contributor2_ + ); + assertEq(contribution, 50); + + uint totalContribution = + fundingPot.exposed_getTotalRoundContributions(roundId); + assertEq(totalContribution, _defaultRoundParams.roundCap); + assertTrue(fundingPot.isRoundClosed(roundId)); } function testcontributeToRoundFor_worksGivenContributionPartiallyExceedingPersonalCap( @@ -1608,7 +2035,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { address nftContract, bytes32 merkleRoot, address[] memory allowedAddresses - ) = _helper_createAccessCriteria(accessId); + ) = _helper_createAccessCriteria(accessId, roundId); fundingPot.setAccessCriteriaForRound( roundId, accessId, nftContract, merkleRoot, allowedAddresses @@ -1626,6 +2053,11 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { vm.prank(contributor1_); _token.approve(address(fundingPot), 1000 ether); + vm.expectEmit(true, false, false, true); + emit ILM_PC_FundingPot_v1.ContributionMade( + roundId, contributor1_, firstAmount + ); + // First contribution vm.prank(contributor1_); fundingPot.contributeToRoundFor( @@ -1633,6 +2065,12 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { ); uint secondAmount = 200; + uint expectedSecondAmount = personalCap - firstAmount; + + vm.expectEmit(true, false, false, true); + emit ILM_PC_FundingPot_v1.ContributionMade( + roundId, contributor1_, expectedSecondAmount + ); vm.prank(contributor1_); fundingPot.contributeToRoundFor( @@ -1659,7 +2097,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { address nftContract, bytes32 merkleRoot, address[] memory allowedAddresses - ) = _helper_createAccessCriteria(accessId); + ) = _helper_createAccessCriteria(accessId, roundId); fundingPot.setAccessCriteriaForRound( roundId, accessId, nftContract, merkleRoot, allowedAddresses @@ -1680,6 +2118,12 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { vm.prank(contributor1_); _token.approve(address(fundingPot), amount); + // Expect the ContributionMade event to be emitted + vm.expectEmit(true, true, false, true); + emit ILM_PC_FundingPot_v1.ContributionMade( + roundId, contributor1_, amount + ); + // This should succeed despite being after round end, due to override privilege vm.prank(contributor1_); fundingPot.contributeToRoundFor( @@ -1713,7 +2157,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { address nftContract, bytes32 merkleRoot, address[] memory allowedAddresses - ) = _helper_createAccessCriteria(accessCriteriaId); + ) = _helper_createAccessCriteria(accessCriteriaId, round1Id); fundingPot.setAccessCriteriaForRound( round1Id, accessCriteriaId, @@ -1767,6 +2211,9 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { merkleProof: new bytes32[](0) }); + vm.expectEmit(true, false, false, true); + emit ILM_PC_FundingPot_v1.ContributionMade(round2Id, contributor1_, 700); + // Contribute to round 2 with unspent capacity from round 1 fundingPot.contributeToRoundFor( contributor1_, @@ -1778,7 +2225,6 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { ); vm.stopPrank(); - // Verify contributions are recorded correctly assertEq( fundingPot.exposed_getUserContributionToRound( round1Id, contributor1_ @@ -1797,7 +2243,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { function testcontributeToRoundFor_worksGivenTotalRoundCapAccumulation() public { - _defaultRoundParams.globalAccumulativeCaps = true; // global accumulative caps enabled + _defaultRoundParams.globalAccumulativeCaps = true; // Create Round 1 fundingPot.createRound( @@ -1811,12 +2257,12 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { ); uint32 round1Id = fundingPot.getRoundCount(); - uint8 accessId = 0; + uint8 accessId = 1; ( address nftContract, bytes32 merkleRoot, address[] memory allowedAddresses - ) = _helper_createAccessCriteria(accessId); + ) = _helper_createAccessCriteria(accessId, round1Id); fundingPot.setAccessCriteriaForRound( round1Id, accessId, nftContract, merkleRoot, allowedAddresses ); @@ -1863,8 +2309,6 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { // Move to Round 2 vm.warp(_defaultRoundParams.roundStart + 3 days + 1); - // Round 2: Contributors try to use the accumulated capacity - vm.startPrank(contributor2_); _token.approve(address(fundingPot), 400); fundingPot.contributeToRoundFor( @@ -1879,7 +2323,6 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { ); vm.stopPrank(); - // Verify Round 1 contributions assertEq(fundingPot.exposed_getTotalRoundContributions(round1Id), 500); assertEq( fundingPot.exposed_getUserContributionToRound( @@ -1894,7 +2337,6 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { 200 ); - // Verify Round 2 contributions assertEq(fundingPot.exposed_getTotalRoundContributions(round2Id), 700); assertEq( fundingPot.exposed_getUserContributionToRound( @@ -1912,6 +2354,78 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { assertEq(fundingPot.exposed_getTotalRoundContributions(round2Id), 700); } + /* Test Close Round + └── Given a round + └── When the round is not closed + └── And the closure conditions are not met + └── When the user attempts to close the round + └── Then the transaction should revert + */ + function testCloseRound_revertsGivenClosureConditionsNotMet() public { + uint32 roundId = fundingPot.createRound( + _defaultRoundParams.roundStart, + _defaultRoundParams.roundEnd, + _defaultRoundParams.roundCap, + _defaultRoundParams.hookContract, + _defaultRoundParams.hookFunction, + _defaultRoundParams.autoClosure, + _defaultRoundParams.globalAccumulativeCaps + ); + + vm.warp(_defaultRoundParams.roundStart); + + vm.expectRevert( + ILM_PC_FundingPot_v1 + .Module__LM_PC_FundingPot__ClosureConditionsNotMet + .selector + ); + fundingPot.closeRound(roundId); + } + + function testCloseRound_revertsGivenFailingHookExecution() public { + MockFailingHookContract mockHook = new MockFailingHookContract(); + _defaultRoundParams.hookContract = address(mockHook); + _defaultRoundParams.hookFunction = + abi.encodeWithSignature("executeHook()"); + + uint32 roundId = fundingPot.createRound( + _defaultRoundParams.roundStart, + _defaultRoundParams.roundEnd, + _defaultRoundParams.roundCap, + _defaultRoundParams.hookContract, + _defaultRoundParams.hookFunction, + _defaultRoundParams.autoClosure, + _defaultRoundParams.globalAccumulativeCaps + ); + + fundingPot.setAccessCriteriaForRound( + roundId, + uint8(ILM_PC_FundingPot_v1.AccessCriteriaType.OPEN), + address(0), + bytes32(0), + new address[](0) + ); + + fundingPot.setAccessCriteriaPrivileges( + roundId, + uint8(ILM_PC_FundingPot_v1.AccessCriteriaType.OPEN), + 1000, + false, + 0, + 0, + 0 + ); + + vm.warp(_defaultRoundParams.roundEnd); + + vm.expectRevert( + ILM_PC_FundingPot_v1 + .Module__LM_PC_FundingPot__HookExecutionFailed + .selector + ); + fundingPot.closeRound(roundId); + } + // ------------------------------------------------------------------------- // Test: closeRound() @@ -2014,7 +2528,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { address nftContract, bytes32 merkleRoot, address[] memory allowedAddresses - ) = _helper_createAccessCriteria(accessId); + ) = _helper_createAccessCriteria(accessId, roundId); fundingPot.setAccessCriteriaForRound( roundId, accessId, nftContract, merkleRoot, allowedAddresses @@ -2051,7 +2565,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { address nftContract, bytes32 merkleRoot, address[] memory allowedAddresses - ) = _helper_createAccessCriteria(accessId); + ) = _helper_createAccessCriteria(accessId, roundId); fundingPot.setAccessCriteriaForRound( roundId, accessId, nftContract, merkleRoot, allowedAddresses @@ -2093,7 +2607,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { address nftContract, bytes32 merkleRoot, address[] memory allowedAddresses - ) = _helper_createAccessCriteria(accessId); + ) = _helper_createAccessCriteria(accessId, roundId); fundingPot.setAccessCriteriaForRound( roundId, accessId, nftContract, merkleRoot, allowedAddresses @@ -2132,7 +2646,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { address nftContract, bytes32 merkleRoot, address[] memory allowedAddresses - ) = _helper_createAccessCriteria(accessId); + ) = _helper_createAccessCriteria(accessId, roundId); fundingPot.setAccessCriteriaForRound( roundId, accessId, nftContract, merkleRoot, allowedAddresses @@ -2168,7 +2682,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { address nftContract, bytes32 merkleRoot, address[] memory allowedAddresses - ) = _helper_createAccessCriteria(accessId); + ) = _helper_createAccessCriteria(accessId, roundId); fundingPot.setAccessCriteriaForRound( roundId, accessId, nftContract, merkleRoot, allowedAddresses @@ -2346,22 +2860,6 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { // ------------------------------------------------------------------------- // Internal Functions - function testFuzz_validateAccessCriteria( - uint32 roundId_, - uint8 accessId_, - bytes32[] calldata merkleProof_ - ) external view { - vm.assume(roundId_ <= fundingPot.getRoundCount() + 1); - vm.assume(accessId_ <= 4); - - try fundingPot.exposed_validateAccessCriteria( - roundId_, accessId_, merkleProof_, msg.sender - ) { - assert(true); - } catch (bytes memory) { - assert(false); - } - } function testFuzz_validateAndAdjustCapsWithUnspentCap( uint32 roundId_, @@ -2412,6 +2910,65 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { } } + function testFuzz_ValidTimes(uint start, uint cliff, uint end) public { + vm.assume(cliff <= type(uint).max - start); + + bool isValid = fundingPot.exposed_validTimes(start, cliff, end); + + assertEq(isValid, start + cliff <= end); + + if (start > end) { + assertFalse(isValid); + } + + if (start == end) { + assertEq(isValid, cliff == 0); + } + } + + // ------------------------------------------------------------------------- + // Test: _calculateUnusedCapacityFromPreviousRounds + + function test_calculateUnusedCapacityFromPreviousRounds() public { + // round 1 (no accumulation) + fundingPot.createRound( + _defaultRoundParams.roundStart, + _defaultRoundParams.roundEnd, + _defaultRoundParams.roundCap, + _defaultRoundParams.hookContract, + _defaultRoundParams.hookFunction, + _defaultRoundParams.autoClosure, + _defaultRoundParams.globalAccumulativeCaps + ); + + // round 2 (with accumulation) + fundingPot.createRound( + _defaultRoundParams.roundStart + 300, + _defaultRoundParams.roundEnd + 400, + _defaultRoundParams.roundCap, + _defaultRoundParams.hookContract, + _defaultRoundParams.hookFunction, + _defaultRoundParams.autoClosure, + true // globalAccumulativeCaps on + ); + + // round 3 + fundingPot.createRound( + _defaultRoundParams.roundStart + 500, + _defaultRoundParams.roundEnd + 600, + _defaultRoundParams.roundCap, + _defaultRoundParams.hookContract, + _defaultRoundParams.hookFunction, + _defaultRoundParams.autoClosure, + true + ); + + // Calculate unused capacity + uint actualUnusedCapacity = + fundingPot.exposed_calculateUnusedCapacityFromPreviousRounds(3); + assertEq(actualUnusedCapacity, 1000); + } + // ------------------------------------------------------------------------- // Test: _checkRoundClosureConditions @@ -2437,21 +2994,14 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { address nftContract, bytes32 merkleRoot, address[] memory allowedAddresses - ) = _helper_createAccessCriteria(accessId); + ) = _helper_createAccessCriteria(accessId, roundId); fundingPot.setAccessCriteriaForRound( roundId, accessId, nftContract, merkleRoot, allowedAddresses ); fundingPot.setAccessCriteriaPrivileges( - roundId, - accessId, - 1000, // personal cap equal to round cap - false, - 0, // no start - 0, // no cliff - 0 // no end + roundId, accessId, 1000, false, 0, 0, 0 ); - // Contribute up to the cap vm.warp(params.roundStart + 1); vm.startPrank(contributor1_); _token.approve(address(fundingPot), params.roundCap); @@ -2528,14 +3078,14 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { address nftContract, bytes32 merkleRoot, address[] memory allowedAddresses - ) = _helper_createAccessCriteria(accessId); + ) = _helper_createAccessCriteria(accessId, roundId); fundingPot.setAccessCriteriaForRound( roundId, accessId, nftContract, merkleRoot, allowedAddresses ); fundingPot.setAccessCriteriaPrivileges( roundId, accessId, - 1000, // personal cap equal to round cap + 1000, false, 0, // no start 0, // no cliff @@ -2597,7 +3147,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { address nftContract, bytes32 merkleRoot, address[] memory allowedAddresses - ) = _helper_createAccessCriteria(accessId); + ) = _helper_createAccessCriteria(accessId, roundId); fundingPot.setAccessCriteriaForRound( roundId, accessId, nftContract, merkleRoot, allowedAddresses @@ -2659,7 +3209,43 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { }); } - function _helper_createAccessCriteria(uint8 accessCriteriaEnum) + function _helper_generateMerkleTreeForTwoLeaves( + address contributorA, + address contributorB, + uint32 roundId + ) + internal + pure + returns ( + bytes32 root, + bytes32 leafA, + bytes32 leafB, + bytes32[] memory proofA, + bytes32[] memory proofB + ) + { + leafA = keccak256(abi.encodePacked(contributorA, roundId)); + leafB = keccak256(abi.encodePacked(contributorB, roundId)); + + proofA = new bytes32[](1); + proofB = new bytes32[](1); + + // Ensure consistent ordering for root calculation + if (leafA < leafB) { + root = keccak256(abi.encodePacked(leafA, leafB)); + proofA[0] = leafB; // Proof for A is B + proofB[0] = leafA; // Proof for B is A + } else { + root = keccak256(abi.encodePacked(leafB, leafA)); + proofA[0] = leafB; // Proof for A is still B + proofB[0] = leafA; // Proof for B is still A + } + } + + function _helper_createAccessCriteria( + uint8 accessCriteriaEnum, + uint32 roundId + ) internal view returns ( @@ -2689,7 +3275,10 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { accessCriteriaEnum == uint8(ILM_PC_FundingPot_v1.AccessCriteriaType.MERKLE) ) { - bytes32 merkleRoot = ROOT; + (bytes32 merkleRoot,,,,) = + _helper_generateMerkleTreeForTwoLeaves( + contributor1_, contributor2_, roundId + ); nftContract_ = address(0x0); merkleRoot_ = merkleRoot; @@ -2698,11 +3287,11 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { accessCriteriaEnum == uint8(ILM_PC_FundingPot_v1.AccessCriteriaType.LIST) ) { - address[] memory allowedAddresses = new address[](3); + address[] memory allowedAddresses = new address[](4); allowedAddresses[0] = address(this); allowedAddresses[1] = address(0x2); allowedAddresses[2] = address(0x3); - + allowedAddresses[3] = contributor2_; nftContract_ = address(0x0); merkleRoot_ = bytes32(uint(0x0)); allowedAddresses_ = allowedAddresses; @@ -2728,7 +3317,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { address nftContract, bytes32 merkleRoot, address[] memory allowedAddresses - ) = _helper_createAccessCriteria(accessCriteriaEnum); + ) = _helper_createAccessCriteria(accessCriteriaEnum, roundId); fundingPot.setAccessCriteriaForRound( roundId, diff --git a/test/unit/modules/logicModule/LM_PC_FundingPot_v1_Exposed.sol b/test/unit/modules/logicModule/LM_PC_FundingPot_v1_Exposed.sol index 1e2f754a7..0611fe3e6 100644 --- a/test/unit/modules/logicModule/LM_PC_FundingPot_v1_Exposed.sol +++ b/test/unit/modules/logicModule/LM_PC_FundingPot_v1_Exposed.sol @@ -5,7 +5,7 @@ pragma solidity 0.8.23; import {LM_PC_FundingPot_v1} from "src/modules/logicModule/LM_PC_FundingPot_v1.sol"; -// Access Mock of the PP_Template_v1 contract for Testing. +// Access Mock of the LM_PC_FundingPot_v1 contract for Testing. contract LM_PC_FundingPot_v1_Exposed is LM_PC_FundingPot_v1 { // Use the `exposed_` prefix for functions to expose internal functions for // testing. @@ -65,6 +65,7 @@ contract LM_PC_FundingPot_v1_Exposed is LM_PC_FundingPot_v1 { } /** +<<<<<<< HEAD * @notice Exposes the internal _validateAccessCriteria function for testing */ function exposed_validateAccessCriteria( @@ -77,6 +78,8 @@ contract LM_PC_FundingPot_v1_Exposed is LM_PC_FundingPot_v1 { } /** +======= +>>>>>>> bec7682a (test: improve test coverage) * @notice Exposes the internal _checkAccessCriteriaEligibility function for testing */ function exposed_checkAccessCriteriaEligibility( @@ -91,6 +94,7 @@ contract LM_PC_FundingPot_v1_Exposed is LM_PC_FundingPot_v1 { } /** +<<<<<<< HEAD * @notice Exposes the internal _checkNftOwnership function for testing */ function exposed_checkNftOwnership(address nftContract_, address user_) @@ -114,6 +118,8 @@ contract LM_PC_FundingPot_v1_Exposed is LM_PC_FundingPot_v1 { } /** +======= +>>>>>>> bec7682a (test: improve test coverage) * @notice Exposes the internal _calculateUnusedCapacityFromPreviousRounds function for testing */ function exposed_calculateUnusedCapacityFromPreviousRounds(uint32 roundId_) @@ -125,6 +131,7 @@ contract LM_PC_FundingPot_v1_Exposed is LM_PC_FundingPot_v1 { } /** +<<<<<<< HEAD * @notice Exposes the internal _closeRound function for testing */ function exposed_closeRound(uint32 roundId_) external { @@ -132,6 +139,8 @@ contract LM_PC_FundingPot_v1_Exposed is LM_PC_FundingPot_v1 { } /** +======= +>>>>>>> bec7682a (test: improve test coverage) * @notice Exposes the internal _checkRoundClosureConditions function for testing */ function exposed_checkRoundClosureConditions(uint32 roundId_) From 8d3323524be4a3c2299870c29443985acf2d546c Mon Sep 17 00:00:00 2001 From: JeffreyJoel Date: Wed, 23 Apr 2025 08:57:02 +0100 Subject: [PATCH 096/130] chore: merge branch contribute and close functionality --- .../logicModule/LM_PC_FundingPot_v1.t.sol | 100 ------------------ 1 file changed, 100 deletions(-) diff --git a/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol b/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol index ae4794b65..fd0849c2e 100644 --- a/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol +++ b/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol @@ -2354,78 +2354,6 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { assertEq(fundingPot.exposed_getTotalRoundContributions(round2Id), 700); } - /* Test Close Round - └── Given a round - └── When the round is not closed - └── And the closure conditions are not met - └── When the user attempts to close the round - └── Then the transaction should revert - */ - function testCloseRound_revertsGivenClosureConditionsNotMet() public { - uint32 roundId = fundingPot.createRound( - _defaultRoundParams.roundStart, - _defaultRoundParams.roundEnd, - _defaultRoundParams.roundCap, - _defaultRoundParams.hookContract, - _defaultRoundParams.hookFunction, - _defaultRoundParams.autoClosure, - _defaultRoundParams.globalAccumulativeCaps - ); - - vm.warp(_defaultRoundParams.roundStart); - - vm.expectRevert( - ILM_PC_FundingPot_v1 - .Module__LM_PC_FundingPot__ClosureConditionsNotMet - .selector - ); - fundingPot.closeRound(roundId); - } - - function testCloseRound_revertsGivenFailingHookExecution() public { - MockFailingHookContract mockHook = new MockFailingHookContract(); - _defaultRoundParams.hookContract = address(mockHook); - _defaultRoundParams.hookFunction = - abi.encodeWithSignature("executeHook()"); - - uint32 roundId = fundingPot.createRound( - _defaultRoundParams.roundStart, - _defaultRoundParams.roundEnd, - _defaultRoundParams.roundCap, - _defaultRoundParams.hookContract, - _defaultRoundParams.hookFunction, - _defaultRoundParams.autoClosure, - _defaultRoundParams.globalAccumulativeCaps - ); - - fundingPot.setAccessCriteriaForRound( - roundId, - uint8(ILM_PC_FundingPot_v1.AccessCriteriaType.OPEN), - address(0), - bytes32(0), - new address[](0) - ); - - fundingPot.setAccessCriteriaPrivileges( - roundId, - uint8(ILM_PC_FundingPot_v1.AccessCriteriaType.OPEN), - 1000, - false, - 0, - 0, - 0 - ); - - vm.warp(_defaultRoundParams.roundEnd); - - vm.expectRevert( - ILM_PC_FundingPot_v1 - .Module__LM_PC_FundingPot__HookExecutionFailed - .selector - ); - fundingPot.closeRound(roundId); - } - // ------------------------------------------------------------------------- // Test: closeRound() @@ -2491,34 +2419,6 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { vm.stopPrank(); } - function testCloseRound_revertsGivenRoundDoesNotExist() public { - uint32 nonExistentRoundId = 999; - - vm.expectRevert( - abi.encodeWithSelector( - ILM_PC_FundingPot_v1 - .Module__LM_PC_FundingPot__RoundNotCreated - .selector - ) - ); - fundingPot.closeRound(nonExistentRoundId); - } - - function testCloseRound_revertsGivenRoundIsAlreadyClosed() public { - testCloseRound_worksGivenRoundCapHasBeenReached(); - // Try to close it again - uint32 roundId = fundingPot.getRoundCount(); - - vm.expectRevert( - abi.encodeWithSelector( - ILM_PC_FundingPot_v1 - .Module__LM_PC_FundingPot__RoundHasEnded - .selector - ) - ); - fundingPot.closeRound(roundId); - } - function testCloseRound_worksGivenRoundHasStartedButNotEnded() public { testCreateRound(); uint32 roundId = fundingPot.getRoundCount(); From a16f13db625ed75741096dad806af5d9514cbb11 Mon Sep 17 00:00:00 2001 From: JeffreyJoel Date: Wed, 23 Apr 2025 14:14:42 +0100 Subject: [PATCH 097/130] fix: fix merge changes --- .../modules/logicModule/LM_PC_FundingPot_v1.t.sol | 1 - .../logicModule/LM_PC_FundingPot_v1_Exposed.sol | 15 ++++++--------- 2 files changed, 6 insertions(+), 10 deletions(-) diff --git a/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol b/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol index fd0849c2e..1266af794 100644 --- a/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol +++ b/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol @@ -2760,7 +2760,6 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { // ------------------------------------------------------------------------- // Internal Functions - function testFuzz_validateAndAdjustCapsWithUnspentCap( uint32 roundId_, uint amount_, diff --git a/test/unit/modules/logicModule/LM_PC_FundingPot_v1_Exposed.sol b/test/unit/modules/logicModule/LM_PC_FundingPot_v1_Exposed.sol index 0611fe3e6..bf8abbb1e 100644 --- a/test/unit/modules/logicModule/LM_PC_FundingPot_v1_Exposed.sol +++ b/test/unit/modules/logicModule/LM_PC_FundingPot_v1_Exposed.sol @@ -65,7 +65,7 @@ contract LM_PC_FundingPot_v1_Exposed is LM_PC_FundingPot_v1 { } /** -<<<<<<< HEAD + * <<<<<<< HEAD * @notice Exposes the internal _validateAccessCriteria function for testing */ function exposed_validateAccessCriteria( @@ -78,8 +78,8 @@ contract LM_PC_FundingPot_v1_Exposed is LM_PC_FundingPot_v1 { } /** -======= ->>>>>>> bec7682a (test: improve test coverage) + * ======= + * >>>>>>> bec7682a (test: improve test coverage) * @notice Exposes the internal _checkAccessCriteriaEligibility function for testing */ function exposed_checkAccessCriteriaEligibility( @@ -94,7 +94,7 @@ contract LM_PC_FundingPot_v1_Exposed is LM_PC_FundingPot_v1 { } /** -<<<<<<< HEAD + * <<<<<<< HEAD * @notice Exposes the internal _checkNftOwnership function for testing */ function exposed_checkNftOwnership(address nftContract_, address user_) @@ -118,8 +118,8 @@ contract LM_PC_FundingPot_v1_Exposed is LM_PC_FundingPot_v1 { } /** -======= ->>>>>>> bec7682a (test: improve test coverage) + * ======= + * >>>>>>> bec7682a (test: improve test coverage) * @notice Exposes the internal _calculateUnusedCapacityFromPreviousRounds function for testing */ function exposed_calculateUnusedCapacityFromPreviousRounds(uint32 roundId_) @@ -131,7 +131,6 @@ contract LM_PC_FundingPot_v1_Exposed is LM_PC_FundingPot_v1 { } /** -<<<<<<< HEAD * @notice Exposes the internal _closeRound function for testing */ function exposed_closeRound(uint32 roundId_) external { @@ -139,8 +138,6 @@ contract LM_PC_FundingPot_v1_Exposed is LM_PC_FundingPot_v1 { } /** -======= ->>>>>>> bec7682a (test: improve test coverage) * @notice Exposes the internal _checkRoundClosureConditions function for testing */ function exposed_checkRoundClosureConditions(uint32 roundId_) From 707fcbba635fa4956dd30afac2e7d1b861654726 Mon Sep 17 00:00:00 2001 From: JeffreyJoel Date: Fri, 25 Apr 2025 04:16:27 +0100 Subject: [PATCH 098/130] chore: rebase contribute-close onto dev --- test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol b/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol index 1266af794..9373e0273 100644 --- a/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol +++ b/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol @@ -26,6 +26,11 @@ import { MockFailingHookContract } from "test/utils/mocks/modules/logicModules/LM_PC_FundingPot_v1Mock.sol"; +import {LM_PC_FundingPot_v1ERC20Mock} from + "test/utils/mocks/modules/logicModules/LM_PC_FundingPot_v1ERC20Mock.sol"; +import {IBondingCurveBase_v1} from + "@fm/bondingCurve/interfaces/IBondingCurveBase_v1.sol"; + // System under Test (SuT) import {LM_PC_FundingPot_v1_Exposed} from "test/modules/logicModule/LM_PC_FundingPot_v1_Exposed.sol"; From 5f8e127453f2531e89d66b52fee75167d21b4794 Mon Sep 17 00:00:00 2001 From: JeffreyJoel Date: Wed, 30 Apr 2025 11:07:27 +0100 Subject: [PATCH 099/130] fix: fix rebase issues --- test/e2e/logicModule/FundingPotE2E.t.sol | 12 ++- .../logicModule/LM_PC_FundingPot_v1.t.sol | 101 ++++++------------ 2 files changed, 39 insertions(+), 74 deletions(-) diff --git a/test/e2e/logicModule/FundingPotE2E.t.sol b/test/e2e/logicModule/FundingPotE2E.t.sol index 115927724..895caf249 100644 --- a/test/e2e/logicModule/FundingPotE2E.t.sol +++ b/test/e2e/logicModule/FundingPotE2E.t.sol @@ -257,17 +257,23 @@ contract FundingPotE2E is E2ETest { vm.startPrank(contributor1); contributionToken.approve(address(fundingPot), 400e18); - fundingPot.contributeToRound(round1Id, 400e18, 0, new bytes32[](0)); + fundingPot.contributeToRoundFor( + contributor1, round1Id, 400e18, 0, new bytes32[](0) + ); vm.stopPrank(); vm.startPrank(contributor2); contributionToken.approve(address(fundingPot), 600e18); - fundingPot.contributeToRound(round1Id, 600e18, 0, new bytes32[](0)); + fundingPot.contributeToRoundFor( + contributor2, round1Id, 600e18, 0, new bytes32[](0) + ); vm.stopPrank(); vm.startPrank(contributor3); contributionToken.approve(address(fundingPot), 750e18); - fundingPot.contributeToRound(round2Id, 750e18, 0, new bytes32[](0)); + fundingPot.contributeToRoundFor( + contributor3, round2Id, 750e18, 0, new bytes32[](0) + ); vm.stopPrank(); // 7. Fast forward to after rounds end diff --git a/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol b/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol index 9373e0273..af4e2963b 100644 --- a/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol +++ b/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol @@ -18,16 +18,13 @@ import { ERC20PaymentClientBaseV2Mock, ERC20Mock } from "test/utils/mocks/modules/paymentClient/ERC20PaymentClientBaseV2Mock.sol"; -import {IBondingCurveBase_v1} from - "@fm/bondingCurve/interfaces/IBondingCurveBase_v1.sol"; + import { ERC721Mock, MockHookContract, MockFailingHookContract } from "test/utils/mocks/modules/logicModules/LM_PC_FundingPot_v1Mock.sol"; -import {LM_PC_FundingPot_v1ERC20Mock} from - "test/utils/mocks/modules/logicModules/LM_PC_FundingPot_v1ERC20Mock.sol"; import {IBondingCurveBase_v1} from "@fm/bondingCurve/interfaces/IBondingCurveBase_v1.sol"; @@ -1466,9 +1463,9 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { ) public { testCreateRound(); - vm.assume(amount > 0 && amount <= 500); - vm.assume(roundStart > block.timestamp && roundStart < roundEnd); - vm.assume(roundCap > 0); + // vm.assume(amount > 0 && amount <= 500); + // vm.assume(roundStart > block.timestamp && roundStart < roundEnd); + // vm.assume(roundCap > 0); uint32 roundId = fundingPot.getRoundCount(); uint8 accessId = 1; @@ -1586,7 +1583,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { testCreateRound(); uint32 roundId = fundingPot.getRoundCount(); - uint8 accessId = 3; + uint8 accessId = uint8(ILM_PC_FundingPot_v1.AccessCriteriaType.MERKLE); uint amount = 250; (,,,, bytes32[] memory proofB) = _helper_generateMerkleTreeForTwoLeaves( @@ -1623,7 +1620,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { vm.prank(contributor1_); fundingPot.contributeToRoundFor( - contributor1_, roundId, amount, accessId, PROOF + contributor1_, roundId, amount, accessId, proofB ); } @@ -1844,20 +1841,9 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { accessCriteriaEnumNew != accessCriteriaEnumOld && accessCriteriaEnumNew >= 0 && accessCriteriaEnumNew <= 4 ); - + uint8 accessId = uint8(ILM_PC_FundingPot_v1.AccessCriteriaType.NFT); _helper_setupRoundWithAccessCriteria(accessId); uint32 roundId = fundingPot.getRoundCount(); - uint amount = 201; - - ( - address nftContract, - bytes32 merkleRoot, - address[] memory allowedAddresses - ) = _helper_createAccessCriteria(accessId, roundId); - - fundingPot.setAccessCriteriaForRound( - roundId, accessId, nftContract, merkleRoot, allowedAddresses - ); mockNFTContract.mint(contributor1_); @@ -1873,7 +1859,9 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { vm.expectEmit(true, false, false, true); emit ILM_PC_FundingPot_v1.ContributionMade(roundId, contributor1_, 250); - fundingPot.contributeToRound(roundId, 250, accessId, new bytes32[](0)); + fundingPot.contributeToRoundFor( + contributor1_, roundId, 250, accessId, new bytes32[](0) + ); vm.stopPrank(); uint userContribution = fundingPot.exposed_getUserContributionToRound( @@ -1886,31 +1874,17 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { assertEq(totalContributions, 250); } - function testContributeToRound_worksGivenMerkleAccessCriteriaMet() public { + function testContributeToRoundFor_worksGivenMerkleAccessCriteriaMet() + public + { uint8 accessId = uint8(ILM_PC_FundingPot_v1.AccessCriteriaType.MERKLE); - uint32 roundId = fundingPot.createRound( - _defaultRoundParams.roundStart, - _defaultRoundParams.roundEnd, - _defaultRoundParams.roundCap, - _defaultRoundParams.hookContract, - _defaultRoundParams.hookFunction, - _defaultRoundParams.autoClosure, - _defaultRoundParams.globalAccumulativeCaps - ); - - (,,,, bytes32[] memory proofB) = _helper_generateMerkleTreeForTwoLeaves( - contributor1_, contributor2_, roundId - ); + _helper_setupRoundWithAccessCriteria(accessId); - ( - address nftContract, - bytes32 merkleRoot, - address[] memory allowedAddresses - ) = _helper_createAccessCriteria(accessId, roundId); + uint32 roundId = fundingPot.getRoundCount(); - fundingPot.setAccessCriteriaForRound( - roundId, accessId, nftContract, merkleRoot, allowedAddresses + _helper_generateMerkleTreeForTwoLeaves( + contributor1_, contributor2_, roundId ); fundingPot.setAccessCriteriaPrivileges( @@ -1930,42 +1904,23 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { vm.prank(contributor1_); fundingPot.contributeToRoundFor( - contributor1_, roundId, amount, accessId, new bytes32[](0) - ); - - ( - address nftContract, - bytes32 merkleRoot, - address[] memory allowedAddresses - ) = _helper_createAccessCriteria(accessId, roundId); - - fundingPot.setAccessCriteriaForRound( - roundId, accessId, nftContract, merkleRoot, allowedAddresses - ); - - fundingPot.setAccessCriteriaPrivileges( - roundId, accessId, 500, false, 0, 0, 0 + contributor1_, + roundId, + contributionAmount, + accessId, + new bytes32[](0) ); - vm.warp(_defaultRoundParams.roundStart + 1); - - vm.startPrank(contributor2_); - _token.approve(address(fundingPot), 250); - - vm.expectEmit(true, false, false, true); - emit ILM_PC_FundingPot_v1.ContributionMade(roundId, contributor2_, 250); - - fundingPot.contributeToRound(roundId, 250, accessId, new bytes32[](0)); vm.stopPrank(); uint userContribution = fundingPot.exposed_getUserContributionToRound( roundId, contributor2_ ); - assertEq(userContribution, 250); + assertEq(userContribution, contributionAmount); uint totalContributions = fundingPot.exposed_getTotalRoundContributions(roundId); - assertEq(totalContributions, 250); + assertEq(totalContributions, contributionAmount); } function testContributeToRound_worksGivenUserCurrentContributionExceedsTheRoundCap( @@ -2007,12 +1962,16 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { vm.startPrank(contributor1_); _token.approve(address(fundingPot), 100); - fundingPot.contributeToRound(roundId, 100, accessId, new bytes32[](0)); + fundingPot.contributeToRoundFor( + contributor1_, roundId, 100, accessId, new bytes32[](0) + ); vm.stopPrank(); vm.startPrank(contributor2_); _token.approve(address(fundingPot), 100); - fundingPot.contributeToRound(roundId, 100, accessId, new bytes32[](0)); + fundingPot.contributeToRoundFor( + contributor2_, roundId, 100, accessId, new bytes32[](0) + ); vm.stopPrank(); uint contribution = fundingPot.exposed_getUserContributionToRound( From 1218cec5a4ea8d12be2607082d48e177d5a7c3be Mon Sep 17 00:00:00 2001 From: JeffreyJoel Date: Wed, 30 Apr 2025 11:27:26 +0100 Subject: [PATCH 100/130] fix: fix failing test --- .../logicModule/LM_PC_FundingPot_v1.t.sol | 20 +++++++------------ 1 file changed, 7 insertions(+), 13 deletions(-) diff --git a/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol b/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol index af4e2963b..f490b7df1 100644 --- a/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol +++ b/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol @@ -1883,7 +1883,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { uint32 roundId = fundingPot.getRoundCount(); - _helper_generateMerkleTreeForTwoLeaves( + (,,,, bytes32[] memory proofB) = _helper_generateMerkleTreeForTwoLeaves( contributor1_, contributor2_, roundId ); @@ -1901,14 +1901,8 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { emit ILM_PC_FundingPot_v1.ContributionMade( roundId, contributor2_, contributionAmount ); - - vm.prank(contributor1_); fundingPot.contributeToRoundFor( - contributor1_, - roundId, - contributionAmount, - accessId, - new bytes32[](0) + contributor2_, roundId, contributionAmount, accessId, proofB ); vm.stopPrank(); @@ -1923,7 +1917,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { assertEq(totalContributions, contributionAmount); } - function testContributeToRound_worksGivenUserCurrentContributionExceedsTheRoundCap( + function testContributeToRoundFor_worksGivenUserCurrentContributionExceedsTheRoundCap( ) public { _defaultRoundParams.roundCap = 150; _defaultRoundParams.autoClosure = true; @@ -1985,7 +1979,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { assertTrue(fundingPot.isRoundClosed(roundId)); } - function testcontributeToRoundFor_worksGivenContributionPartiallyExceedingPersonalCap( + function testContributeToRoundFor_worksGivenContributionPartiallyExceedingPersonalCap( ) public { testCreateRound(); @@ -2048,7 +2042,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { assertEq(totalContribution, personalCap); } - function testcontributeToRoundFor_worksGivenUserCanOverrideTimeConstraints() + function testContributeToRoundFor_worksGivenUserCanOverrideTimeConstraints() public { testCreateRound(); @@ -2100,7 +2094,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { assertEq(totalContribution, amount); } - function testcontributeToRoundFor_worksGivenPersonalCapAccumulation() + function testContributeToRoundFor_worksGivenPersonalCapAccumulation() public { _defaultRoundParams.globalAccumulativeCaps = true; @@ -2204,7 +2198,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { ); } - function testcontributeToRoundFor_worksGivenTotalRoundCapAccumulation() + function testContributeToRoundFor_worksGivenTotalRoundCapAccumulation() public { _defaultRoundParams.globalAccumulativeCaps = true; From d6daec1fc5d7e717668311eff292e140b72647ac Mon Sep 17 00:00:00 2001 From: JeffreyJoel Date: Thu, 1 May 2025 08:06:19 +0100 Subject: [PATCH 101/130] tests: improve test coverage --- .../logicModule/LM_PC_FundingPot_v1.sol | 1 - .../logicModule/LM_PC_FundingPot_v1.t.sol | 197 ++++++++++++++++-- .../LM_PC_FundingPot_v1_Exposed.sol | 18 ++ 3 files changed, 200 insertions(+), 16 deletions(-) diff --git a/src/modules/logicModule/LM_PC_FundingPot_v1.sol b/src/modules/logicModule/LM_PC_FundingPot_v1.sol index d1e2b1024..ed0df0624 100644 --- a/src/modules/logicModule/LM_PC_FundingPot_v1.sol +++ b/src/modules/logicModule/LM_PC_FundingPot_v1.sol @@ -1001,7 +1001,6 @@ contract LM_PC_FundingPot_v1 is view returns (uint unusedCapacityFromPrevious) { - unusedCapacityFromPrevious = 0; // Iterate through all previous rounds (1 to roundId_-1) for (uint32 i = 1; i < roundId_; ++i) { Round storage prevRound = rounds[i]; diff --git a/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol b/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol index f490b7df1..11edd8803 100644 --- a/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol +++ b/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol @@ -81,6 +81,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { } ERC721Mock mockNFTContract = new ERC721Mock("NFT Mock", "NFT"); + MockFailingHookContract failingHook = new MockFailingHookContract(); // ------------------------------------------------------------------------- // Setup @@ -1459,7 +1460,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { └── Then the transaction should revert */ - function testcontributeToRoundFor_revertsGivenContributionIsBeforeRoundStart( + function testContributeToRoundFor_revertsGivenContributionIsBeforeRoundStart( ) public { testCreateRound(); @@ -1502,7 +1503,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { ); } - function testcontributeToRoundFor_revertsGivenContributionIsAfterRoundEnd() + function testContributeToRoundFor_revertsGivenContributionIsAfterRoundEnd() public { testCreateRound(); @@ -1545,7 +1546,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { ); } - function testcontributeToRoundFor_revertsGivenNFTAccessCriteriaIsNotMet() + function testContributeToRoundFor_revertsGivenNFTAccessCriteriaIsNotMet() public { uint8 accessId = uint8(ILM_PC_FundingPot_v1.AccessCriteriaType.NFT); @@ -1578,7 +1579,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { ); } - function testcontributeToRoundFor_revertsGivenMerkleRootAccessCriteriaIsNotMet( + function testContributeToRoundFor_revertsGivenMerkleRootAccessCriteriaIsNotMet( ) public { testCreateRound(); @@ -1624,12 +1625,12 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { ); } - function testcontributeToRoundFor_revertsGivenAllowedListAccessCriteriaIsNotMet( + function testContributeToRoundFor_revertsGivenAllowedListAccessCriteriaIsNotMet( ) public { testCreateRound(); uint32 roundId = fundingPot.getRoundCount(); - uint8 accessId = 4; + uint8 accessId = uint8(ILM_PC_FundingPot_v1.AccessCriteriaType.LIST); uint amount = 250; ( @@ -1666,12 +1667,12 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { ); } - function testcontributeToRoundFor_revertsGivenPreviousContributionExceedsPersonalCap( + function testContributeToRoundFor_revertsGivenPreviousContributionExceedsPersonalCap( ) public { testCreateRound(); uint32 roundId = fundingPot.getRoundCount(); - uint8 accessId = 1; + uint8 accessId = uint8(ILM_PC_FundingPot_v1.AccessCriteriaType.NFT); uint amount = 500; ( @@ -1984,7 +1985,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { testCreateRound(); uint32 roundId = fundingPot.getRoundCount(); - uint8 accessId = 1; + uint8 accessId = uint8(ILM_PC_FundingPot_v1.AccessCriteriaType.NFT); uint firstAmount = 400; uint personalCap = 500; @@ -2048,7 +2049,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { testCreateRound(); uint32 roundId = fundingPot.getRoundCount(); - uint8 accessId = 1; + uint8 accessId = uint8(ILM_PC_FundingPot_v1.AccessCriteriaType.NFT); uint amount = 250; ( @@ -2110,7 +2111,8 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { uint32 round1Id = fundingPot.getRoundCount(); - uint8 accessCriteriaId = 1; + uint8 accessCriteriaId = + uint8(ILM_PC_FundingPot_v1.AccessCriteriaType.NFT); ( address nftContract, bytes32 merkleRoot, @@ -2142,7 +2144,11 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { uint32 round2Id = fundingPot.getRoundCount(); fundingPot.setAccessCriteriaForRound( - round2Id, accessCriteriaId, address(0), bytes32(0), allowedAddresses + round2Id, + accessCriteriaId, + nftContract, + merkleRoot, + allowedAddresses ); // Set personal cap of 400 for round 2 @@ -2324,6 +2330,15 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { │ └── When user attempts to close the round │ └── Then it should revert with Module__LM_PC_FundingPot__RoundNotCreated │ + ├── Given hook execution fails + │ └── When user attempts to close the round + │ └── Then it should revert with Module__LM_PC_FundingPot__HookExecutionFailed + │ + ├── Given closure conditions are not met + │ └── When user attempts to close the round + │ └── Then it should revert with Module__LM_PC_FundingPot__ClosureConditionsNotMet + │ + ├── Given round has started but not ended ├── Given round is already closed │ └── When user attempts to close the round again │ └── Then it should revert with Module__LM_PC_FundingPot__RoundHasEnded @@ -2377,11 +2392,117 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { vm.stopPrank(); } + function testFuzzCloseRound_revertsGivenRoundDoesNotExist( + uint8 accessCriteriaEnum + ) public { + vm.assume(accessCriteriaEnum >= 0 && accessCriteriaEnum <= 4); + + uint32 roundId = fundingPot.getRoundCount(); + + ( + address nftContract, + bytes32 merkleRoot, + address[] memory allowedAddresses + ) = _helper_createAccessCriteria(accessCriteriaEnum, roundId); + + vm.expectRevert( + abi.encodeWithSelector( + ILM_PC_FundingPot_v1 + .Module__LM_PC_FundingPot__RoundNotCreated + .selector + ) + ); + fundingPot.closeRound(roundId); + } + + function testCloseRound_revertsGivenHookExecutionFails() public { + uint8 accessCriteriaId = + uint8(ILM_PC_FundingPot_v1.AccessCriteriaType.OPEN); + + uint32 roundId = fundingPot.createRound( + _defaultRoundParams.roundStart, + _defaultRoundParams.roundEnd, + _defaultRoundParams.roundCap, + address(failingHook), + abi.encodeWithSignature("executeHook()"), + _defaultRoundParams.autoClosure, + _defaultRoundParams.globalAccumulativeCaps + ); + + ( + address nftContract, + bytes32 merkleRoot, + address[] memory allowedAddresses + ) = _helper_createAccessCriteria(accessCriteriaId, roundId); + + fundingPot.setAccessCriteriaForRound( + roundId, accessCriteriaId, nftContract, merkleRoot, allowedAddresses + ); + + fundingPot.setAccessCriteriaPrivileges(roundId, 0, 1000, false, 0, 0, 0); + + vm.warp(_defaultRoundParams.roundEnd + 1); + + vm.expectRevert( + ILM_PC_FundingPot_v1 + .Module__LM_PC_FundingPot__HookExecutionFailed + .selector + ); + fundingPot.closeRound(roundId); + } + + function testCloseRound_revertsGivenClosureConditionsNotMet() public { + uint8 accessCriteriaId = + uint8(ILM_PC_FundingPot_v1.AccessCriteriaType.OPEN); + + _helper_setupRoundWithAccessCriteria(accessCriteriaId); + uint32 roundId = fundingPot.getRoundCount(); + + fundingPot.setAccessCriteriaPrivileges(roundId, 0, 1000, false, 0, 0, 0); + + vm.expectRevert( + ILM_PC_FundingPot_v1 + .Module__LM_PC_FundingPot__ClosureConditionsNotMet + .selector + ); + fundingPot.closeRound(roundId); + } + + function testCloseRound_revertsGivenRoundHasAlreadyBeenClosed() public { + uint8 accessCriteriaId = + uint8(ILM_PC_FundingPot_v1.AccessCriteriaType.OPEN); + + _helper_setupRoundWithAccessCriteria(accessCriteriaId); + uint32 roundId = fundingPot.getRoundCount(); + fundingPot.setAccessCriteriaPrivileges( + roundId, accessCriteriaId, 1000, false, 0, 0, 0 + ); + + (uint roundStart,,,,,,) = fundingPot.getRoundGenericParameters(roundId); + + vm.warp(roundStart + 1); + + vm.startPrank(contributor1_); + _token.approve(address(fundingPot), 1000); + fundingPot.contributeToRoundFor( + contributor1_, roundId, 1000, accessCriteriaId, new bytes32[](0) + ); + vm.stopPrank(); + + fundingPot.closeRound(roundId); + vm.expectRevert( + ILM_PC_FundingPot_v1 + .Module__LM_PC_FundingPot__RoundHasEnded + .selector + ); + fundingPot.closeRound(roundId); + } + function testCloseRound_worksGivenRoundHasStartedButNotEnded() public { testCreateRound(); uint32 roundId = fundingPot.getRoundCount(); - uint8 accessId = 1; + uint8 accessId = uint8(ILM_PC_FundingPot_v1.AccessCriteriaType.OPEN); ( address nftContract, bytes32 merkleRoot, @@ -2826,6 +2947,39 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { assertEq(actualUnusedCapacity, 1000); } + // ------------------------------------------------------------------------- + // Test: _contributeToRoundFor() + + function testFuzz_contributeToRoundFor_revertsGivenInvalidAccessCriteria( + uint8 accessCriteriaEnum + ) public { + vm.assume(accessCriteriaEnum > 4); + + testCreateRound(); + uint32 roundId = fundingPot.getRoundCount(); + (uint roundStart,,,,,,) = fundingPot.getRoundGenericParameters(roundId); + vm.warp(roundStart + 1); + + vm.prank(contributor1_); + _token.approve(address(fundingPot), 1000); + + vm.expectRevert( + abi.encodeWithSelector( + ILM_PC_FundingPot_v1 + .Module__LM_PC_FundingPot__InvalidAccessCriteriaId + .selector + ) + ); + + fundingPot.exposed_contributeToRoundFor( + contributor1_, + roundId, + 1000, + accessCriteriaEnum, + new bytes32[](0), + 0 + ); + } // ------------------------------------------------------------------------- // Test: _checkRoundClosureConditions @@ -2909,7 +3063,6 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { params.globalAccumulativeCaps ); - // Time is before end and no contributions assertFalse(fundingPot.exposed_checkRoundClosureConditions(roundId)); } @@ -2992,7 +3145,6 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { ); } - // Test exposed internal function closeRound function test_closeRound_worksGivenCapReached() public { testCreateRound(); @@ -3042,6 +3194,21 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { assertTrue(fundingPot.isRoundClosed(roundId)); } + // ------------------------------------------------------------------------- + // Test: _buyBondingCurveToken + function test_buyBondingCurveToken_revertsGivenNoContributions() public { + testCreateRound(); + uint32 roundId = fundingPot.getRoundCount(); + (uint roundStart,,,,,,) = fundingPot.getRoundGenericParameters(roundId); + vm.warp(roundStart + 1); + + vm.expectRevert( + ILM_PC_FundingPot_v1 + .Module__LM_PC_FundingPot__NoContributions + .selector + ); + fundingPot.exposed_buyBondingCurveToken(roundId); + } // ------------------------------------------------------------------------- // Helper Functions diff --git a/test/unit/modules/logicModule/LM_PC_FundingPot_v1_Exposed.sol b/test/unit/modules/logicModule/LM_PC_FundingPot_v1_Exposed.sol index bf8abbb1e..cb430ac4a 100644 --- a/test/unit/modules/logicModule/LM_PC_FundingPot_v1_Exposed.sol +++ b/test/unit/modules/logicModule/LM_PC_FundingPot_v1_Exposed.sol @@ -130,6 +130,24 @@ contract LM_PC_FundingPot_v1_Exposed is LM_PC_FundingPot_v1 { return _calculateUnusedCapacityFromPreviousRounds(roundId_); } + function exposed_contributeToRoundFor( + address user_, + uint32 roundId_, + uint amount_, + uint8 accessCriteriaId__, + bytes32[] memory merkleProof_, + uint unspentPersonalCap_ + ) external { + _contributeToRoundFor( + user_, + roundId_, + amount_, + accessCriteriaId__, + merkleProof_, + unspentPersonalCap_ + ); + } + /** * @notice Exposes the internal _closeRound function for testing */ From a9d9a7ff1f45bf00684a91a3c73983e34ae616b6 Mon Sep 17 00:00:00 2001 From: Zuhaib Mohammed Date: Thu, 1 May 2025 15:45:25 +0530 Subject: [PATCH 102/130] chore: code format --- src/modules/logicModule/LM_PC_FundingPot_v1.sol | 6 +++--- test/e2e/logicModule/FundingPotE2E.t.sol | 8 ++------ 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/src/modules/logicModule/LM_PC_FundingPot_v1.sol b/src/modules/logicModule/LM_PC_FundingPot_v1.sol index ed0df0624..a512883b4 100644 --- a/src/modules/logicModule/LM_PC_FundingPot_v1.sol +++ b/src/modules/logicModule/LM_PC_FundingPot_v1.sol @@ -1193,19 +1193,19 @@ contract LM_PC_FundingPot_v1 is uint8 flagCount = 0; if (start_ > 0) { - flags |= bytes32(uint(1) << 1); + flags |= bytes32(uint(1) << FLAG_START); data[flagCount] = bytes32(start_); flagCount++; } if (cliff_ > 0) { - flags |= bytes32(uint(1) << 2); + flags |= bytes32(uint(1) << FLAG_CLIFF); data[flagCount] = bytes32(cliff_); flagCount++; } if (end_ > 0) { - flags |= bytes32(uint(1) << 3); + flags |= bytes32(uint(1) << FLAG_END); data[flagCount] = bytes32(end_); flagCount++; } diff --git a/test/e2e/logicModule/FundingPotE2E.t.sol b/test/e2e/logicModule/FundingPotE2E.t.sol index 895caf249..033b8990a 100644 --- a/test/e2e/logicModule/FundingPotE2E.t.sol +++ b/test/e2e/logicModule/FundingPotE2E.t.sol @@ -55,10 +55,6 @@ contract FundingPotE2E is E2ETest { ERC20Mock contributionToken; function setUp() public override { - // vm.label({ - // account: address(contributionToken), - // newLabel: ERC20Mock(address(contributionToken)).symbol() - // }); // Setup common E2E framework super.setUp(); @@ -251,8 +247,8 @@ contract FundingPotE2E is E2ETest { vm.warp(block.timestamp + 1 days); // 6. Fund contributors and contribute to rounds - contributionToken.mint(contributor1, 500e18); - contributionToken.mint(contributor2, 500e18); + contributionToken.mint(contributor1, 1000e18); + contributionToken.mint(contributor2, 1000e18); contributionToken.mint(contributor3, 1000e18); vm.startPrank(contributor1); From b419bc863f117199019d16a4371ae3b507019af4 Mon Sep 17 00:00:00 2001 From: Zuhaib Mohammed Date: Thu, 1 May 2025 15:53:22 +0530 Subject: [PATCH 103/130] chore: code format & fix minor bugs --- test/e2e/logicModule/FundingPotE2E.t.sol | 2 -- test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol | 2 -- .../modules/logicModule/LM_PC_FundingPot_v1_Exposed.sol | 6 ------ 3 files changed, 10 deletions(-) diff --git a/test/e2e/logicModule/FundingPotE2E.t.sol b/test/e2e/logicModule/FundingPotE2E.t.sol index 033b8990a..e7cb04eb3 100644 --- a/test/e2e/logicModule/FundingPotE2E.t.sol +++ b/test/e2e/logicModule/FundingPotE2E.t.sol @@ -32,8 +32,6 @@ import {ERC20Mock} from "test/utils/mocks/ERC20Mock.sol"; import {SafeERC20} from "@oz/token/ERC20/utils/SafeERC20.sol"; import {ERC20Issuance_v1} from "@ex/token/ERC20Issuance_v1.sol"; -import {console2} from "forge-std/console2.sol"; - contract FundingPotE2E is E2ETest { // Module Configurations for the current E2E test. Should be filled during setUp() call. IOrchestratorFactory_v1.ModuleConfig[] moduleConfigurations; diff --git a/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol b/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol index 11edd8803..2e3e9845d 100644 --- a/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol +++ b/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol @@ -34,7 +34,6 @@ import {LM_PC_FundingPot_v1_Exposed} from import {ILM_PC_FundingPot_v1} from "src/modules/logicModule/interfaces/ILM_PC_FundingPot_v1.sol"; -import {console2} from "forge-std/console2.sol"; /** * @title Inverter Funding Pot Logic Module Tests * @@ -48,7 +47,6 @@ import {console2} from "forge-std/console2.sol"; * * @author Inverter Network */ - contract LM_PC_FundingPot_v1_Test is ModuleTest { // ------------------------------------------------------------------------- // Constants diff --git a/test/unit/modules/logicModule/LM_PC_FundingPot_v1_Exposed.sol b/test/unit/modules/logicModule/LM_PC_FundingPot_v1_Exposed.sol index cb430ac4a..a7cc8d29b 100644 --- a/test/unit/modules/logicModule/LM_PC_FundingPot_v1_Exposed.sol +++ b/test/unit/modules/logicModule/LM_PC_FundingPot_v1_Exposed.sol @@ -65,7 +65,6 @@ contract LM_PC_FundingPot_v1_Exposed is LM_PC_FundingPot_v1 { } /** - * <<<<<<< HEAD * @notice Exposes the internal _validateAccessCriteria function for testing */ function exposed_validateAccessCriteria( @@ -78,8 +77,6 @@ contract LM_PC_FundingPot_v1_Exposed is LM_PC_FundingPot_v1 { } /** - * ======= - * >>>>>>> bec7682a (test: improve test coverage) * @notice Exposes the internal _checkAccessCriteriaEligibility function for testing */ function exposed_checkAccessCriteriaEligibility( @@ -94,7 +91,6 @@ contract LM_PC_FundingPot_v1_Exposed is LM_PC_FundingPot_v1 { } /** - * <<<<<<< HEAD * @notice Exposes the internal _checkNftOwnership function for testing */ function exposed_checkNftOwnership(address nftContract_, address user_) @@ -118,8 +114,6 @@ contract LM_PC_FundingPot_v1_Exposed is LM_PC_FundingPot_v1 { } /** - * ======= - * >>>>>>> bec7682a (test: improve test coverage) * @notice Exposes the internal _calculateUnusedCapacityFromPreviousRounds function for testing */ function exposed_calculateUnusedCapacityFromPreviousRounds(uint32 roundId_) From 8ac8cb9690bd51ef9a5b7b35b13571828bb23464 Mon Sep 17 00:00:00 2001 From: Zuhaib Mohammed Date: Mon, 5 May 2025 16:36:23 +0530 Subject: [PATCH 104/130] chore: Remove stale/irrelevant values in set/edit roundAccessCriteria --- .../logicModule/LM_PC_FundingPot_v1.sol | 93 ++++---- .../interfaces/ILM_PC_FundingPot_v1.sol | 17 +- test/e2e/logicModule/FundingPotE2E.t.sol | 4 +- .../logicModule/LM_PC_FundingPot_v1.t.sol | 214 +++++------------- 4 files changed, 99 insertions(+), 229 deletions(-) diff --git a/src/modules/logicModule/LM_PC_FundingPot_v1.sol b/src/modules/logicModule/LM_PC_FundingPot_v1.sol index a512883b4..58a80f14d 100644 --- a/src/modules/logicModule/LM_PC_FundingPot_v1.sol +++ b/src/modules/logicModule/LM_PC_FundingPot_v1.sol @@ -429,7 +429,7 @@ contract LM_PC_FundingPot_v1 is } /// @inheritdoc ILM_PC_FundingPot_v1 - function setAccessCriteriaForRound( + function setAccessCriteria( uint32 roundId_, uint8 accessCriteriaId_, address nftContract_, @@ -444,68 +444,61 @@ contract LM_PC_FundingPot_v1 is _validateEditRoundParameters(round); - if ( - ( - accessCriteriaId_ == uint8(AccessCriteriaType.NFT) - && nftContract_ == address(0) - ) - || ( - accessCriteriaId_ == uint8(AccessCriteriaType.MERKLE) - && merkleRoot_ == bytes32("") - ) - || ( - accessCriteriaId_ == uint8(AccessCriteriaType.LIST) - && allowedAddresses_.length == 0 - ) - ) { - revert Module__LM_PC_FundingPot__MissingRequiredAccessCriteriaData(); - } + // Check if this is a new setting or an edit + bool isEdit = round.accessCriterias[accessCriteriaId_] + .accessCriteriaType != AccessCriteriaType.UNSET; + // Validate required data based on access criteria type AccessCriteriaType accessCriteriaType = AccessCriteriaType(accessCriteriaId_); - round.accessCriterias[accessCriteriaId_].accessCriteriaType = - accessCriteriaType; - round.accessCriterias[accessCriteriaId_].nftContract = nftContract_; - round.accessCriterias[accessCriteriaId_].merkleRoot = merkleRoot_; - - for (uint i = 0; i < allowedAddresses_.length; i++) { - round.accessCriterias[accessCriteriaId_].allowedAddresses[allowedAddresses_[i]] - = true; - } - - emit AccessCriteriaSet(roundId_, accessCriteriaId_); - } - - /// @inheritdoc ILM_PC_FundingPot_v1 - function editAccessCriteriaForRound( - uint32 roundId_, - uint8 accessCriteriaId_, - address nftContract_, - bytes32 merkleRoot_, - address[] calldata allowedAddresses_ - ) external onlyModuleRole(FUNDING_POT_ADMIN_ROLE) { - Round storage round = rounds[roundId_]; - if (accessCriteriaId_ > MAX_ACCESS_CRITERIA_ID) { - revert Module__LM_PC_FundingPot__InvalidAccessCriteriaId(); + if (accessCriteriaType == AccessCriteriaType.NFT) { + if (nftContract_ == address(0)) { + revert + Module__LM_PC_FundingPot__MissingRequiredAccessCriteriaData(); + } + } else if (accessCriteriaType == AccessCriteriaType.MERKLE) { + if (merkleRoot_ == bytes32(0)) { + revert + Module__LM_PC_FundingPot__MissingRequiredAccessCriteriaData(); + } + } else if (accessCriteriaType == AccessCriteriaType.LIST) { + if (allowedAddresses_.length == 0) { + revert + Module__LM_PC_FundingPot__MissingRequiredAccessCriteriaData(); + } } - _validateEditRoundParameters(round); + // Clear all existing data to prevent stale data + round.accessCriterias[accessCriteriaId_].nftContract = address(0); + round.accessCriterias[accessCriteriaId_].merkleRoot = bytes32(0); + // @note: When changing allowlists, call removeAllowlistedAddresses first to clear previous entries - AccessCriteriaType accessCriteriaType = - AccessCriteriaType(accessCriteriaId_); + // Set the access criteria type round.accessCriterias[accessCriteriaId_].accessCriteriaType = accessCriteriaType; - round.accessCriterias[accessCriteriaId_].nftContract = nftContract_; - round.accessCriterias[accessCriteriaId_].merkleRoot = merkleRoot_; - for (uint i = 0; i < allowedAddresses_.length; i++) { - round.accessCriterias[accessCriteriaId_].allowedAddresses[allowedAddresses_[i]] - = true; + // Set only the relevant data based on the access criteria type + if (accessCriteriaType == AccessCriteriaType.NFT) { + round.accessCriterias[accessCriteriaId_].nftContract = nftContract_; + } else if (accessCriteriaType == AccessCriteriaType.MERKLE) { + round.accessCriterias[accessCriteriaId_].merkleRoot = merkleRoot_; + } else if (accessCriteriaType == AccessCriteriaType.LIST) { + // For LIST type, update the allowed addresses + for (uint i = 0; i < allowedAddresses_.length; i++) { + round.accessCriterias[accessCriteriaId_].allowedAddresses[allowedAddresses_[i]] + = true; + } } - emit AccessCriteriaEdited(roundId_, accessCriteriaId_); + // Emit the appropriate event based on whether this is a new setting or an edit + if (isEdit) { + emit AccessCriteriaEdited(roundId_, accessCriteriaId_); + } else { + emit AccessCriteriaSet(roundId_, accessCriteriaId_); + } } + /// @inheritdoc ILM_PC_FundingPot_v1 function removeAllowlistedAddresses( uint32 roundId_, uint8 accessCriteriaId_, diff --git a/src/modules/logicModule/interfaces/ILM_PC_FundingPot_v1.sol b/src/modules/logicModule/interfaces/ILM_PC_FundingPot_v1.sol index 76ca4ad48..dabfdd89b 100644 --- a/src/modules/logicModule/interfaces/ILM_PC_FundingPot_v1.sol +++ b/src/modules/logicModule/interfaces/ILM_PC_FundingPot_v1.sol @@ -442,22 +442,7 @@ interface ILM_PC_FundingPot_v1 is IERC20PaymentClientBase_v2 { /// @param nftContract_ Address of the NFT contract. /// @param merkleRoot_ Merkle root for the access criteria. /// @param allowedAddresses_ List of explicitly allowed addresses. - function setAccessCriteriaForRound( - uint32 roundId_, - uint8 accessCriteriaId_, - address nftContract_, - bytes32 merkleRoot_, - address[] memory allowedAddresses_ - ) external; - - /// @notice Edits an existing access criteria for a round. - /// @dev Only callable by funding pot admin and only before the round has started. - /// @param roundId_ ID of the round. - /// @param accessCriteriaId_ ID of the access criteria. - /// @param nftContract_ Address of the NFT contract. - /// @param merkleRoot_ Merkle root for the access criteria. - /// @param allowedAddresses_ List of explicitly allowed addresses. - function editAccessCriteriaForRound( + function setAccessCriteria( uint32 roundId_, uint8 accessCriteriaId_, address nftContract_, diff --git a/test/e2e/logicModule/FundingPotE2E.t.sol b/test/e2e/logicModule/FundingPotE2E.t.sol index e7cb04eb3..fb5e5695b 100644 --- a/test/e2e/logicModule/FundingPotE2E.t.sol +++ b/test/e2e/logicModule/FundingPotE2E.t.sol @@ -201,7 +201,7 @@ contract FundingPotE2E is E2ETest { allowedAddresses[0] = contributor1; allowedAddresses[1] = contributor2; - fundingPot.setAccessCriteriaForRound( + fundingPot.setAccessCriteria( round1Id, uint8(ILM_PC_FundingPot_v1.AccessCriteriaType.LIST), address(0), @@ -213,7 +213,7 @@ contract FundingPotE2E is E2ETest { allowedAddresses = new address[](1); allowedAddresses[0] = contributor3; - fundingPot.setAccessCriteriaForRound( + fundingPot.setAccessCriteria( round2Id, uint8(ILM_PC_FundingPot_v1.AccessCriteriaType.LIST), address(0), diff --git a/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol b/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol index 2e3e9845d..f210ab604 100644 --- a/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol +++ b/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol @@ -807,9 +807,12 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { │ └── When user attempts to set access criteria │ └── Then it should revert │ - └── Given all the valid parameters are provided - └── When user attempts to set access criteria - └── Then it should not revert + ├── Given all the valid parameters are provided + │ └── When user attempts to set access criteria + │ └── Then it should not revert + └── Given all the valid parameters and access criteria is set + └── When user attempts to edit access criteria + └── Then it should not revert */ function testFuzzSetAccessCriteria_revertsGivenUserDoesNotHaveFundingPotAdminRole( @@ -837,7 +840,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { IModule_v1.Module__CallerNotAuthorized.selector, roleId, user_ ) ); - fundingPot.setAccessCriteriaForRound( + fundingPot.setAccessCriteria( roundId, accessCriteriaEnum_, nftContract, @@ -867,7 +870,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { .selector ) ); - fundingPot.setAccessCriteriaForRound( + fundingPot.setAccessCriteria( roundId, accessCriteriaEnum, nftContract, @@ -900,7 +903,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { .selector ) ); - fundingPot.setAccessCriteriaForRound( + fundingPot.setAccessCriteria( roundId, accessCriteriaEnum, nftContract, @@ -933,7 +936,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { .selector ) ); - fundingPot.setAccessCriteriaForRound( + fundingPot.setAccessCriteria( roundId, accessCriteriaEnum, nftContract, @@ -964,7 +967,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { .selector ) ); - fundingPot.setAccessCriteriaForRound( + fundingPot.setAccessCriteria( roundId, accessCriteriaEnum, nftContract, @@ -995,7 +998,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { .selector ) ); - fundingPot.setAccessCriteriaForRound( + fundingPot.setAccessCriteria( roundId, accessCriteriaEnum, nftContract, @@ -1026,7 +1029,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { .selector ) ); - fundingPot.setAccessCriteriaForRound( + fundingPot.setAccessCriteria( roundId, accessCriteriaEnum, nftContract, @@ -1047,7 +1050,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { address[] memory allowedAddresses ) = _helper_createAccessCriteria(accessCriteriaEnum, roundId); - fundingPot.setAccessCriteriaForRound( + fundingPot.setAccessCriteria( roundId, accessCriteriaEnum, nftContract, @@ -1074,144 +1077,33 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { } } - /* Test editAccessCriteriaForRound() - ├── Given user does not have FUNDING_POT_ADMIN_ROLE - │ └── When user attempts to edit access criteria - │ └── Then it should revert - │ - └── Given user has FUNDING_POT_ADMIN_ROLE - ├── Given access criteria id is greater than the number of access criteria for the round - │ └── When user attempts to edit access criteria - │ └── Then it should revert - │ - ├── Given round does not exist - │ └── When user attempts to edit access criteria - │ └── Then it should revert - │ - ├── Given round is active - │ └── When user attempts to edit access criteria - │ └── Then it should revert - │ - └── Given all valid parameters are provided - └── When user attempts to edit access criteria - ├── Then it should not revert - └── Then the access criteria should be updated - */ - - function testFuzzEditAccessCriteriaForRound_revertsGivenUserDoesNotHaveFundingPotAdminRole( - uint8 accessCriteriaEnum, - address user_ + function testFuzzEditAccessCriteria( + uint8 oldAccessCriteriaEnum, + uint8 newAccessCriteriaEnum ) public { - vm.assume(accessCriteriaEnum >= 0 && accessCriteriaEnum <= 4); - vm.assume(user_ != address(0) && user_ != address(this)); + vm.assume(oldAccessCriteriaEnum >= 1 && oldAccessCriteriaEnum <= 4); + vm.assume( + newAccessCriteriaEnum != oldAccessCriteriaEnum + && newAccessCriteriaEnum >= 0 && newAccessCriteriaEnum <= 4 + ); + + testFuzzSetAccessCriteria(oldAccessCriteriaEnum); - _helper_setupRoundWithAccessCriteria(accessCriteriaEnum); uint32 roundId = fundingPot.getRoundCount(); ( address nftContract, bytes32 merkleRoot, address[] memory allowedAddresses - ) = _helper_createAccessCriteria(accessCriteriaEnum, roundId); + ) = _helper_createAccessCriteria(oldAccessCriteriaEnum, roundId); - vm.startPrank(user_); - bytes32 roleId = _authorizer.generateRoleId( - address(fundingPot), fundingPot.FUNDING_POT_ADMIN_ROLE() - ); - vm.expectRevert( - abi.encodeWithSelector( - IModule_v1.Module__CallerNotAuthorized.selector, roleId, user_ - ) - ); - fundingPot.editAccessCriteriaForRound( + fundingPot.setAccessCriteria( roundId, - accessCriteriaEnum, + oldAccessCriteriaEnum, nftContract, merkleRoot, allowedAddresses ); - vm.stopPrank(); - } - - function testFuzzEditAccessCriteriaForRound_revertsGivenAccessCriteriaIdIsGreaterThanAccessCriteriaForTheRound( - uint8 accessCriteriaEnum - ) public { - vm.assume(accessCriteriaEnum >= 0 && accessCriteriaEnum <= 4); - - _helper_setupRoundWithAccessCriteria(accessCriteriaEnum); - uint32 roundId = fundingPot.getRoundCount(); - uint8 accessCriteriaId = 10; // Invalid ID - - ( - address nftContract, - bytes32 merkleRoot, - address[] memory allowedAddresses - ) = _helper_createAccessCriteria(accessCriteriaEnum, roundId); - - vm.expectRevert( - abi.encodeWithSelector( - ILM_PC_FundingPot_v1 - .Module__LM_PC_FundingPot__InvalidAccessCriteriaId - .selector - ) - ); - fundingPot.editAccessCriteriaForRound( - roundId, accessCriteriaId, nftContract, merkleRoot, allowedAddresses - ); - } - - function testFuzzEditAccessCriteriaForRound_revertsGivenRoundDoesNotExist( - uint8 accessCriteriaEnum - ) public { - vm.assume(accessCriteriaEnum >= 0 && accessCriteriaEnum <= 4); - uint32 roundId = fundingPot.getRoundCount(); - uint8 accessCriteriaId = 0; - - ( - address nftContract, - bytes32 merkleRoot, - address[] memory allowedAddresses - ) = _helper_createAccessCriteria(accessCriteriaEnum, roundId); - - vm.expectRevert(); - fundingPot.editAccessCriteriaForRound( - roundId, accessCriteriaId, nftContract, merkleRoot, allowedAddresses - ); - } - - function testFuzzEditAccessCriteriaForRound_revertsGivenRoundIsActive( - uint8 accessCriteriaEnum - ) public { - vm.assume(accessCriteriaEnum >= 0 && accessCriteriaEnum <= 4); - - // Set up a round with access criteria - _helper_setupRoundWithAccessCriteria(accessCriteriaEnum); - uint32 roundId = fundingPot.getRoundCount(); - - // Warp to make the round active - (uint roundStart,,,,,,) = fundingPot.getRoundGenericParameters(roundId); - vm.warp(roundStart + 1); - - // Create a new access criteria to try to edit with - ( - address nftContract, - bytes32 merkleRoot, - address[] memory allowedAddresses - ) = _helper_createAccessCriteria((accessCriteriaEnum + 1) % 5, roundId); // Use a different access criteria type - - // Expect revert when trying to edit access criteria for an active round - vm.expectRevert( - abi.encodeWithSelector( - ILM_PC_FundingPot_v1 - .Module__LM_PC_FundingPot__RoundAlreadyStarted - .selector - ) - ); - - // Attempt to edit the access criteria for the active round - fundingPot.editAccessCriteriaForRound( - roundId, 0, nftContract, merkleRoot, allowedAddresses - ); } /* Test removeAllowlistedAddresses @@ -1242,7 +1134,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { address[] memory allowedAddresses ) = _helper_createAccessCriteria(accessId, roundId); - fundingPot.setAccessCriteriaForRound( + fundingPot.setAccessCriteria( roundId, accessId, nftContract, merkleRoot, allowedAddresses ); @@ -1303,7 +1195,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { address[] memory allowedAddresses ) = _helper_createAccessCriteria(accessCriteriaEnum, roundId); - fundingPot.setAccessCriteriaForRound( + fundingPot.setAccessCriteria( roundId, accessCriteriaEnum, nftContract, @@ -1347,7 +1239,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { address[] memory allowedAddresses ) = _helper_createAccessCriteria(accessCriteriaEnum, roundId); - fundingPot.setAccessCriteriaForRound( + fundingPot.setAccessCriteria( roundId, accessCriteriaEnum, nftContract, @@ -1476,7 +1368,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { address[] memory allowedAddresses ) = _helper_createAccessCriteria(accessId, roundId); - fundingPot.setAccessCriteriaForRound( + fundingPot.setAccessCriteria( roundId, accessId, nftContract, merkleRoot, allowedAddresses ); fundingPot.setAccessCriteriaPrivileges( @@ -1516,7 +1408,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { address[] memory allowedAddresses ) = _helper_createAccessCriteria(accessId, roundId); - fundingPot.setAccessCriteriaForRound( + fundingPot.setAccessCriteria( roundId, accessId, nftContract, merkleRoot, allowedAddresses ); fundingPot.setAccessCriteriaPrivileges( @@ -1595,7 +1487,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { address[] memory allowedAddresses ) = _helper_createAccessCriteria(accessId, roundId); - fundingPot.setAccessCriteriaForRound( + fundingPot.setAccessCriteria( roundId, accessId, nftContract, merkleRoot, allowedAddresses ); fundingPot.setAccessCriteriaPrivileges( @@ -1637,7 +1529,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { address[] memory allowedAddresses ) = _helper_createAccessCriteria(accessId, roundId); - fundingPot.setAccessCriteriaForRound( + fundingPot.setAccessCriteria( roundId, accessId, nftContract, merkleRoot, allowedAddresses ); fundingPot.setAccessCriteriaPrivileges( @@ -1679,7 +1571,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { address[] memory allowedAddresses ) = _helper_createAccessCriteria(accessId, roundId); - fundingPot.setAccessCriteriaForRound( + fundingPot.setAccessCriteria( roundId, accessId, nftContract, merkleRoot, allowedAddresses ); fundingPot.setAccessCriteriaPrivileges( @@ -1791,7 +1683,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { address[] memory allowedAddresses ) = _helper_createAccessCriteria(accessId, roundId); - fundingPot.setAccessCriteriaForRound( + fundingPot.setAccessCriteria( roundId, accessId, nftContract, merkleRoot, allowedAddresses ); fundingPot.setAccessCriteriaPrivileges( @@ -1939,7 +1831,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { address[] memory allowedAddresses ) = _helper_createAccessCriteria(accessId, roundId); - fundingPot.setAccessCriteriaForRound( + fundingPot.setAccessCriteria( roundId, accessId, nftContract, merkleRoot, allowedAddresses ); @@ -1994,7 +1886,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { address[] memory allowedAddresses ) = _helper_createAccessCriteria(accessId, roundId); - fundingPot.setAccessCriteriaForRound( + fundingPot.setAccessCriteria( roundId, accessId, nftContract, merkleRoot, allowedAddresses ); fundingPot.setAccessCriteriaPrivileges( @@ -2056,7 +1948,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { address[] memory allowedAddresses ) = _helper_createAccessCriteria(accessId, roundId); - fundingPot.setAccessCriteriaForRound( + fundingPot.setAccessCriteria( roundId, accessId, nftContract, merkleRoot, allowedAddresses ); @@ -2116,7 +2008,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { bytes32 merkleRoot, address[] memory allowedAddresses ) = _helper_createAccessCriteria(accessCriteriaId, round1Id); - fundingPot.setAccessCriteriaForRound( + fundingPot.setAccessCriteria( round1Id, accessCriteriaId, nftContract, @@ -2141,7 +2033,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { ); uint32 round2Id = fundingPot.getRoundCount(); - fundingPot.setAccessCriteriaForRound( + fundingPot.setAccessCriteria( round2Id, accessCriteriaId, nftContract, @@ -2225,7 +2117,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { bytes32 merkleRoot, address[] memory allowedAddresses ) = _helper_createAccessCriteria(accessId, round1Id); - fundingPot.setAccessCriteriaForRound( + fundingPot.setAccessCriteria( round1Id, accessId, nftContract, merkleRoot, allowedAddresses ); fundingPot.setAccessCriteriaPrivileges( @@ -2244,7 +2136,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { _defaultRoundParams.globalAccumulativeCaps ); uint32 round2Id = fundingPot.getRoundCount(); - fundingPot.setAccessCriteriaForRound( + fundingPot.setAccessCriteria( round2Id, accessId, nftContract, merkleRoot, allowedAddresses ); fundingPot.setAccessCriteriaPrivileges( @@ -2433,7 +2325,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { address[] memory allowedAddresses ) = _helper_createAccessCriteria(accessCriteriaId, roundId); - fundingPot.setAccessCriteriaForRound( + fundingPot.setAccessCriteria( roundId, accessCriteriaId, nftContract, merkleRoot, allowedAddresses ); @@ -2507,7 +2399,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { address[] memory allowedAddresses ) = _helper_createAccessCriteria(accessId, roundId); - fundingPot.setAccessCriteriaForRound( + fundingPot.setAccessCriteria( roundId, accessId, nftContract, merkleRoot, allowedAddresses ); fundingPot.setAccessCriteriaPrivileges( @@ -2544,7 +2436,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { address[] memory allowedAddresses ) = _helper_createAccessCriteria(accessId, roundId); - fundingPot.setAccessCriteriaForRound( + fundingPot.setAccessCriteria( roundId, accessId, nftContract, merkleRoot, allowedAddresses ); fundingPot.setAccessCriteriaPrivileges( @@ -2586,7 +2478,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { address[] memory allowedAddresses ) = _helper_createAccessCriteria(accessId, roundId); - fundingPot.setAccessCriteriaForRound( + fundingPot.setAccessCriteria( roundId, accessId, nftContract, merkleRoot, allowedAddresses ); fundingPot.setAccessCriteriaPrivileges( @@ -2625,7 +2517,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { address[] memory allowedAddresses ) = _helper_createAccessCriteria(accessId, roundId); - fundingPot.setAccessCriteriaForRound( + fundingPot.setAccessCriteria( roundId, accessId, nftContract, merkleRoot, allowedAddresses ); @@ -2661,7 +2553,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { address[] memory allowedAddresses ) = _helper_createAccessCriteria(accessId, roundId); - fundingPot.setAccessCriteriaForRound( + fundingPot.setAccessCriteria( roundId, accessId, nftContract, merkleRoot, allowedAddresses ); fundingPot.setAccessCriteriaPrivileges( @@ -3004,7 +2896,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { bytes32 merkleRoot, address[] memory allowedAddresses ) = _helper_createAccessCriteria(accessId, roundId); - fundingPot.setAccessCriteriaForRound( + fundingPot.setAccessCriteria( roundId, accessId, nftContract, merkleRoot, allowedAddresses ); fundingPot.setAccessCriteriaPrivileges( @@ -3087,7 +2979,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { bytes32 merkleRoot, address[] memory allowedAddresses ) = _helper_createAccessCriteria(accessId, roundId); - fundingPot.setAccessCriteriaForRound( + fundingPot.setAccessCriteria( roundId, accessId, nftContract, merkleRoot, allowedAddresses ); fundingPot.setAccessCriteriaPrivileges( @@ -3156,7 +3048,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { address[] memory allowedAddresses ) = _helper_createAccessCriteria(accessId, roundId); - fundingPot.setAccessCriteriaForRound( + fundingPot.setAccessCriteria( roundId, accessId, nftContract, merkleRoot, allowedAddresses ); fundingPot.setAccessCriteriaPrivileges( @@ -3341,7 +3233,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { address[] memory allowedAddresses ) = _helper_createAccessCriteria(accessCriteriaEnum, roundId); - fundingPot.setAccessCriteriaForRound( + fundingPot.setAccessCriteria( roundId, accessCriteriaEnum, nftContract, From 398a9fcf03a6cf5f4aa996f383f96ab2f574c421 Mon Sep 17 00:00:00 2001 From: Zuhaib Mohammed Date: Tue, 6 May 2025 15:58:52 +0530 Subject: [PATCH 105/130] chore: add support for multiple access control criteria --- .../logicModule/LM_PC_FundingPot_v1.sol | 55 +++-- .../interfaces/ILM_PC_FundingPot_v1.sol | 5 +- test/e2e/logicModule/FundingPotE2E.t.sol | 12 +- .../logicModule/LM_PC_FundingPot_v1.t.sol | 207 +++++++++++------- 4 files changed, 176 insertions(+), 103 deletions(-) diff --git a/src/modules/logicModule/LM_PC_FundingPot_v1.sol b/src/modules/logicModule/LM_PC_FundingPot_v1.sol index 58a80f14d..bcecaa22f 100644 --- a/src/modules/logicModule/LM_PC_FundingPot_v1.sol +++ b/src/modules/logicModule/LM_PC_FundingPot_v1.sol @@ -148,6 +148,9 @@ contract LM_PC_FundingPot_v1 is /// @notice Add a mapping to track the next unprocessed index for each round. mapping(uint32 => uint) private roundIdToNextUnprocessedIndex; + /// @notice The next available access criteria ID for each round + mapping(uint32 => uint8) private roundIdToNextAccessCriteriaId; + /// @notice Storage gap for future upgrades. uint[50] private __gap; @@ -431,26 +434,42 @@ contract LM_PC_FundingPot_v1 is /// @inheritdoc ILM_PC_FundingPot_v1 function setAccessCriteria( uint32 roundId_, - uint8 accessCriteriaId_, + uint8 accessCriteriaType_, + uint8 accessCriteriaId_, // Optional: 0 for new, non-zero for edit address nftContract_, bytes32 merkleRoot_, address[] calldata allowedAddresses_ ) external onlyModuleRole(FUNDING_POT_ADMIN_ROLE) { Round storage round = rounds[roundId_]; - if (accessCriteriaId_ > MAX_ACCESS_CRITERIA_ID) { + if (accessCriteriaType_ > MAX_ACCESS_CRITERIA_ID) { revert Module__LM_PC_FundingPot__InvalidAccessCriteriaId(); } _validateEditRoundParameters(round); - // Check if this is a new setting or an edit - bool isEdit = round.accessCriterias[accessCriteriaId_] - .accessCriteriaType != AccessCriteriaType.UNSET; + uint8 criteriaId; + bool isEdit = false; + + // If accessCriteriaId_ is 0, create a new access criteria + // Otherwise, edit the existing one + if (accessCriteriaId_ == 0) { + criteriaId = ++roundIdToNextAccessCriteriaId[roundId_]; + } else { + criteriaId = accessCriteriaId_; + isEdit = true; + + if ( + round.accessCriterias[criteriaId].accessCriteriaType + == AccessCriteriaType.UNSET + ) { + revert Module__LM_PC_FundingPot__InvalidAccessCriteriaId(); + } + } // Validate required data based on access criteria type AccessCriteriaType accessCriteriaType = - AccessCriteriaType(accessCriteriaId_); + AccessCriteriaType(accessCriteriaType_); if (accessCriteriaType == AccessCriteriaType.NFT) { if (nftContract_ == address(0)) { revert @@ -469,35 +488,36 @@ contract LM_PC_FundingPot_v1 is } // Clear all existing data to prevent stale data - round.accessCriterias[accessCriteriaId_].nftContract = address(0); - round.accessCriterias[accessCriteriaId_].merkleRoot = bytes32(0); + round.accessCriterias[criteriaId].nftContract = address(0); + round.accessCriterias[criteriaId].merkleRoot = bytes32(0); // @note: When changing allowlists, call removeAllowlistedAddresses first to clear previous entries // Set the access criteria type - round.accessCriterias[accessCriteriaId_].accessCriteriaType = + round.accessCriterias[criteriaId].accessCriteriaType = accessCriteriaType; // Set only the relevant data based on the access criteria type if (accessCriteriaType == AccessCriteriaType.NFT) { - round.accessCriterias[accessCriteriaId_].nftContract = nftContract_; + round.accessCriterias[criteriaId].nftContract = nftContract_; } else if (accessCriteriaType == AccessCriteriaType.MERKLE) { - round.accessCriterias[accessCriteriaId_].merkleRoot = merkleRoot_; + round.accessCriterias[criteriaId].merkleRoot = merkleRoot_; } else if (accessCriteriaType == AccessCriteriaType.LIST) { // For LIST type, update the allowed addresses for (uint i = 0; i < allowedAddresses_.length; i++) { - round.accessCriterias[accessCriteriaId_].allowedAddresses[allowedAddresses_[i]] + round.accessCriterias[criteriaId].allowedAddresses[allowedAddresses_[i]] = true; } } // Emit the appropriate event based on whether this is a new setting or an edit if (isEdit) { - emit AccessCriteriaEdited(roundId_, accessCriteriaId_); + emit AccessCriteriaEdited(roundId_, criteriaId); } else { - emit AccessCriteriaSet(roundId_, accessCriteriaId_); + emit AccessCriteriaSet(roundId_, criteriaId); } } + // Update removeAllowlistedAddresses to match the new approach /// @inheritdoc ILM_PC_FundingPot_v1 function removeAllowlistedAddresses( uint32 roundId_, @@ -505,7 +525,12 @@ contract LM_PC_FundingPot_v1 is address[] calldata addressesToRemove_ ) external onlyModuleRole(FUNDING_POT_ADMIN_ROLE) { Round storage round = rounds[roundId_]; - if (accessCriteriaId_ > MAX_ACCESS_CRITERIA_ID) { + + // Verify the access criteria exists + if ( + round.accessCriterias[accessCriteriaId_].accessCriteriaType + == AccessCriteriaType.UNSET + ) { revert Module__LM_PC_FundingPot__InvalidAccessCriteriaId(); } diff --git a/src/modules/logicModule/interfaces/ILM_PC_FundingPot_v1.sol b/src/modules/logicModule/interfaces/ILM_PC_FundingPot_v1.sol index dabfdd89b..7f44488ef 100644 --- a/src/modules/logicModule/interfaces/ILM_PC_FundingPot_v1.sol +++ b/src/modules/logicModule/interfaces/ILM_PC_FundingPot_v1.sol @@ -271,9 +271,6 @@ interface ILM_PC_FundingPot_v1 is IERC20PaymentClientBase_v2 { /// @notice User is not on the allowlist. error Module__LM_PC_FundingPot__AccessCriteriaListFailed(); - /// @notice Invalid access criteria type. - error Module__LM_PC_FundingPot__InvalidAccessCriteriaType(); - /// @notice Access not permitted. error Module__LM_PC_FundingPot__AccessNotPermitted(); @@ -438,12 +435,14 @@ interface ILM_PC_FundingPot_v1 is IERC20PaymentClientBase_v2 { /// @notice Set Access Control Check. /// @dev Only callable by funding pot admin and only before the round has started. /// @param roundId_ ID of the round. + /// @param accessCriteriaType_ access criteria type of the round. /// @param accessCriteriaId_ ID of the access criteria. /// @param nftContract_ Address of the NFT contract. /// @param merkleRoot_ Merkle root for the access criteria. /// @param allowedAddresses_ List of explicitly allowed addresses. function setAccessCriteria( uint32 roundId_, + uint8 accessCriteriaType_, uint8 accessCriteriaId_, address nftContract_, bytes32 merkleRoot_, diff --git a/test/e2e/logicModule/FundingPotE2E.t.sol b/test/e2e/logicModule/FundingPotE2E.t.sol index fb5e5695b..f00b5e120 100644 --- a/test/e2e/logicModule/FundingPotE2E.t.sol +++ b/test/e2e/logicModule/FundingPotE2E.t.sol @@ -204,6 +204,7 @@ contract FundingPotE2E is E2ETest { fundingPot.setAccessCriteria( round1Id, uint8(ILM_PC_FundingPot_v1.AccessCriteriaType.LIST), + 0, address(0), bytes32(0), allowedAddresses @@ -216,6 +217,7 @@ contract FundingPotE2E is E2ETest { fundingPot.setAccessCriteria( round2Id, uint8(ILM_PC_FundingPot_v1.AccessCriteriaType.LIST), + 0, address(0), bytes32(0), allowedAddresses @@ -224,7 +226,7 @@ contract FundingPotE2E is E2ETest { // 5. Set access criteria privileges for the rounds fundingPot.setAccessCriteriaPrivileges( round1Id, - 0, // accessCriteriaId + 1, // accessCriteriaId 500e18, // personalCap true, // overrideContributionSpan block.timestamp, // start @@ -234,7 +236,7 @@ contract FundingPotE2E is E2ETest { fundingPot.setAccessCriteriaPrivileges( round2Id, - 0, // accessCriteriaId + 1, // accessCriteriaId 750e18, // personalCap true, // overrideContributionSpan block.timestamp, // start @@ -252,21 +254,21 @@ contract FundingPotE2E is E2ETest { vm.startPrank(contributor1); contributionToken.approve(address(fundingPot), 400e18); fundingPot.contributeToRoundFor( - contributor1, round1Id, 400e18, 0, new bytes32[](0) + contributor1, round1Id, 400e18, 1, new bytes32[](0) ); vm.stopPrank(); vm.startPrank(contributor2); contributionToken.approve(address(fundingPot), 600e18); fundingPot.contributeToRoundFor( - contributor2, round1Id, 600e18, 0, new bytes32[](0) + contributor2, round1Id, 600e18, 1, new bytes32[](0) ); vm.stopPrank(); vm.startPrank(contributor3); contributionToken.approve(address(fundingPot), 750e18); fundingPot.contributeToRoundFor( - contributor3, round2Id, 750e18, 0, new bytes32[](0) + contributor3, round2Id, 750e18, 1, new bytes32[](0) ); vm.stopPrank(); diff --git a/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol b/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol index f210ab604..293b3cf9a 100644 --- a/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol +++ b/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol @@ -843,6 +843,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { fundingPot.setAccessCriteria( roundId, accessCriteriaEnum_, + 0, nftContract, merkleRoot, allowedAddresses @@ -873,6 +874,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { fundingPot.setAccessCriteria( roundId, accessCriteriaEnum, + 0, nftContract, merkleRoot, allowedAddresses @@ -906,6 +908,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { fundingPot.setAccessCriteria( roundId, accessCriteriaEnum, + 0, nftContract, merkleRoot, allowedAddresses @@ -939,6 +942,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { fundingPot.setAccessCriteria( roundId, accessCriteriaEnum, + 0, nftContract, merkleRoot, allowedAddresses @@ -970,6 +974,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { fundingPot.setAccessCriteria( roundId, accessCriteriaEnum, + 0, nftContract, merkleRoot, allowedAddresses @@ -1001,6 +1006,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { fundingPot.setAccessCriteria( roundId, accessCriteriaEnum, + 0, nftContract, merkleRoot, allowedAddresses @@ -1032,6 +1038,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { fundingPot.setAccessCriteria( roundId, accessCriteriaEnum, + 0, nftContract, merkleRoot, allowedAddresses @@ -1043,6 +1050,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { testCreateRound(); uint32 roundId = fundingPot.getRoundCount(); + uint8 accessId = 1; ( address nftContract, @@ -1053,6 +1061,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { fundingPot.setAccessCriteria( roundId, accessCriteriaEnum, + 0, nftContract, merkleRoot, allowedAddresses @@ -1063,9 +1072,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { address retrievedNftContract, bytes32 retrievedMerkleRoot, bool hasAccess - ) = fundingPot.getRoundAccessCriteria( - uint32(roundId), accessCriteriaEnum - ); + ) = fundingPot.getRoundAccessCriteria(uint32(roundId), accessId); assertEq(isOpen, accessCriteriaEnum == 1); assertEq(retrievedNftContract, nftContract); @@ -1084,22 +1091,26 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { vm.assume(oldAccessCriteriaEnum >= 1 && oldAccessCriteriaEnum <= 4); vm.assume( newAccessCriteriaEnum != oldAccessCriteriaEnum - && newAccessCriteriaEnum >= 0 && newAccessCriteriaEnum <= 4 + && newAccessCriteriaEnum >= 1 && newAccessCriteriaEnum <= 4 ); testFuzzSetAccessCriteria(oldAccessCriteriaEnum); uint32 roundId = fundingPot.getRoundCount(); - ( address nftContract, bytes32 merkleRoot, address[] memory allowedAddresses - ) = _helper_createAccessCriteria(oldAccessCriteriaEnum, roundId); + ) = _helper_createAccessCriteria(newAccessCriteriaEnum, roundId); + vm.expectEmit(true, true, true, false); + emit ILM_PC_FundingPot_v1.AccessCriteriaEdited( + roundId, uint8(newAccessCriteriaEnum) + ); fundingPot.setAccessCriteria( roundId, - oldAccessCriteriaEnum, + newAccessCriteriaEnum, + 1, nftContract, merkleRoot, allowedAddresses @@ -1127,15 +1138,16 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { testCreateRound(); uint32 roundId = fundingPot.getRoundCount(); - uint8 accessId = uint8(ILM_PC_FundingPot_v1.AccessCriteriaType.LIST); + uint8 accessId = 1; + uint8 accessType = uint8(ILM_PC_FundingPot_v1.AccessCriteriaType.LIST); ( address nftContract, bytes32 merkleRoot, address[] memory allowedAddresses - ) = _helper_createAccessCriteria(accessId, roundId); + ) = _helper_createAccessCriteria(accessType, roundId); fundingPot.setAccessCriteria( - roundId, accessId, nftContract, merkleRoot, allowedAddresses + roundId, accessType, 0, nftContract, merkleRoot, allowedAddresses ); address[] memory addressesToRemove = new address[](2); @@ -1198,6 +1210,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { fundingPot.setAccessCriteria( roundId, accessCriteriaEnum, + 0, nftContract, merkleRoot, allowedAddresses @@ -1213,9 +1226,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { ) ); - fundingPot.setAccessCriteriaPrivileges( - roundId, accessCriteriaEnum, 1000, false, 0, 0, 0 - ); + fundingPot.setAccessCriteriaPrivileges(roundId, 1, 1000, false, 0, 0, 0); vm.stopPrank(); } @@ -1239,16 +1250,19 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { address[] memory allowedAddresses ) = _helper_createAccessCriteria(accessCriteriaEnum, roundId); + uint8 accessId = 1; + fundingPot.setAccessCriteria( roundId, accessCriteriaEnum, + 0, nftContract, merkleRoot, allowedAddresses ); fundingPot.setAccessCriteriaPrivileges( - roundId, accessCriteriaEnum, 1000, false, 0, 0, 0 + roundId, accessId, 1000, false, 0, 0, 0 ); ( @@ -1257,9 +1271,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { uint start, uint cliff, uint end - ) = fundingPot.getRoundAccessCriteriaPrivileges( - roundId, accessCriteriaEnum - ); + ) = fundingPot.getRoundAccessCriteriaPrivileges(roundId, accessId); assertEq(personalCap, 1000); assertEq(overrideContributionSpan, false); @@ -1354,22 +1366,19 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { ) public { testCreateRound(); - // vm.assume(amount > 0 && amount <= 500); - // vm.assume(roundStart > block.timestamp && roundStart < roundEnd); - // vm.assume(roundCap > 0); - uint32 roundId = fundingPot.getRoundCount(); uint8 accessId = 1; + uint8 accessType = uint8(ILM_PC_FundingPot_v1.AccessCriteriaType.OPEN); uint amount = 250; ( address nftContract, bytes32 merkleRoot, address[] memory allowedAddresses - ) = _helper_createAccessCriteria(accessId, roundId); + ) = _helper_createAccessCriteria(accessType, roundId); fundingPot.setAccessCriteria( - roundId, accessId, nftContract, merkleRoot, allowedAddresses + roundId, accessType, 0, nftContract, merkleRoot, allowedAddresses ); fundingPot.setAccessCriteriaPrivileges( roundId, accessId, 500, false, 0, 0, 0 @@ -1399,17 +1408,19 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { testCreateRound(); uint32 roundId = fundingPot.getRoundCount(); - uint8 accessId = 0; + uint8 accessId = 1; + uint8 accessType = uint8(ILM_PC_FundingPot_v1.AccessCriteriaType.OPEN); + uint amount = 250; ( address nftContract, bytes32 merkleRoot, address[] memory allowedAddresses - ) = _helper_createAccessCriteria(accessId, roundId); + ) = _helper_createAccessCriteria(accessType, roundId); fundingPot.setAccessCriteria( - roundId, accessId, nftContract, merkleRoot, allowedAddresses + roundId, accessType, 0, nftContract, merkleRoot, allowedAddresses ); fundingPot.setAccessCriteriaPrivileges( roundId, accessId, 500, false, 0, 0, 0 @@ -1439,8 +1450,9 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { function testContributeToRoundFor_revertsGivenNFTAccessCriteriaIsNotMet() public { - uint8 accessId = uint8(ILM_PC_FundingPot_v1.AccessCriteriaType.NFT); - _helper_setupRoundWithAccessCriteria(accessId); + uint8 accessId = 1; + uint8 accessType = uint8(ILM_PC_FundingPot_v1.AccessCriteriaType.NFT); + _helper_setupRoundWithAccessCriteria(accessType); uint32 roundId = fundingPot.getRoundCount(); @@ -1474,7 +1486,8 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { testCreateRound(); uint32 roundId = fundingPot.getRoundCount(); - uint8 accessId = uint8(ILM_PC_FundingPot_v1.AccessCriteriaType.MERKLE); + uint8 accessId = 1; + uint8 accessType = uint8(ILM_PC_FundingPot_v1.AccessCriteriaType.MERKLE); uint amount = 250; (,,,, bytes32[] memory proofB) = _helper_generateMerkleTreeForTwoLeaves( @@ -1485,10 +1498,10 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { address nftContract, bytes32 merkleRoot, address[] memory allowedAddresses - ) = _helper_createAccessCriteria(accessId, roundId); + ) = _helper_createAccessCriteria(accessType, roundId); fundingPot.setAccessCriteria( - roundId, accessId, nftContract, merkleRoot, allowedAddresses + roundId, accessType, 0, nftContract, merkleRoot, allowedAddresses ); fundingPot.setAccessCriteriaPrivileges( roundId, accessId, 500, false, 0, 0, 0 @@ -1520,17 +1533,18 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { testCreateRound(); uint32 roundId = fundingPot.getRoundCount(); - uint8 accessId = uint8(ILM_PC_FundingPot_v1.AccessCriteriaType.LIST); + uint8 accessId = 1; + uint8 accessType = uint8(ILM_PC_FundingPot_v1.AccessCriteriaType.LIST); uint amount = 250; ( address nftContract, bytes32 merkleRoot, address[] memory allowedAddresses - ) = _helper_createAccessCriteria(accessId, roundId); + ) = _helper_createAccessCriteria(accessType, roundId); fundingPot.setAccessCriteria( - roundId, accessId, nftContract, merkleRoot, allowedAddresses + roundId, accessType, 0, nftContract, merkleRoot, allowedAddresses ); fundingPot.setAccessCriteriaPrivileges( roundId, accessId, 500, false, 0, 0, 0 @@ -1562,17 +1576,18 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { testCreateRound(); uint32 roundId = fundingPot.getRoundCount(); - uint8 accessId = uint8(ILM_PC_FundingPot_v1.AccessCriteriaType.NFT); + uint8 accessId = 1; + uint8 accessType = uint8(ILM_PC_FundingPot_v1.AccessCriteriaType.NFT); uint amount = 500; ( address nftContract, bytes32 merkleRoot, address[] memory allowedAddresses - ) = _helper_createAccessCriteria(accessId, roundId); + ) = _helper_createAccessCriteria(accessType, roundId); fundingPot.setAccessCriteria( - roundId, accessId, nftContract, merkleRoot, allowedAddresses + roundId, accessType, 0, nftContract, merkleRoot, allowedAddresses ); fundingPot.setAccessCriteriaPrivileges( roundId, accessId, 500, false, 0, 0, 0 @@ -1672,7 +1687,8 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { function testcontributeToRoundFor_worksGivenAllConditionsMet() public { testCreateRound(); - uint8 accessId = uint8(ILM_PC_FundingPot_v1.AccessCriteriaType.NFT); + uint8 accessId = 1; + uint8 accessType = uint8(ILM_PC_FundingPot_v1.AccessCriteriaType.NFT); uint32 roundId = fundingPot.getRoundCount(); uint amount = 250; @@ -1681,10 +1697,10 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { address nftContract, bytes32 merkleRoot, address[] memory allowedAddresses - ) = _helper_createAccessCriteria(accessId, roundId); + ) = _helper_createAccessCriteria(accessType, roundId); fundingPot.setAccessCriteria( - roundId, accessId, nftContract, merkleRoot, allowedAddresses + roundId, accessType, 0, nftContract, merkleRoot, allowedAddresses ); fundingPot.setAccessCriteriaPrivileges( roundId, accessId, 1000, false, 0, 0, 0 @@ -1732,7 +1748,8 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { accessCriteriaEnumNew != accessCriteriaEnumOld && accessCriteriaEnumNew >= 0 && accessCriteriaEnumNew <= 4 ); - uint8 accessId = uint8(ILM_PC_FundingPot_v1.AccessCriteriaType.NFT); + uint8 accessId = 1; + uint8 accessType = uint8(ILM_PC_FundingPot_v1.AccessCriteriaType.NFT); _helper_setupRoundWithAccessCriteria(accessId); uint32 roundId = fundingPot.getRoundCount(); @@ -1768,9 +1785,10 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { function testContributeToRoundFor_worksGivenMerkleAccessCriteriaMet() public { - uint8 accessId = uint8(ILM_PC_FundingPot_v1.AccessCriteriaType.MERKLE); + uint8 accessId = 1; + uint8 accessType = uint8(ILM_PC_FundingPot_v1.AccessCriteriaType.MERKLE); - _helper_setupRoundWithAccessCriteria(accessId); + _helper_setupRoundWithAccessCriteria(accessType); uint32 roundId = fundingPot.getRoundCount(); @@ -1813,7 +1831,8 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { _defaultRoundParams.roundCap = 150; _defaultRoundParams.autoClosure = true; - uint8 accessId = uint8(ILM_PC_FundingPot_v1.AccessCriteriaType.NFT); + uint8 accessId = 1; + uint8 accessType = uint8(ILM_PC_FundingPot_v1.AccessCriteriaType.NFT); uint32 roundId = fundingPot.createRound( _defaultRoundParams.roundStart, @@ -1829,10 +1848,10 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { address nftContract, bytes32 merkleRoot, address[] memory allowedAddresses - ) = _helper_createAccessCriteria(accessId, roundId); + ) = _helper_createAccessCriteria(accessType, roundId); fundingPot.setAccessCriteria( - roundId, accessId, nftContract, merkleRoot, allowedAddresses + roundId, accessType, 0, nftContract, merkleRoot, allowedAddresses ); uint personalCap = 200; @@ -1875,7 +1894,8 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { testCreateRound(); uint32 roundId = fundingPot.getRoundCount(); - uint8 accessId = uint8(ILM_PC_FundingPot_v1.AccessCriteriaType.NFT); + uint8 accessId = 1; + uint8 accessType = uint8(ILM_PC_FundingPot_v1.AccessCriteriaType.NFT); uint firstAmount = 400; uint personalCap = 500; @@ -1884,10 +1904,10 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { address nftContract, bytes32 merkleRoot, address[] memory allowedAddresses - ) = _helper_createAccessCriteria(accessId, roundId); + ) = _helper_createAccessCriteria(accessType, roundId); fundingPot.setAccessCriteria( - roundId, accessId, nftContract, merkleRoot, allowedAddresses + roundId, accessType, 0, nftContract, merkleRoot, allowedAddresses ); fundingPot.setAccessCriteriaPrivileges( roundId, accessId, personalCap, false, 0, 0, 0 @@ -1939,17 +1959,18 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { testCreateRound(); uint32 roundId = fundingPot.getRoundCount(); - uint8 accessId = uint8(ILM_PC_FundingPot_v1.AccessCriteriaType.NFT); + uint8 accessId = 1; + uint8 accessType = uint8(ILM_PC_FundingPot_v1.AccessCriteriaType.NFT); uint amount = 250; ( address nftContract, bytes32 merkleRoot, address[] memory allowedAddresses - ) = _helper_createAccessCriteria(accessId, roundId); + ) = _helper_createAccessCriteria(accessType, roundId); fundingPot.setAccessCriteria( - roundId, accessId, nftContract, merkleRoot, allowedAddresses + roundId, accessType, 0, nftContract, merkleRoot, allowedAddresses ); // Set privileges with override capability @@ -2001,16 +2022,18 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { uint32 round1Id = fundingPot.getRoundCount(); - uint8 accessCriteriaId = + uint8 accessCriteriaId = 1; + uint8 accessCriteriaType = uint8(ILM_PC_FundingPot_v1.AccessCriteriaType.NFT); ( address nftContract, bytes32 merkleRoot, address[] memory allowedAddresses - ) = _helper_createAccessCriteria(accessCriteriaId, round1Id); + ) = _helper_createAccessCriteria(accessCriteriaType, round1Id); fundingPot.setAccessCriteria( round1Id, - accessCriteriaId, + accessCriteriaType, + 0, nftContract, merkleRoot, allowedAddresses @@ -2035,7 +2058,8 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { fundingPot.setAccessCriteria( round2Id, - accessCriteriaId, + accessCriteriaType, + 0, nftContract, merkleRoot, allowedAddresses @@ -2112,13 +2136,15 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { uint32 round1Id = fundingPot.getRoundCount(); uint8 accessId = 1; + uint8 accessType = uint8(ILM_PC_FundingPot_v1.AccessCriteriaType.OPEN); + ( address nftContract, bytes32 merkleRoot, address[] memory allowedAddresses - ) = _helper_createAccessCriteria(accessId, round1Id); + ) = _helper_createAccessCriteria(accessType, round1Id); fundingPot.setAccessCriteria( - round1Id, accessId, nftContract, merkleRoot, allowedAddresses + round1Id, accessType, 0, nftContract, merkleRoot, allowedAddresses ); fundingPot.setAccessCriteriaPrivileges( round1Id, accessId, 500, false, 0, 0, 0 @@ -2137,7 +2163,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { ); uint32 round2Id = fundingPot.getRoundCount(); fundingPot.setAccessCriteria( - round2Id, accessId, nftContract, merkleRoot, allowedAddresses + round2Id, accessType, 0, nftContract, merkleRoot, allowedAddresses ); fundingPot.setAccessCriteriaPrivileges( round2Id, accessId, 500, false, 0, 0, 0 @@ -2326,7 +2352,12 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { ) = _helper_createAccessCriteria(accessCriteriaId, roundId); fundingPot.setAccessCriteria( - roundId, accessCriteriaId, nftContract, merkleRoot, allowedAddresses + roundId, + accessCriteriaId, + 0, + nftContract, + merkleRoot, + allowedAddresses ); fundingPot.setAccessCriteriaPrivileges(roundId, 0, 1000, false, 0, 0, 0); @@ -2392,15 +2423,16 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { testCreateRound(); uint32 roundId = fundingPot.getRoundCount(); - uint8 accessId = uint8(ILM_PC_FundingPot_v1.AccessCriteriaType.OPEN); + uint8 accessId = 1; + uint8 accessType = uint8(ILM_PC_FundingPot_v1.AccessCriteriaType.OPEN); ( address nftContract, bytes32 merkleRoot, address[] memory allowedAddresses - ) = _helper_createAccessCriteria(accessId, roundId); + ) = _helper_createAccessCriteria(accessType, roundId); fundingPot.setAccessCriteria( - roundId, accessId, nftContract, merkleRoot, allowedAddresses + roundId, accessType, 0, nftContract, merkleRoot, allowedAddresses ); fundingPot.setAccessCriteriaPrivileges( roundId, accessId, 1000, false, 0, 0, 0 @@ -2430,14 +2462,16 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { uint32 roundId = fundingPot.getRoundCount(); uint8 accessId = 1; + uint8 accessType = uint8(ILM_PC_FundingPot_v1.AccessCriteriaType.OPEN); + ( address nftContract, bytes32 merkleRoot, address[] memory allowedAddresses - ) = _helper_createAccessCriteria(accessId, roundId); + ) = _helper_createAccessCriteria(accessType, roundId); fundingPot.setAccessCriteria( - roundId, accessId, nftContract, merkleRoot, allowedAddresses + roundId, accessType, 0, nftContract, merkleRoot, allowedAddresses ); fundingPot.setAccessCriteriaPrivileges( roundId, accessId, 1000, false, 0, 0, 0 @@ -2469,17 +2503,19 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { testCreateRound(); uint32 roundId = fundingPot.getRoundCount(); - uint8 accessId = 2; + uint8 accessId = 1; + uint8 accessType = uint8(ILM_PC_FundingPot_v1.AccessCriteriaType.NFT); + uint amount = 1000; ( address nftContract, bytes32 merkleRoot, address[] memory allowedAddresses - ) = _helper_createAccessCriteria(accessId, roundId); + ) = _helper_createAccessCriteria(accessType, roundId); fundingPot.setAccessCriteria( - roundId, accessId, nftContract, merkleRoot, allowedAddresses + roundId, accessType, 0, nftContract, merkleRoot, allowedAddresses ); fundingPot.setAccessCriteriaPrivileges( roundId, accessId, 1000, false, 0, 0, 0 @@ -2508,17 +2544,19 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { testEditRound(); uint32 roundId = fundingPot.getRoundCount(); - uint8 accessId = 2; + uint8 accessId = 1; + uint8 accessType = uint8(ILM_PC_FundingPot_v1.AccessCriteriaType.OPEN); + uint amount = 2000; ( address nftContract, bytes32 merkleRoot, address[] memory allowedAddresses - ) = _helper_createAccessCriteria(accessId, roundId); + ) = _helper_createAccessCriteria(accessType, roundId); fundingPot.setAccessCriteria( - roundId, accessId, nftContract, merkleRoot, allowedAddresses + roundId, accessType, 0, nftContract, merkleRoot, allowedAddresses ); fundingPot.setAccessCriteriaPrivileges( @@ -2547,14 +2585,16 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { // Set up access criteria uint8 accessId = 1; + uint8 accessType = uint8(ILM_PC_FundingPot_v1.AccessCriteriaType.OPEN); + ( address nftContract, bytes32 merkleRoot, address[] memory allowedAddresses - ) = _helper_createAccessCriteria(accessId, roundId); + ) = _helper_createAccessCriteria(accessType, roundId); fundingPot.setAccessCriteria( - roundId, accessId, nftContract, merkleRoot, allowedAddresses + roundId, accessType, 0, nftContract, merkleRoot, allowedAddresses ); fundingPot.setAccessCriteriaPrivileges( roundId, accessId, 1000, false, 0, 0, 0 @@ -2891,13 +2931,15 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { // Set access criteria and privileges uint8 accessId = 1; + uint8 accessType = uint8(ILM_PC_FundingPot_v1.AccessCriteriaType.OPEN); + ( address nftContract, bytes32 merkleRoot, address[] memory allowedAddresses - ) = _helper_createAccessCriteria(accessId, roundId); + ) = _helper_createAccessCriteria(accessType, roundId); fundingPot.setAccessCriteria( - roundId, accessId, nftContract, merkleRoot, allowedAddresses + roundId, accessType, 0, nftContract, merkleRoot, allowedAddresses ); fundingPot.setAccessCriteriaPrivileges( roundId, accessId, 1000, false, 0, 0, 0 @@ -2974,13 +3016,15 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { // Set access criteria and privileges uint8 accessId = 1; + uint8 accessType = uint8(ILM_PC_FundingPot_v1.AccessCriteriaType.OPEN); + ( address nftContract, bytes32 merkleRoot, address[] memory allowedAddresses - ) = _helper_createAccessCriteria(accessId, roundId); + ) = _helper_createAccessCriteria(accessType, roundId); fundingPot.setAccessCriteria( - roundId, accessId, nftContract, merkleRoot, allowedAddresses + roundId, accessType, 0, nftContract, merkleRoot, allowedAddresses ); fundingPot.setAccessCriteriaPrivileges( roundId, @@ -3039,17 +3083,19 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { testCreateRound(); uint32 roundId = fundingPot.getRoundCount(); - uint8 accessId = 2; + uint8 accessId = 1; + uint8 accessType = uint8(ILM_PC_FundingPot_v1.AccessCriteriaType.OPEN); + uint amount = 1000; ( address nftContract, bytes32 merkleRoot, address[] memory allowedAddresses - ) = _helper_createAccessCriteria(accessId, roundId); + ) = _helper_createAccessCriteria(accessType, roundId); fundingPot.setAccessCriteria( - roundId, accessId, nftContract, merkleRoot, allowedAddresses + roundId, accessType, 0, nftContract, merkleRoot, allowedAddresses ); fundingPot.setAccessCriteriaPrivileges( roundId, accessId, 1000, false, 0, 0, 0 @@ -3236,6 +3282,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { fundingPot.setAccessCriteria( roundId, accessCriteriaEnum, + 0, nftContract, merkleRoot, allowedAddresses From 8ad0cd526ec417abb0ee3bdf49580a4b776ca9f9 Mon Sep 17 00:00:00 2001 From: JeffreyJoel Date: Tue, 6 May 2025 20:00:46 +0100 Subject: [PATCH 106/130] fix: fix double underscore styling issue --- src/modules/logicModule/LM_PC_FundingPot_v1.sol | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/modules/logicModule/LM_PC_FundingPot_v1.sol b/src/modules/logicModule/LM_PC_FundingPot_v1.sol index bcecaa22f..941f797e8 100644 --- a/src/modules/logicModule/LM_PC_FundingPot_v1.sol +++ b/src/modules/logicModule/LM_PC_FundingPot_v1.sol @@ -253,7 +253,7 @@ contract LM_PC_FundingPot_v1 is /// @inheritdoc ILM_PC_FundingPot_v1 function getRoundAccessCriteriaPrivileges( uint32 roundId_, - uint8 accessCriteriaId__ + uint8 accessCriteriaId_ ) external view @@ -267,7 +267,7 @@ contract LM_PC_FundingPot_v1 is { Round storage round = rounds[roundId_]; AccessCriteria storage accessCriteria = - round.accessCriterias[accessCriteriaId__]; + round.accessCriterias[accessCriteriaId_]; if (accessCriteria.accessCriteriaType == AccessCriteriaType.UNSET) { return (0, false, 0, 0, 0); @@ -275,7 +275,7 @@ contract LM_PC_FundingPot_v1 is // Store the privileges in a local variable to reduce stack usage. AccessCriteriaPrivileges storage privileges = - roundIdToAccessCriteriaIdToPrivileges[roundId_][accessCriteriaId__]; + roundIdToAccessCriteriaIdToPrivileges[roundId_][accessCriteriaId_]; return ( privileges.personalCap, @@ -912,14 +912,14 @@ contract LM_PC_FundingPot_v1 is /// @param user_ The address of the user to contribute for. /// @param roundId_ The ID of the round to contribute to. /// @param amount_ The amount to contribute. - /// @param accessCriteriaId__ The ID of the access criteria to use for this contribution. + /// @param accessCriteriaId_ The ID of the access criteria to use for this contribution. /// @param canOverrideContributionSpan_ Whether the contribution span can be overridden. /// @param unspentPersonalCap_ The amount of unused capacity from previous rounds. function _validateAndAdjustCapsWithUnspentCap( address user_, uint32 roundId_, uint amount_, - uint8 accessCriteriaId__, + uint8 accessCriteriaId_, bool canOverrideContributionSpan_, uint unspentPersonalCap_ ) internal view returns (uint adjustedAmount) { @@ -956,7 +956,7 @@ contract LM_PC_FundingPot_v1 is // Get the base personal cap for this round and criteria AccessCriteriaPrivileges storage privileges = - roundIdToAccessCriteriaIdToPrivileges[roundId_][accessCriteriaId__]; + roundIdToAccessCriteriaIdToPrivileges[roundId_][accessCriteriaId_]; uint userPersonalCap = privileges.personalCap; // Add unspent capacity if global accumulative caps are enabled From be5acae91c3a6849898d5571f53d1b672c74772b Mon Sep 17 00:00:00 2001 From: JeffreyJoel Date: Tue, 6 May 2025 22:28:13 +0100 Subject: [PATCH 107/130] chore: change 'accessId' to 'accessCriteriaId' in the test file --- .../logicModule/LM_PC_FundingPot_v1.t.sol | 190 ++++++++++-------- 1 file changed, 105 insertions(+), 85 deletions(-) diff --git a/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol b/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol index 293b3cf9a..78624e65d 100644 --- a/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol +++ b/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol @@ -1050,7 +1050,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { testCreateRound(); uint32 roundId = fundingPot.getRoundCount(); - uint8 accessId = 1; + uint8 accessCriteriaId = 1; ( address nftContract, @@ -1072,7 +1072,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { address retrievedNftContract, bytes32 retrievedMerkleRoot, bool hasAccess - ) = fundingPot.getRoundAccessCriteria(uint32(roundId), accessId); + ) = fundingPot.getRoundAccessCriteria(uint32(roundId), accessCriteriaId); assertEq(isOpen, accessCriteriaEnum == 1); assertEq(retrievedNftContract, nftContract); @@ -1138,7 +1138,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { testCreateRound(); uint32 roundId = fundingPot.getRoundCount(); - uint8 accessId = 1; + uint8 accessCriteriaId = 1; uint8 accessType = uint8(ILM_PC_FundingPot_v1.AccessCriteriaType.LIST); ( address nftContract, @@ -1155,18 +1155,18 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { addressesToRemove[1] = contributor2_; fundingPot.removeAllowlistedAddresses( - roundId, accessId, addressesToRemove + roundId, accessCriteriaId, addressesToRemove ); bool hasAccess = fundingPot.exposed_checkAccessCriteriaEligibility( - roundId, accessId, new bytes32[](0), contributor2_ + roundId, accessCriteriaId, new bytes32[](0), contributor2_ ); assertFalse(hasAccess); bool otherAddressesHaveAccess = fundingPot .exposed_checkAccessCriteriaEligibility( - roundId, accessId, new bytes32[](0), address(0x3) + roundId, accessCriteriaId, new bytes32[](0), address(0x3) ); assertTrue(otherAddressesHaveAccess); @@ -1250,7 +1250,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { address[] memory allowedAddresses ) = _helper_createAccessCriteria(accessCriteriaEnum, roundId); - uint8 accessId = 1; + uint8 accessCriteriaId = 1; fundingPot.setAccessCriteria( roundId, @@ -1262,7 +1262,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { ); fundingPot.setAccessCriteriaPrivileges( - roundId, accessId, 1000, false, 0, 0, 0 + roundId, accessCriteriaId, 1000, false, 0, 0, 0 ); ( @@ -1271,7 +1271,9 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { uint start, uint cliff, uint end - ) = fundingPot.getRoundAccessCriteriaPrivileges(roundId, accessId); + ) = fundingPot.getRoundAccessCriteriaPrivileges( + roundId, accessCriteriaId + ); assertEq(personalCap, 1000); assertEq(overrideContributionSpan, false); @@ -1367,7 +1369,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { testCreateRound(); uint32 roundId = fundingPot.getRoundCount(); - uint8 accessId = 1; + uint8 accessCriteriaId = 1; uint8 accessType = uint8(ILM_PC_FundingPot_v1.AccessCriteriaType.OPEN); uint amount = 250; @@ -1381,7 +1383,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { roundId, accessType, 0, nftContract, merkleRoot, allowedAddresses ); fundingPot.setAccessCriteriaPrivileges( - roundId, accessId, 500, false, 0, 0, 0 + roundId, accessCriteriaId, 500, false, 0, 0, 0 ); // Approve @@ -1398,7 +1400,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { vm.prank(contributor1_); fundingPot.contributeToRoundFor( - contributor1_, roundId, amount, accessId, new bytes32[](0) + contributor1_, roundId, amount, accessCriteriaId, new bytes32[](0) ); } @@ -1408,7 +1410,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { testCreateRound(); uint32 roundId = fundingPot.getRoundCount(); - uint8 accessId = 1; + uint8 accessCriteriaId = 1; uint8 accessType = uint8(ILM_PC_FundingPot_v1.AccessCriteriaType.OPEN); uint amount = 250; @@ -1423,7 +1425,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { roundId, accessType, 0, nftContract, merkleRoot, allowedAddresses ); fundingPot.setAccessCriteriaPrivileges( - roundId, accessId, 500, false, 0, 0, 0 + roundId, accessCriteriaId, 500, false, 0, 0, 0 ); (uint roundStart,,,,,,) = fundingPot.getRoundGenericParameters(roundId); @@ -1443,14 +1445,14 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { vm.prank(contributor1_); fundingPot.contributeToRoundFor( - contributor1_, roundId, amount, accessId, new bytes32[](0) + contributor1_, roundId, amount, accessCriteriaId, new bytes32[](0) ); } function testContributeToRoundFor_revertsGivenNFTAccessCriteriaIsNotMet() public { - uint8 accessId = 1; + uint8 accessCriteriaId = 1; uint8 accessType = uint8(ILM_PC_FundingPot_v1.AccessCriteriaType.NFT); _helper_setupRoundWithAccessCriteria(accessType); @@ -1477,7 +1479,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { vm.prank(contributor1_); fundingPot.contributeToRoundFor( - contributor1_, roundId, amount, accessId, new bytes32[](0) + contributor1_, roundId, amount, accessCriteriaId, new bytes32[](0) ); } @@ -1486,7 +1488,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { testCreateRound(); uint32 roundId = fundingPot.getRoundCount(); - uint8 accessId = 1; + uint8 accessCriteriaId = 1; uint8 accessType = uint8(ILM_PC_FundingPot_v1.AccessCriteriaType.MERKLE); uint amount = 250; @@ -1504,7 +1506,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { roundId, accessType, 0, nftContract, merkleRoot, allowedAddresses ); fundingPot.setAccessCriteriaPrivileges( - roundId, accessId, 500, false, 0, 0, 0 + roundId, accessCriteriaId, 500, false, 0, 0, 0 ); (uint roundStart,,,,,,) = fundingPot.getRoundGenericParameters(roundId); @@ -1524,7 +1526,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { vm.prank(contributor1_); fundingPot.contributeToRoundFor( - contributor1_, roundId, amount, accessId, proofB + contributor1_, roundId, amount, accessCriteriaId, proofB ); } @@ -1533,7 +1535,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { testCreateRound(); uint32 roundId = fundingPot.getRoundCount(); - uint8 accessId = 1; + uint8 accessCriteriaId = 1; uint8 accessType = uint8(ILM_PC_FundingPot_v1.AccessCriteriaType.LIST); uint amount = 250; @@ -1547,7 +1549,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { roundId, accessType, 0, nftContract, merkleRoot, allowedAddresses ); fundingPot.setAccessCriteriaPrivileges( - roundId, accessId, 500, false, 0, 0, 0 + roundId, accessCriteriaId, 500, false, 0, 0, 0 ); (uint roundStart,,,,,,) = fundingPot.getRoundGenericParameters(roundId); @@ -1567,7 +1569,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { vm.prank(contributor1_); fundingPot.contributeToRoundFor( - contributor1_, roundId, amount, accessId, new bytes32[](0) + contributor1_, roundId, amount, accessCriteriaId, new bytes32[](0) ); } @@ -1576,7 +1578,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { testCreateRound(); uint32 roundId = fundingPot.getRoundCount(); - uint8 accessId = 1; + uint8 accessCriteriaId = 1; uint8 accessType = uint8(ILM_PC_FundingPot_v1.AccessCriteriaType.NFT); uint amount = 500; @@ -1590,7 +1592,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { roundId, accessType, 0, nftContract, merkleRoot, allowedAddresses ); fundingPot.setAccessCriteriaPrivileges( - roundId, accessId, 500, false, 0, 0, 0 + roundId, accessCriteriaId, 500, false, 0, 0, 0 ); mockNFTContract.mint(contributor1_); @@ -1604,7 +1606,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { vm.prank(contributor1_); fundingPot.contributeToRoundFor( - contributor1_, roundId, amount, accessId, new bytes32[](0) + contributor1_, roundId, amount, accessCriteriaId, new bytes32[](0) ); // Attempt to contribute beyond personal cap @@ -1618,7 +1620,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { vm.prank(contributor1_); fundingPot.contributeToRoundFor( - contributor1_, roundId, 251, accessId, new bytes32[](0) + contributor1_, roundId, 251, accessCriteriaId, new bytes32[](0) ); } @@ -1687,7 +1689,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { function testcontributeToRoundFor_worksGivenAllConditionsMet() public { testCreateRound(); - uint8 accessId = 1; + uint8 accessCriteriaId = 1; uint8 accessType = uint8(ILM_PC_FundingPot_v1.AccessCriteriaType.NFT); uint32 roundId = fundingPot.getRoundCount(); @@ -1703,12 +1705,14 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { roundId, accessType, 0, nftContract, merkleRoot, allowedAddresses ); fundingPot.setAccessCriteriaPrivileges( - roundId, accessId, 1000, false, 0, 0, 0 + roundId, accessCriteriaId, 1000, false, 0, 0, 0 ); mockNFTContract.mint(contributor1_); (bool isEligible, uint remainingAmountAllowedToContribute) = fundingPot - .getUserEligibility(roundId, accessId, new bytes32[](0), contributor1_); + .getUserEligibility( + roundId, accessCriteriaId, new bytes32[](0), contributor1_ + ); assertTrue(isEligible); assertEq(remainingAmountAllowedToContribute, 1000); @@ -1726,7 +1730,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { vm.prank(contributor1_); fundingPot.contributeToRoundFor( - contributor1_, roundId, amount, accessId, new bytes32[](0) + contributor1_, roundId, amount, accessCriteriaId, new bytes32[](0) ); uint totalContributions = @@ -1748,15 +1752,15 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { accessCriteriaEnumNew != accessCriteriaEnumOld && accessCriteriaEnumNew >= 0 && accessCriteriaEnumNew <= 4 ); - uint8 accessId = 1; + uint8 accessCriteriaId = 1; uint8 accessType = uint8(ILM_PC_FundingPot_v1.AccessCriteriaType.NFT); - _helper_setupRoundWithAccessCriteria(accessId); + _helper_setupRoundWithAccessCriteria(accessCriteriaId); uint32 roundId = fundingPot.getRoundCount(); mockNFTContract.mint(contributor1_); fundingPot.setAccessCriteriaPrivileges( - roundId, accessId, 500, false, 0, 0, 0 + roundId, accessCriteriaId, 500, false, 0, 0, 0 ); vm.warp(_defaultRoundParams.roundStart + 1); @@ -1768,7 +1772,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { emit ILM_PC_FundingPot_v1.ContributionMade(roundId, contributor1_, 250); fundingPot.contributeToRoundFor( - contributor1_, roundId, 250, accessId, new bytes32[](0) + contributor1_, roundId, 250, accessCriteriaId, new bytes32[](0) ); vm.stopPrank(); @@ -1785,7 +1789,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { function testContributeToRoundFor_worksGivenMerkleAccessCriteriaMet() public { - uint8 accessId = 1; + uint8 accessCriteriaId = 1; uint8 accessType = uint8(ILM_PC_FundingPot_v1.AccessCriteriaType.MERKLE); _helper_setupRoundWithAccessCriteria(accessType); @@ -1797,7 +1801,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { ); fundingPot.setAccessCriteriaPrivileges( - roundId, accessId, 500, false, 0, 0, 0 + roundId, accessCriteriaId, 500, false, 0, 0, 0 ); vm.warp(_defaultRoundParams.roundStart + 1); @@ -1811,7 +1815,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { roundId, contributor2_, contributionAmount ); fundingPot.contributeToRoundFor( - contributor2_, roundId, contributionAmount, accessId, proofB + contributor2_, roundId, contributionAmount, accessCriteriaId, proofB ); vm.stopPrank(); @@ -1831,7 +1835,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { _defaultRoundParams.roundCap = 150; _defaultRoundParams.autoClosure = true; - uint8 accessId = 1; + uint8 accessCriteriaId = 1; uint8 accessType = uint8(ILM_PC_FundingPot_v1.AccessCriteriaType.NFT); uint32 roundId = fundingPot.createRound( @@ -1856,7 +1860,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { uint personalCap = 200; fundingPot.setAccessCriteriaPrivileges( - roundId, accessId, personalCap, false, 0, 0, 0 + roundId, accessCriteriaId, personalCap, false, 0, 0, 0 ); mockNFTContract.mint(contributor1_); @@ -1867,14 +1871,14 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { vm.startPrank(contributor1_); _token.approve(address(fundingPot), 100); fundingPot.contributeToRoundFor( - contributor1_, roundId, 100, accessId, new bytes32[](0) + contributor1_, roundId, 100, accessCriteriaId, new bytes32[](0) ); vm.stopPrank(); vm.startPrank(contributor2_); _token.approve(address(fundingPot), 100); fundingPot.contributeToRoundFor( - contributor2_, roundId, 100, accessId, new bytes32[](0) + contributor2_, roundId, 100, accessCriteriaId, new bytes32[](0) ); vm.stopPrank(); @@ -1894,7 +1898,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { testCreateRound(); uint32 roundId = fundingPot.getRoundCount(); - uint8 accessId = 1; + uint8 accessCriteriaId = 1; uint8 accessType = uint8(ILM_PC_FundingPot_v1.AccessCriteriaType.NFT); uint firstAmount = 400; @@ -1910,7 +1914,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { roundId, accessType, 0, nftContract, merkleRoot, allowedAddresses ); fundingPot.setAccessCriteriaPrivileges( - roundId, accessId, personalCap, false, 0, 0, 0 + roundId, accessCriteriaId, personalCap, false, 0, 0, 0 ); mockNFTContract.mint(contributor1_); @@ -1930,7 +1934,11 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { // First contribution vm.prank(contributor1_); fundingPot.contributeToRoundFor( - contributor1_, roundId, firstAmount, accessId, new bytes32[](0) + contributor1_, + roundId, + firstAmount, + accessCriteriaId, + new bytes32[](0) ); uint secondAmount = 200; @@ -1943,7 +1951,11 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { vm.prank(contributor1_); fundingPot.contributeToRoundFor( - contributor1_, roundId, secondAmount, accessId, new bytes32[](0) + contributor1_, + roundId, + secondAmount, + accessCriteriaId, + new bytes32[](0) ); uint totalContribution = fundingPot.exposed_getUserContributionToRound( @@ -1959,7 +1971,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { testCreateRound(); uint32 roundId = fundingPot.getRoundCount(); - uint8 accessId = 1; + uint8 accessCriteriaId = 1; uint8 accessType = uint8(ILM_PC_FundingPot_v1.AccessCriteriaType.NFT); uint amount = 250; @@ -1975,7 +1987,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { // Set privileges with override capability fundingPot.setAccessCriteriaPrivileges( - roundId, accessId, 500, true, 0, 0, 0 + roundId, accessCriteriaId, 500, true, 0, 0, 0 ); mockNFTContract.mint(contributor1_); @@ -1997,7 +2009,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { // This should succeed despite being after round end, due to override privilege vm.prank(contributor1_); fundingPot.contributeToRoundFor( - contributor1_, roundId, amount, accessId, new bytes32[](0) + contributor1_, roundId, amount, accessCriteriaId, new bytes32[](0) ); // Verify the contribution was recorded @@ -2135,7 +2147,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { ); uint32 round1Id = fundingPot.getRoundCount(); - uint8 accessId = 1; + uint8 accessCriteriaId = 1; uint8 accessType = uint8(ILM_PC_FundingPot_v1.AccessCriteriaType.OPEN); ( @@ -2147,7 +2159,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { round1Id, accessType, 0, nftContract, merkleRoot, allowedAddresses ); fundingPot.setAccessCriteriaPrivileges( - round1Id, accessId, 500, false, 0, 0, 0 + round1Id, accessCriteriaId, 500, false, 0, 0, 0 ); // Round 2 with a different cap @@ -2166,7 +2178,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { round2Id, accessType, 0, nftContract, merkleRoot, allowedAddresses ); fundingPot.setAccessCriteriaPrivileges( - round2Id, accessId, 500, false, 0, 0, 0 + round2Id, accessCriteriaId, 500, false, 0, 0, 0 ); // Round 1: Multiple users contribute, but don't reach the cap @@ -2175,14 +2187,14 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { vm.startPrank(contributor1_); _token.approve(address(fundingPot), 300); fundingPot.contributeToRoundFor( - contributor1_, round1Id, 300, accessId, new bytes32[](0) + contributor1_, round1Id, 300, accessCriteriaId, new bytes32[](0) ); vm.stopPrank(); vm.startPrank(contributor2_); _token.approve(address(fundingPot), 200); fundingPot.contributeToRoundFor( - contributor2_, round1Id, 200, accessId, new bytes32[](0) + contributor2_, round1Id, 200, accessCriteriaId, new bytes32[](0) ); vm.stopPrank(); @@ -2192,14 +2204,14 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { vm.startPrank(contributor2_); _token.approve(address(fundingPot), 400); fundingPot.contributeToRoundFor( - contributor2_, round2Id, 400, accessId, new bytes32[](0) + contributor2_, round2Id, 400, accessCriteriaId, new bytes32[](0) ); vm.stopPrank(); vm.startPrank(contributor3_); _token.approve(address(fundingPot), 300); fundingPot.contributeToRoundFor( - contributor3_, round2Id, 300, accessId, new bytes32[](0) + contributor3_, round2Id, 300, accessCriteriaId, new bytes32[](0) ); vm.stopPrank(); @@ -2423,7 +2435,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { testCreateRound(); uint32 roundId = fundingPot.getRoundCount(); - uint8 accessId = 1; + uint8 accessCriteriaId = 1; uint8 accessType = uint8(ILM_PC_FundingPot_v1.AccessCriteriaType.OPEN); ( address nftContract, @@ -2435,7 +2447,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { roundId, accessType, 0, nftContract, merkleRoot, allowedAddresses ); fundingPot.setAccessCriteriaPrivileges( - roundId, accessId, 1000, false, 0, 0, 0 + roundId, accessCriteriaId, 1000, false, 0, 0, 0 ); // Warp to round start @@ -2446,7 +2458,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { vm.startPrank(contributor1_); _token.approve(address(fundingPot), 1000); fundingPot.contributeToRoundFor( - contributor1_, roundId, 1000, accessId, new bytes32[](0) + contributor1_, roundId, 1000, accessCriteriaId, new bytes32[](0) ); vm.stopPrank(); @@ -2461,7 +2473,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { testCreateRound(); uint32 roundId = fundingPot.getRoundCount(); - uint8 accessId = 1; + uint8 accessCriteriaId = 1; uint8 accessType = uint8(ILM_PC_FundingPot_v1.AccessCriteriaType.OPEN); ( @@ -2474,7 +2486,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { roundId, accessType, 0, nftContract, merkleRoot, allowedAddresses ); fundingPot.setAccessCriteriaPrivileges( - roundId, accessId, 1000, false, 0, 0, 0 + roundId, accessCriteriaId, 1000, false, 0, 0, 0 ); // Make a contribution @@ -2485,7 +2497,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { vm.startPrank(contributor1_); _token.approve(address(fundingPot), 500); fundingPot.contributeToRoundFor( - contributor1_, roundId, 500, accessId, new bytes32[](0) + contributor1_, roundId, 500, accessCriteriaId, new bytes32[](0) ); vm.stopPrank(); @@ -2503,7 +2515,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { testCreateRound(); uint32 roundId = fundingPot.getRoundCount(); - uint8 accessId = 1; + uint8 accessCriteriaId = 1; uint8 accessType = uint8(ILM_PC_FundingPot_v1.AccessCriteriaType.NFT); uint amount = 1000; @@ -2518,7 +2530,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { roundId, accessType, 0, nftContract, merkleRoot, allowedAddresses ); fundingPot.setAccessCriteriaPrivileges( - roundId, accessId, 1000, false, 0, 0, 0 + roundId, accessCriteriaId, 1000, false, 0, 0, 0 ); mockNFTContract.mint(contributor1_); @@ -2532,7 +2544,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { vm.prank(contributor1_); fundingPot.contributeToRoundFor( - contributor1_, roundId, amount, accessId, new bytes32[](0) + contributor1_, roundId, amount, accessCriteriaId, new bytes32[](0) ); assertEq(fundingPot.isRoundClosed(roundId), false); @@ -2544,7 +2556,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { testEditRound(); uint32 roundId = fundingPot.getRoundCount(); - uint8 accessId = 1; + uint8 accessCriteriaId = 1; uint8 accessType = uint8(ILM_PC_FundingPot_v1.AccessCriteriaType.OPEN); uint amount = 2000; @@ -2560,7 +2572,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { ); fundingPot.setAccessCriteriaPrivileges( - roundId, accessId, 2000, false, 0, 0, 0 + roundId, accessCriteriaId, 2000, false, 0, 0, 0 ); mockNFTContract.mint(contributor1_); @@ -2573,7 +2585,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { vm.prank(contributor1_); fundingPot.contributeToRoundFor( - contributor1_, roundId, amount, accessId, new bytes32[](0) + contributor1_, roundId, amount, accessCriteriaId, new bytes32[](0) ); assertEq(fundingPot.isRoundClosed(roundId), true); @@ -2584,7 +2596,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { uint32 roundId = fundingPot.getRoundCount(); // Set up access criteria - uint8 accessId = 1; + uint8 accessCriteriaId = 1; uint8 accessType = uint8(ILM_PC_FundingPot_v1.AccessCriteriaType.OPEN); ( @@ -2597,7 +2609,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { roundId, accessType, 0, nftContract, merkleRoot, allowedAddresses ); fundingPot.setAccessCriteriaPrivileges( - roundId, accessId, 1000, false, 0, 0, 0 + roundId, accessCriteriaId, 1000, false, 0, 0, 0 ); // Warp to round start @@ -2608,21 +2620,21 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { vm.startPrank(contributor1_); _token.approve(address(fundingPot), 500); fundingPot.contributeToRoundFor( - contributor1_, roundId, 500, accessId, new bytes32[](0) + contributor1_, roundId, 500, accessCriteriaId, new bytes32[](0) ); vm.stopPrank(); vm.startPrank(contributor2_); _token.approve(address(fundingPot), 200); fundingPot.contributeToRoundFor( - contributor2_, roundId, 200, accessId, new bytes32[](0) + contributor2_, roundId, 200, accessCriteriaId, new bytes32[](0) ); vm.stopPrank(); vm.startPrank(contributor3_); _token.approve(address(fundingPot), 300); fundingPot.contributeToRoundFor( - contributor3_, roundId, 300, accessId, new bytes32[](0) + contributor3_, roundId, 300, accessCriteriaId, new bytes32[](0) ); vm.stopPrank(); @@ -2772,20 +2784,20 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { function testFuzz_validateAndAdjustCapsWithUnspentCap( uint32 roundId_, uint amount_, - uint8 accessId_, + uint8 accessCriteriaId_, bool canOverrideContributionSpan_, uint unspentPersonalCap_ ) external { vm.assume(roundId_ > 0 && roundId_ >= fundingPot.getRoundCount()); vm.assume(amount_ <= 1000); - vm.assume(accessId_ <= 4); + vm.assume(accessCriteriaId_ <= 4); vm.assume(unspentPersonalCap_ >= 0); try fundingPot.exposed_validateAndAdjustCapsWithUnspentCap( contributor1_, roundId_, amount_, - accessId_, + accessCriteriaId_, canOverrideContributionSpan_, unspentPersonalCap_ ) returns (uint adjustedAmount) { @@ -2930,7 +2942,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { ); // Set access criteria and privileges - uint8 accessId = 1; + uint8 accessCriteriaId = 1; uint8 accessType = uint8(ILM_PC_FundingPot_v1.AccessCriteriaType.OPEN); ( @@ -2942,14 +2954,18 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { roundId, accessType, 0, nftContract, merkleRoot, allowedAddresses ); fundingPot.setAccessCriteriaPrivileges( - roundId, accessId, 1000, false, 0, 0, 0 + roundId, accessCriteriaId, 1000, false, 0, 0, 0 ); vm.warp(params.roundStart + 1); vm.startPrank(contributor1_); _token.approve(address(fundingPot), params.roundCap); fundingPot.contributeToRoundFor( - contributor1_, roundId, params.roundCap, accessId, new bytes32[](0) + contributor1_, + roundId, + params.roundCap, + accessCriteriaId, + new bytes32[](0) ); vm.stopPrank(); @@ -3015,7 +3031,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { ); // Set access criteria and privileges - uint8 accessId = 1; + uint8 accessCriteriaId = 1; uint8 accessType = uint8(ILM_PC_FundingPot_v1.AccessCriteriaType.OPEN); ( @@ -3028,7 +3044,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { ); fundingPot.setAccessCriteriaPrivileges( roundId, - accessId, + accessCriteriaId, 1000, false, 0, // no start @@ -3044,7 +3060,11 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { vm.startPrank(contributor1_); _token.approve(address(fundingPot), params.roundCap); fundingPot.contributeToRoundFor( - contributor1_, roundId, params.roundCap, accessId, new bytes32[](0) + contributor1_, + roundId, + params.roundCap, + accessCriteriaId, + new bytes32[](0) ); vm.stopPrank(); @@ -3083,7 +3103,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { testCreateRound(); uint32 roundId = fundingPot.getRoundCount(); - uint8 accessId = 1; + uint8 accessCriteriaId = 1; uint8 accessType = uint8(ILM_PC_FundingPot_v1.AccessCriteriaType.OPEN); uint amount = 1000; @@ -3098,7 +3118,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { roundId, accessType, 0, nftContract, merkleRoot, allowedAddresses ); fundingPot.setAccessCriteriaPrivileges( - roundId, accessId, 1000, false, 0, 0, 0 + roundId, accessCriteriaId, 1000, false, 0, 0, 0 ); mockNFTContract.mint(contributor1_); @@ -3112,7 +3132,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { vm.prank(contributor1_); fundingPot.contributeToRoundFor( - contributor1_, roundId, amount, accessId, new bytes32[](0) + contributor1_, roundId, amount, accessCriteriaId, new bytes32[](0) ); assertTrue( From 57d3a44c27a2e441560e06a76d2d64a6bafe4c4e Mon Sep 17 00:00:00 2001 From: JeffreyJoel Date: Wed, 7 May 2025 21:51:29 +0100 Subject: [PATCH 108/130] chore: convert necessary getter functions from internal to external --- .../logicModule/LM_PC_FundingPot_v1.sol | 55 ++++++-------- .../interfaces/ILM_PC_FundingPot_v1.sol | 17 +++++ .../logicModule/LM_PC_FundingPot_v1.t.sol | 75 ++++++------------- .../LM_PC_FundingPot_v1_Exposed.sol | 22 ------ 4 files changed, 65 insertions(+), 104 deletions(-) diff --git a/src/modules/logicModule/LM_PC_FundingPot_v1.sol b/src/modules/logicModule/LM_PC_FundingPot_v1.sol index 941f797e8..129a489de 100644 --- a/src/modules/logicModule/LM_PC_FundingPot_v1.sol +++ b/src/modules/logicModule/LM_PC_FundingPot_v1.sol @@ -332,7 +332,7 @@ contract LM_PC_FundingPot_v1 is AccessCriteriaPrivileges storage privileges = roundIdToAccessCriteriaIdToPrivileges[roundId_][accessCriteriaId_]; uint userPersonalCap = privileges.personalCap; - uint userContribution = _getUserContributionToRound(roundId_, user_); + uint userContribution = roundIdToUserToContribution[roundId_][user_]; uint personalCapRemaining = userPersonalCap > userContribution ? userPersonalCap - userContribution @@ -352,6 +352,24 @@ contract LM_PC_FundingPot_v1 is } } + /// @inheritdoc ILM_PC_FundingPot_v1 + function getTotalRoundContribution(uint32 roundId_) + external + view + returns (uint) + { + return roundIdToTotalContributions[roundId_]; + } + + /// @inheritdoc ILM_PC_FundingPot_v1 + function getUserContributionToRound(uint32 roundId_, address user_) + external + view + returns (uint) + { + return roundIdToUserToContribution[roundId_][user_]; + } + // ------------------------------------------------------------------------- // Public - Mutating @@ -632,7 +650,7 @@ contract LM_PC_FundingPot_v1 is .accessCriteriaId]; uint userContribution = - _getUserContributionToRound(uint32(roundCap.roundId), user_); + roundIdToUserToContribution[roundCap.roundId][user_]; uint personalCap = privileges.personalCap; if (userContribution < personalCap) { @@ -928,7 +946,7 @@ contract LM_PC_FundingPot_v1 is Round storage round = rounds[roundId_]; if (!canOverrideContributionSpan_ && round.roundCap > 0) { - uint totalRoundContribution = _getTotalRoundContribution(roundId_); + uint totalRoundContribution = roundIdToTotalContributions[roundId_]; uint effectiveRoundCap = round.roundCap; // If global accumulative caps are enabled, @@ -952,7 +970,7 @@ contract LM_PC_FundingPot_v1 is // Check and adjust for personal cap uint userPreviousContribution = - _getUserContributionToRound(roundId_, user_); + roundIdToUserToContribution[roundId_][user_]; // Get the base personal cap for this round and criteria AccessCriteriaPrivileges storage privileges = @@ -1024,7 +1042,7 @@ contract LM_PC_FundingPot_v1 is Round storage prevRound = rounds[i]; if (!prevRound.globalAccumulativeCaps) continue; - uint prevRoundTotal = _getTotalRoundContribution(i); + uint prevRoundTotal = roundIdToTotalContributions[i]; if (prevRoundTotal < prevRound.roundCap) { unusedCapacityFromPrevious += (prevRound.roundCap - prevRoundTotal); @@ -1033,31 +1051,6 @@ contract LM_PC_FundingPot_v1 is return unusedCapacityFromPrevious; } - /// @notice Retrieves the total contribution for a specific round. - /// @dev Returns the accumulated contributions for the given round. - /// @param roundId_ The ID of the round to check contributions for. - /// @return The total contributions for the specified round. - function _getTotalRoundContribution(uint32 roundId_) - internal - view - returns (uint) - { - return roundIdToTotalContributions[roundId_]; - } - - /// @notice Retrieves the contribution amount for a specific user in a round. - /// @dev Returns the individual user's contribution for the given round. - /// @param roundId_ The ID of the round to check contributions for. - /// @param user_ The address of the user. - /// @return The user's contribution amount for the specified round. - function _getUserContributionToRound(uint32 roundId_, address user_) - internal - view - returns (uint) - { - return roundIdToUserToContribution[roundId_][user_]; - } - /// @notice Verifies NFT ownership for access control. /// @dev Safely checks the NFT balance of a user using a try-catch block. /// @param nftContract_ Address of the NFT contract. @@ -1285,7 +1278,7 @@ contract LM_PC_FundingPot_v1 is } function _buyBondingCurveToken(uint32 roundId_) internal { - uint totalContributions = _getTotalRoundContribution(roundId_); + uint totalContributions = roundIdToTotalContributions[roundId_]; if (totalContributions == 0) { revert Module__LM_PC_FundingPot__NoContributions(); } diff --git a/src/modules/logicModule/interfaces/ILM_PC_FundingPot_v1.sol b/src/modules/logicModule/interfaces/ILM_PC_FundingPot_v1.sol index 7f44488ef..ca49238ba 100644 --- a/src/modules/logicModule/interfaces/ILM_PC_FundingPot_v1.sol +++ b/src/modules/logicModule/interfaces/ILM_PC_FundingPot_v1.sol @@ -388,6 +388,23 @@ interface ILM_PC_FundingPot_v1 is IERC20PaymentClientBase_v2 { view returns (bool isEligible, uint remainingAmountAllowedToContribute); + /// @notice Retrieves the total contribution for a specific round. + /// @param roundId_ The ID of the round to check contributions for. + /// @return The total contributions for the specified round. + function getTotalRoundContribution(uint32 roundId_) + external + view + returns (uint); + + /// @notice Retrieves the contribution amount for a specific user in a round. + /// @param roundId_ The ID of the round to check contributions for. + /// @param user_ The address of the user. + /// @return The user's contribution amount for the specified round. + function getUserContributionToRound(uint32 roundId_, address user_) + external + view + returns (uint); + // ------------------------------------------------------------------------- // Public - Mutating diff --git a/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol b/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol index 78624e65d..a04207a1f 100644 --- a/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol +++ b/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol @@ -1733,13 +1733,12 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { contributor1_, roundId, amount, accessCriteriaId, new bytes32[](0) ); - uint totalContributions = - fundingPot.exposed_getTotalRoundContributions(roundId); + uint totalContributions = fundingPot.getTotalRoundContribution(roundId); assertEq(totalContributions, amount); - uint personalContributions = fundingPot - .exposed_getUserContributionToRound(roundId, contributor1_); + uint personalContributions = + fundingPot.getUserContributionToRound(roundId, contributor1_); assertEq(personalContributions, amount); } @@ -1776,13 +1775,11 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { ); vm.stopPrank(); - uint userContribution = fundingPot.exposed_getUserContributionToRound( - roundId, contributor1_ - ); + uint userContribution = + fundingPot.getUserContributionToRound(roundId, contributor1_); assertEq(userContribution, 250); - uint totalContributions = - fundingPot.exposed_getTotalRoundContributions(roundId); + uint totalContributions = fundingPot.getTotalRoundContribution(roundId); assertEq(totalContributions, 250); } @@ -1820,13 +1817,11 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { vm.stopPrank(); - uint userContribution = fundingPot.exposed_getUserContributionToRound( - roundId, contributor2_ - ); + uint userContribution = + fundingPot.getUserContributionToRound(roundId, contributor2_); assertEq(userContribution, contributionAmount); - uint totalContributions = - fundingPot.exposed_getTotalRoundContributions(roundId); + uint totalContributions = fundingPot.getTotalRoundContribution(roundId); assertEq(totalContributions, contributionAmount); } @@ -1882,13 +1877,11 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { ); vm.stopPrank(); - uint contribution = fundingPot.exposed_getUserContributionToRound( - roundId, contributor2_ - ); + uint contribution = + fundingPot.getUserContributionToRound(roundId, contributor2_); assertEq(contribution, 50); - uint totalContribution = - fundingPot.exposed_getTotalRoundContributions(roundId); + uint totalContribution = fundingPot.getTotalRoundContribution(roundId); assertEq(totalContribution, _defaultRoundParams.roundCap); assertTrue(fundingPot.isRoundClosed(roundId)); } @@ -1958,9 +1951,8 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { new bytes32[](0) ); - uint totalContribution = fundingPot.exposed_getUserContributionToRound( - roundId, contributor1_ - ); + uint totalContribution = + fundingPot.getUserContributionToRound(roundId, contributor1_); assertEq(totalContribution, personalCap); } @@ -2013,8 +2005,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { ); // Verify the contribution was recorded - uint totalContribution = - fundingPot.exposed_getTotalRoundContributions(roundId); + uint totalContribution = fundingPot.getTotalRoundContribution(roundId); assertEq(totalContribution, amount); } @@ -2116,17 +2107,11 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { vm.stopPrank(); assertEq( - fundingPot.exposed_getUserContributionToRound( - round1Id, contributor1_ - ), - 200 + fundingPot.getUserContributionToRound(round1Id, contributor1_), 200 ); assertEq( - fundingPot.exposed_getUserContributionToRound( - round2Id, contributor1_ - ), - 700 + fundingPot.getUserContributionToRound(round2Id, contributor1_), 700 ); } @@ -2215,35 +2200,23 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { ); vm.stopPrank(); - assertEq(fundingPot.exposed_getTotalRoundContributions(round1Id), 500); + assertEq(fundingPot.getTotalRoundContribution(round1Id), 500); assertEq( - fundingPot.exposed_getUserContributionToRound( - round1Id, contributor1_ - ), - 300 + fundingPot.getUserContributionToRound(round1Id, contributor1_), 300 ); assertEq( - fundingPot.exposed_getUserContributionToRound( - round1Id, contributor2_ - ), - 200 + fundingPot.getUserContributionToRound(round1Id, contributor2_), 200 ); - assertEq(fundingPot.exposed_getTotalRoundContributions(round2Id), 700); + assertEq(fundingPot.getTotalRoundContribution(round2Id), 700); assertEq( - fundingPot.exposed_getUserContributionToRound( - round2Id, contributor2_ - ), - 400 + fundingPot.getUserContributionToRound(round2Id, contributor2_), 400 ); assertEq( - fundingPot.exposed_getUserContributionToRound( - round2Id, contributor3_ - ), - 300 + fundingPot.getUserContributionToRound(round2Id, contributor3_), 300 ); - assertEq(fundingPot.exposed_getTotalRoundContributions(round2Id), 700); + assertEq(fundingPot.getTotalRoundContribution(round2Id), 700); } // ------------------------------------------------------------------------- diff --git a/test/unit/modules/logicModule/LM_PC_FundingPot_v1_Exposed.sol b/test/unit/modules/logicModule/LM_PC_FundingPot_v1_Exposed.sol index a7cc8d29b..350b75753 100644 --- a/test/unit/modules/logicModule/LM_PC_FundingPot_v1_Exposed.sol +++ b/test/unit/modules/logicModule/LM_PC_FundingPot_v1_Exposed.sol @@ -10,28 +10,6 @@ contract LM_PC_FundingPot_v1_Exposed is LM_PC_FundingPot_v1 { // Use the `exposed_` prefix for functions to expose internal functions for // testing. - /** - * @notice Exposes the internal _getTotalRoundContribution function for testing - */ - function exposed_getTotalRoundContributions(uint32 roundId_) - external - view - returns (uint) - { - return _getTotalRoundContribution(roundId_); - } - - /** - * @notice Exposes the internal _getUserContributionToRound function for testing - */ - function exposed_getUserContributionToRound(uint32 roundId_, address user_) - external - view - returns (uint) - { - return _getUserContributionToRound(roundId_, user_); - } - /** * @notice Exposes the internal _validTimes function for testing */ From 3928fc1cec897d3ff23ea063809d6e1d30587a5d Mon Sep 17 00:00:00 2001 From: Zuhaib Mohammed Date: Tue, 13 May 2025 11:54:40 +0530 Subject: [PATCH 109/130] fix: add missing assertion for e2e test --- test/e2e/logicModule/FundingPotE2E.t.sol | 53 ++++++++++++++++++++---- 1 file changed, 44 insertions(+), 9 deletions(-) diff --git a/test/e2e/logicModule/FundingPotE2E.t.sol b/test/e2e/logicModule/FundingPotE2E.t.sol index f00b5e120..0c26be95e 100644 --- a/test/e2e/logicModule/FundingPotE2E.t.sol +++ b/test/e2e/logicModule/FundingPotE2E.t.sol @@ -2,6 +2,7 @@ pragma solidity ^0.8.0; // Internal Dependencies + import { E2ETest, IOrchestratorFactory_v1, @@ -251,24 +252,30 @@ contract FundingPotE2E is E2ETest { contributionToken.mint(contributor2, 1000e18); contributionToken.mint(contributor3, 1000e18); + uint contributor1Amount = 400e18; + uint contributor2Amount = 500e18; + uint contributor3Amount = 750e18; + uint totalContributionForRound1 = + contributor1Amount + contributor2Amount; + vm.startPrank(contributor1); - contributionToken.approve(address(fundingPot), 400e18); + contributionToken.approve(address(fundingPot), contributor1Amount); fundingPot.contributeToRoundFor( - contributor1, round1Id, 400e18, 1, new bytes32[](0) + contributor1, round1Id, contributor1Amount, 1, new bytes32[](0) ); vm.stopPrank(); vm.startPrank(contributor2); - contributionToken.approve(address(fundingPot), 600e18); + contributionToken.approve(address(fundingPot), contributor2Amount); fundingPot.contributeToRoundFor( - contributor2, round1Id, 600e18, 1, new bytes32[](0) + contributor2, round1Id, contributor2Amount, 1, new bytes32[](0) ); vm.stopPrank(); vm.startPrank(contributor3); - contributionToken.approve(address(fundingPot), 750e18); + contributionToken.approve(address(fundingPot), contributor3Amount); fundingPot.contributeToRoundFor( - contributor3, round2Id, 750e18, 1, new bytes32[](0) + contributor3, round2Id, contributor3Amount, 1, new bytes32[](0) ); vm.stopPrank(); @@ -302,9 +309,37 @@ contract FundingPotE2E is E2ETest { vm.prank(contributor3); paymentProcessor.claimAll(address(fundingPot)); - // 12. Assert that contributors received their tokens - assertGt(issuanceToken.balanceOf(contributor1), 0); - assertGt(issuanceToken.balanceOf(contributor2), 0); + // 12. Verify proportional distribution for round 1 + uint contributor1Issuance = issuanceToken.balanceOf(contributor1); + uint contributor2Issuance = issuanceToken.balanceOf(contributor2); + uint totalIssuanceForRound1 = + contributor1Issuance + contributor2Issuance; + + // Calculate the expected proportions (scaled by 1e18 for precision) + uint contributor1ExpectedProportion = + (contributor1Amount * 1e18) / totalContributionForRound1; + uint contributor1ActualProportion = + (contributor1Issuance * 1e18) / totalIssuanceForRound1; + + uint contributor2ExpectedProportion = + (contributor2Amount * 1e18) / totalContributionForRound1; + uint contributor2ActualProportion = + (contributor2Issuance * 1e18) / totalIssuanceForRound1; + + // Using 0.001e18 (0.1%) as the maximum relative error + assertApproxEqRel( + contributor1ActualProportion, + contributor1ExpectedProportion, + 0.001e18 + ); + + assertApproxEqRel( + contributor2ActualProportion, + contributor2ExpectedProportion, + 0.001e18 + ); + + // verify round 2 contributor assertGt(issuanceToken.balanceOf(contributor3), 0); } } From 099fe3fddff2d3be23cf4e8bc542fa788ba48221 Mon Sep 17 00:00:00 2001 From: Zuhaib Mohammed Date: Tue, 13 May 2025 19:58:38 +0530 Subject: [PATCH 110/130] fix: add optimization to reduce contract size --- .../logicModule/LM_PC_FundingPot_v1.sol | 54 +++++++++++++------ .../interfaces/ILM_PC_FundingPot_v1.sol | 6 --- test/e2e/logicModule/FundingPotE2E.t.sol | 4 -- 3 files changed, 38 insertions(+), 26 deletions(-) diff --git a/src/modules/logicModule/LM_PC_FundingPot_v1.sol b/src/modules/logicModule/LM_PC_FundingPot_v1.sol index 129a489de..4eaceaa73 100644 --- a/src/modules/logicModule/LM_PC_FundingPot_v1.sol +++ b/src/modules/logicModule/LM_PC_FundingPot_v1.sol @@ -24,7 +24,7 @@ import {SafeERC20} from "@oz/token/ERC20/utils/SafeERC20.sol"; import {ERC165Upgradeable} from "@oz-up/utils/introspection/ERC165Upgradeable.sol"; -import "@oz/utils/cryptography/MerkleProof.sol"; +import {MerkleProof} from "@oz/utils/cryptography/MerkleProof.sol"; import {EnumerableSet} from "@oz/utils/structs/EnumerableSet.sol"; /** @@ -383,7 +383,9 @@ contract LM_PC_FundingPot_v1 is bool autoClosure_, bool globalAccumulativeCaps_ ) external onlyModuleRole(FUNDING_POT_ADMIN_ROLE) returns (uint32) { - roundCount++; + unchecked { + roundCount++; + } uint32 roundId = roundCount; @@ -472,7 +474,9 @@ contract LM_PC_FundingPot_v1 is // If accessCriteriaId_ is 0, create a new access criteria // Otherwise, edit the existing one if (accessCriteriaId_ == 0) { - criteriaId = ++roundIdToNextAccessCriteriaId[roundId_]; + unchecked { + criteriaId = ++roundIdToNextAccessCriteriaId[roundId_]; + } } else { criteriaId = accessCriteriaId_; isEdit = true; @@ -522,8 +526,10 @@ contract LM_PC_FundingPot_v1 is } else if (accessCriteriaType == AccessCriteriaType.LIST) { // For LIST type, update the allowed addresses for (uint i = 0; i < allowedAddresses_.length; i++) { - round.accessCriterias[criteriaId].allowedAddresses[allowedAddresses_[i]] - = true; + unchecked { + round.accessCriterias[criteriaId].allowedAddresses[allowedAddresses_[i]] + = true; + } } } @@ -555,8 +561,10 @@ contract LM_PC_FundingPot_v1 is _validateEditRoundParameters(round); for (uint i = 0; i < addressesToRemove_.length; i++) { - round.accessCriterias[accessCriteriaId_].allowedAddresses[addressesToRemove_[i]] - = false; + unchecked { + round.accessCriterias[accessCriteriaId_].allowedAddresses[addressesToRemove_[i]] + = false; + } } emit AllowlistedAddressesRemoved( @@ -962,7 +970,10 @@ contract LM_PC_FundingPot_v1 is } // Allow the user to contribute up to the remaining round cap - uint remainingRoundCap = effectiveRoundCap - totalRoundContribution; + uint remainingRoundCap; + unchecked { + remainingRoundCap = effectiveRoundCap - totalRoundContribution; + } if (adjustedAmount > remainingRoundCap) { adjustedAmount = remainingRoundCap; } @@ -1167,9 +1178,12 @@ contract LM_PC_FundingPot_v1 is if (contributionByAccessCriteria == 0) continue; - uint tokensForThisAccessCriteria = ( - contributionByAccessCriteria * tokensBought - ) / totalContributions; + uint tokensForThisAccessCriteria; + unchecked { + tokensForThisAccessCriteria = ( + contributionByAccessCriteria * tokensBought + ) / totalContributions; + } _createAndAddPaymentOrder( roundId_, @@ -1206,24 +1220,32 @@ contract LM_PC_FundingPot_v1 is if (start_ > 0) { flags |= bytes32(uint(1) << FLAG_START); data[flagCount] = bytes32(start_); - flagCount++; + unchecked { + flagCount++; + } } if (cliff_ > 0) { flags |= bytes32(uint(1) << FLAG_CLIFF); data[flagCount] = bytes32(cliff_); - flagCount++; + unchecked { + flagCount++; + } } if (end_ > 0) { flags |= bytes32(uint(1) << FLAG_END); data[flagCount] = bytes32(end_); - flagCount++; + unchecked { + flagCount++; + } } finalData = new bytes32[](flagCount); - for (uint8 j = 0; j < flagCount; j++) { - finalData[j] = data[j]; + for (uint8 j = 0; j < flagCount; ++j) { + unchecked { + finalData[j] = data[j]; + } } return (flags, finalData); diff --git a/src/modules/logicModule/interfaces/ILM_PC_FundingPot_v1.sol b/src/modules/logicModule/interfaces/ILM_PC_FundingPot_v1.sol index ca49238ba..960123380 100644 --- a/src/modules/logicModule/interfaces/ILM_PC_FundingPot_v1.sol +++ b/src/modules/logicModule/interfaces/ILM_PC_FundingPot_v1.sol @@ -232,9 +232,6 @@ interface ILM_PC_FundingPot_v1 is IERC20PaymentClientBase_v2 { /// @notice Round has already started and cannot be modified. error Module__LM_PC_FundingPot__RoundAlreadyStarted(); - /// @notice Hook function is required when a hook contract is provided. - error Module__LM_PC_FundingPot__HookFunctionRequiredWithContract(); - /// @notice Thrown when a hook contract is specified without a hook function. error Module__LM_PC_FundingPot__HookFunctionRequiredWithHookContract(); @@ -250,9 +247,6 @@ interface ILM_PC_FundingPot_v1 is IERC20PaymentClientBase_v2 { /// @notice Invalid access criteria ID. error Module__LM_PC_FundingPot__InvalidAccessCriteriaId(); - /// @notice Cannot set Privileges for open access criteria. - error Module__LM_PC_FundingPot__CannotSetPrivilegesForOpenAccessCriteria(); - /// @notice Invalid times. error Module__LM_PC_FundingPot__InvalidTimes(); diff --git a/test/e2e/logicModule/FundingPotE2E.t.sol b/test/e2e/logicModule/FundingPotE2E.t.sol index 0c26be95e..60d55dd57 100644 --- a/test/e2e/logicModule/FundingPotE2E.t.sol +++ b/test/e2e/logicModule/FundingPotE2E.t.sol @@ -144,10 +144,6 @@ contract FundingPotE2E is E2ETest { paymentProcessor = PP_Streaming_v2(address(orchestrator.paymentProcessor())); - // Define payment processor - PP_Streaming_v2 paymentProcessor = - PP_Streaming_v2(address(orchestrator.paymentProcessor())); - // Get the funding pot address[] memory modulesList = orchestrator.listModules(); for (uint i; i < modulesList.length; ++i) { From 3f1bd06992d4af762e582fdf28eae27fa62b353d Mon Sep 17 00:00:00 2001 From: Fabian Scherer Date: Thu, 15 May 2025 23:39:42 +0200 Subject: [PATCH 111/130] feat: accumulation mode --- .../logicModule/LM_PC_FundingPot_v1.sol | 114 ++++++++++++------ .../interfaces/ILM_PC_FundingPot_v1.sol | 35 ++++-- test/e2e/logicModule/FundingPotE2E.t.sol | 4 +- .../logicModule/LM_PC_FundingPot_v1.t.sol | 110 ++++++++--------- 4 files changed, 156 insertions(+), 107 deletions(-) diff --git a/src/modules/logicModule/LM_PC_FundingPot_v1.sol b/src/modules/logicModule/LM_PC_FundingPot_v1.sol index 4eaceaa73..022c28f57 100644 --- a/src/modules/logicModule/LM_PC_FundingPot_v1.sol +++ b/src/modules/logicModule/LM_PC_FundingPot_v1.sol @@ -195,7 +195,7 @@ contract LM_PC_FundingPot_v1 is address hookContract, bytes memory hookFunction, bool autoClosure, - bool globalAccumulativeCaps + AccumulationMode accumulationMode ) { Round storage round = rounds[roundId_]; @@ -206,7 +206,7 @@ contract LM_PC_FundingPot_v1 is round.hookContract, round.hookFunction, round.autoClosure, - round.globalAccumulativeCaps + round.accumulationMode ); } @@ -381,7 +381,7 @@ contract LM_PC_FundingPot_v1 is address hookContract_, bytes memory hookFunction_, bool autoClosure_, - bool globalAccumulativeCaps_ + AccumulationMode accumulationMode_ ) external onlyModuleRole(FUNDING_POT_ADMIN_ROLE) returns (uint32) { unchecked { roundCount++; @@ -396,7 +396,7 @@ contract LM_PC_FundingPot_v1 is round.hookContract = hookContract_; round.hookFunction = hookFunction_; round.autoClosure = autoClosure_; - round.globalAccumulativeCaps = globalAccumulativeCaps_; + round.accumulationMode = accumulationMode_; _validateRoundParameters(round); @@ -408,7 +408,7 @@ contract LM_PC_FundingPot_v1 is hookContract_, hookFunction_, autoClosure_, - globalAccumulativeCaps_ + accumulationMode_ ); return uint32(roundId); @@ -423,7 +423,7 @@ contract LM_PC_FundingPot_v1 is address hookContract_, bytes memory hookFunction_, bool autoClosure_, - bool globalAccumulativeCaps_ + AccumulationMode accumulationMode_ ) external onlyModuleRole(FUNDING_POT_ADMIN_ROLE) { Round storage round = rounds[roundId_]; @@ -435,7 +435,7 @@ contract LM_PC_FundingPot_v1 is round.hookContract = hookContract_; round.hookFunction = hookFunction_; round.autoClosure = autoClosure_; - round.globalAccumulativeCaps = globalAccumulativeCaps_; + round.accumulationMode = accumulationMode_; _validateRoundParameters(round); @@ -447,7 +447,7 @@ contract LM_PC_FundingPot_v1 is hookContract_, hookFunction_, autoClosure_, - globalAccumulativeCaps_ + accumulationMode_ ); } @@ -642,7 +642,7 @@ contract LM_PC_FundingPot_v1 is unspentPersonalRoundCaps_[i]; Round storage prevRound = rounds[roundCap.roundId]; - if (!prevRound.globalAccumulativeCaps) continue; + if (prevRound.accumulationMode == AccumulationMode.Disabled) continue; // Verify the user was eligible for this access criteria in the previous round bool isEligible = _checkAccessCriteriaEligibility( @@ -957,49 +957,81 @@ contract LM_PC_FundingPot_v1 is uint totalRoundContribution = roundIdToTotalContributions[roundId_]; uint effectiveRoundCap = round.roundCap; - // If global accumulative caps are enabled, - // adjust the round cap to acommodate unused capacity from previous rounds - if (round.globalAccumulativeCaps) { + // If total accumulative caps are enabled for this round, + // adjust the effective round cap to accommodate unused capacity from previous rounds + // NOTE: This part is relevant for Total/All modes, but the check itself is needed here. + if ( + round.accumulationMode == AccumulationMode.Total + || round.accumulationMode == AccumulationMode.All + ) { uint unusedCapacityFromPrevious = _calculateUnusedCapacityFromPreviousRounds(roundId_); effectiveRoundCap += unusedCapacityFromPrevious; } if (totalRoundContribution >= effectiveRoundCap) { - revert Module__LM_PC_FundingPot__RoundCapReached(); + // If user tries to contribute a non-zero amount when cap is full, revert. + if (amount_ > 0) { + revert Module__LM_PC_FundingPot__RoundCapReached(); + } + // If user tries to contribute zero when cap is full, allow adjustedAmount = 0. + adjustedAmount = 0; + } else { + // Cap is not full, calculate remaining and clamp if necessary + uint remainingRoundCap = effectiveRoundCap - totalRoundContribution; + if (adjustedAmount > remainingRoundCap) { + adjustedAmount = remainingRoundCap; + } } + } - // Allow the user to contribute up to the remaining round cap - uint remainingRoundCap; - unchecked { - remainingRoundCap = effectiveRoundCap - totalRoundContribution; - } - if (adjustedAmount > remainingRoundCap) { - adjustedAmount = remainingRoundCap; - } + // If round cap check clamped adjustedAmount to 0, and original amount was > 0, we already reverted. + // If original amount was 0, adjustedAmount is 0, so we can proceed to personal cap check which will also result in 0. + // If adjustedAmount > 0, proceed to personal cap check. + if (adjustedAmount == 0 && amount_ > 0) { + // This state should ideally not be reached due to the revert above if cap was full. + // But as a safeguard, if amount_ was > 0 and adjustedAmount is now 0, return 0. + return 0; } - // Check and adjust for personal cap - uint userPreviousContribution = - roundIdToUserToContribution[roundId_][user_]; + // --- Personal Cap Check --- + // Only proceed if adjustedAmount wasn't already set to 0 by round cap or initial amount. + if (adjustedAmount > 0) { + uint userPreviousContribution = roundIdToUserToContribution[roundId_][user_]; - // Get the base personal cap for this round and criteria - AccessCriteriaPrivileges storage privileges = - roundIdToAccessCriteriaIdToPrivileges[roundId_][accessCriteriaId_]; - uint userPersonalCap = privileges.personalCap; + AccessCriteriaPrivileges storage privileges = + roundIdToAccessCriteriaIdToPrivileges[roundId_][accessCriteriaId_]; + uint userPersonalCap = privileges.personalCap; - // Add unspent capacity if global accumulative caps are enabled - if (round.globalAccumulativeCaps) { - userPersonalCap += unspentPersonalCap_; - } + // Add unspent personal capacity if personal accumulation is enabled for this round (Personal or All) + // Explicitly exclude Total mode here. + if ( + round.accumulationMode == AccumulationMode.Personal + || round.accumulationMode == AccumulationMode.All + ) { + userPersonalCap += unspentPersonalCap_; + } - if (userPreviousContribution + adjustedAmount > userPersonalCap) { - if (userPreviousContribution < userPersonalCap) { - adjustedAmount = userPersonalCap - userPreviousContribution; - } else { - revert Module__LM_PC_FundingPot__PersonalCapReached(); + // Check if the already potentially-clamped amount exceeds personal cap + if (userPreviousContribution + adjustedAmount > userPersonalCap) { + // If user hasn't reached personal cap yet, clamp further to remaining personal cap. + if (userPreviousContribution < userPersonalCap) { + uint remainingPersonalCap = userPersonalCap - userPreviousContribution; + // Ensure we don't accidentally increase amount, only clamp down. + if (remainingPersonalCap < adjustedAmount) { + adjustedAmount = remainingPersonalCap; + } + } else { // User is already at or over personal cap. + // If they tried to contribute a non-zero amount initially, revert. + if (amount_ > 0) { + revert Module__LM_PC_FundingPot__PersonalCapReached(); + } + // If initial amount was 0, just ensure adjustedAmount remains 0. + adjustedAmount = 0; + } } } + // --- End Personal Cap Check --- ' return adjustedAmount; } @@ -1051,7 +1083,13 @@ contract LM_PC_FundingPot_v1 is // Iterate through all previous rounds (1 to roundId_-1) for (uint32 i = 1; i < roundId_; ++i) { Round storage prevRound = rounds[i]; - if (!prevRound.globalAccumulativeCaps) continue; + // Only consider previous rounds that allowed total accumulation + if ( + prevRound.accumulationMode != AccumulationMode.Total + && prevRound.accumulationMode != AccumulationMode.All + ) { + continue; + } uint prevRoundTotal = roundIdToTotalContributions[i]; if (prevRoundTotal < prevRound.roundCap) { diff --git a/src/modules/logicModule/interfaces/ILM_PC_FundingPot_v1.sol b/src/modules/logicModule/interfaces/ILM_PC_FundingPot_v1.sol index 960123380..ffba3e925 100644 --- a/src/modules/logicModule/interfaces/ILM_PC_FundingPot_v1.sol +++ b/src/modules/logicModule/interfaces/ILM_PC_FundingPot_v1.sol @@ -16,7 +16,7 @@ interface ILM_PC_FundingPot_v1 is IERC20PaymentClientBase_v2 { /// @param hookContract Address of an optional hook contract to be called after round closure. /// @param hookFunction Encoded function call to be executed on the `hookContract` after round closure. /// @param autoClosure Indicates whether the hook closure coincides with the contribution span end. - /// @param globalAccumulativeCaps Indicates whether contribution caps accumulate globally across rounds. + /// @param accumulationMode Defines how caps accumulate across rounds (see AccumulationMode enum). /// @param accessCriterias Mapping of access criteria IDs to their respective access criteria. struct Round { uint roundStart; @@ -25,7 +25,7 @@ interface ILM_PC_FundingPot_v1 is IERC20PaymentClientBase_v2 { address hookContract; bytes hookFunction; bool autoClosure; - bool globalAccumulativeCaps; + AccumulationMode accumulationMode; mapping(uint32 id => AccessCriteria) accessCriterias; } @@ -94,6 +94,17 @@ interface ILM_PC_FundingPot_v1 is IERC20PaymentClientBase_v2 { } + /// @notice Enum used to define how caps accumulate across rounds. + /// @dev Determines whether unused personal caps or round caps from previous + /// rounds can affect the limits of the current round. + enum AccumulationMode { + Disabled, // 0 - No accumulation. Personal and round caps are isolated to this round. + Personal, // 1 - Only personal caps roll over from previous compatible rounds. Round cap is isolated. + Total, // 2 - Only total round caps expand based on previous compatible rounds' undersubscription. Personal caps are isolated. + All // 3 - Both personal caps roll over and total round caps expand based on previous compatible rounds. + + } + // ------------------------------------------------------------------------- // Events @@ -106,7 +117,7 @@ interface ILM_PC_FundingPot_v1 is IERC20PaymentClientBase_v2 { /// @param hookContract_ The address of an optional hook contract for custom logic. /// @param hookFunction_ The encoded function call for the hook. /// @param autoClosure_ A boolean indicating whether a specific closure mechanism is enabled. - /// @param globalAccumulativeCaps_ A boolean indicating whether global accumulative caps are enforced. + /// @param accumulationMode_ Defines how caps accumulate across rounds (see AccumulationMode enum). event RoundCreated( uint indexed roundId_, uint roundStart_, @@ -115,7 +126,7 @@ interface ILM_PC_FundingPot_v1 is IERC20PaymentClientBase_v2 { address hookContract_, bytes hookFunction_, bool autoClosure_, - bool globalAccumulativeCaps_ + AccumulationMode accumulationMode_ ); /// @notice Emitted when an existing round is edited. @@ -127,7 +138,7 @@ interface ILM_PC_FundingPot_v1 is IERC20PaymentClientBase_v2 { /// @param hookContract_ The address of an optional hook contract for custom logic. /// @param hookFunction_ The updated encoded function call for the hook. /// @param autoClosure_ A boolean indicating whether a specific closure mechanism is enabled. - /// @param globalAccumulativeCaps_ A boolean indicating whether global accumulative caps are enforced. + /// @param accumulationMode_ The accumulation mode for this round (see AccumulationMode enum). event RoundEdited( uint indexed roundId_, uint roundStart_, @@ -136,7 +147,7 @@ interface ILM_PC_FundingPot_v1 is IERC20PaymentClientBase_v2 { address hookContract_, bytes hookFunction_, bool autoClosure_, - bool globalAccumulativeCaps_ + AccumulationMode accumulationMode_ ); /// @notice Emitted when access criteria is set for a round. @@ -303,7 +314,7 @@ interface ILM_PC_FundingPot_v1 is IERC20PaymentClientBase_v2 { /// @return hookContract_ The address of the hook contract. /// @return hookFunction_ The encoded function call for the hook. /// @return autoClosure_ Whether hook closure coincides with contribution span end. - /// @return globalAccumulativeCaps_ Whether caps accumulate globally across rounds. + /// @return accumulationMode_ The accumulation mode for the round. function getRoundGenericParameters(uint32 roundId_) external view @@ -314,7 +325,7 @@ interface ILM_PC_FundingPot_v1 is IERC20PaymentClientBase_v2 { address hookContract_, bytes memory hookFunction_, bool autoClosure_, - bool globalAccumulativeCaps_ + AccumulationMode accumulationMode_ ); /// @notice Retrieves the access criteria for a specific funding round. @@ -410,7 +421,7 @@ interface ILM_PC_FundingPot_v1 is IERC20PaymentClientBase_v2 { /// @param hookContract_ Address of contract to call after round closure. /// @param hookFunction_ Encoded function call for the hook. /// @param autoClosure_ Whether hook closure coincides with contribution span end. - /// @param globalAccumulativeCaps_ Whether caps accumulate globally. + /// @param accumulationMode_ Defines how caps accumulate across rounds (see AccumulationMode enum). /// @return The ID of the newly created round. function createRound( uint roundStart_, @@ -419,7 +430,7 @@ interface ILM_PC_FundingPot_v1 is IERC20PaymentClientBase_v2 { address hookContract_, bytes memory hookFunction_, bool autoClosure_, - bool globalAccumulativeCaps_ + AccumulationMode accumulationMode_ ) external returns (uint32); /// @notice Edits an existing funding round. @@ -431,7 +442,7 @@ interface ILM_PC_FundingPot_v1 is IERC20PaymentClientBase_v2 { /// @param hookContract_ New hook contract address. /// @param hookFunction_ New encoded function call. /// @param autoClosure_ New closure mechanism setting. - /// @param globalAccumulativeCaps_ New global accumulative caps setting. + /// @param accumulationMode_ New accumulation mode setting. function editRound( uint32 roundId_, uint roundStart_, @@ -440,7 +451,7 @@ interface ILM_PC_FundingPot_v1 is IERC20PaymentClientBase_v2 { address hookContract_, bytes memory hookFunction_, bool autoClosure_, - bool globalAccumulativeCaps_ + AccumulationMode accumulationMode_ ) external; /// @notice Set Access Control Check. diff --git a/test/e2e/logicModule/FundingPotE2E.t.sol b/test/e2e/logicModule/FundingPotE2E.t.sol index 60d55dd57..5c3cdcbb7 100644 --- a/test/e2e/logicModule/FundingPotE2E.t.sol +++ b/test/e2e/logicModule/FundingPotE2E.t.sol @@ -178,7 +178,7 @@ contract FundingPotE2E is E2ETest { address(0), // no hook bytes(""), // no hook function false, // auto closure - false // no global caps + ILM_PC_FundingPot_v1.AccumulationMode.Disabled // no global caps ); // Round 2 @@ -189,7 +189,7 @@ contract FundingPotE2E is E2ETest { address(0), // no hook bytes(""), // no hook function true, // auto closure - false // no global caps + ILM_PC_FundingPot_v1.AccumulationMode.Disabled // no global caps ); // 4. Set access criteria for the rounds diff --git a/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol b/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol index a04207a1f..876bf3af2 100644 --- a/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol +++ b/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol @@ -75,7 +75,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { address hookContract; bytes hookFunction; bool autoClosure; - bool globalAccumulativeCaps; + ILM_PC_FundingPot_v1.AccumulationMode accumulationMode; } ERC721Mock mockNFTContract = new ERC721Mock("NFT Mock", "NFT"); @@ -117,7 +117,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { hookContract: address(0), hookFunction: bytes(""), autoClosure: false, - globalAccumulativeCaps: false + accumulationMode: ILM_PC_FundingPot_v1.AccumulationMode.All }); // Initialize edited round parameters @@ -128,7 +128,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { address(0x1), bytes("test"), true, - true + ILM_PC_FundingPot_v1.AccumulationMode.All ); } @@ -209,7 +209,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { params.hookContract, params.hookFunction, params.autoClosure, - params.globalAccumulativeCaps + params.accumulationMode ); vm.stopPrank(); } @@ -235,7 +235,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { params.hookContract, params.hookFunction, params.autoClosure, - params.globalAccumulativeCaps + params.accumulationMode ); } @@ -260,7 +260,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { params.hookContract, params.hookFunction, params.autoClosure, - params.globalAccumulativeCaps + params.accumulationMode ); } @@ -285,7 +285,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { params.hookContract, params.hookFunction, params.autoClosure, - params.globalAccumulativeCaps + params.accumulationMode ); } @@ -308,7 +308,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { params.hookContract, params.hookFunction, params.autoClosure, - params.globalAccumulativeCaps + params.accumulationMode ); } @@ -332,7 +332,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { params.hookContract, params.hookFunction, params.autoClosure, - params.globalAccumulativeCaps + params.accumulationMode ); } @@ -352,7 +352,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { params.hookContract, params.hookFunction, params.autoClosure, - params.globalAccumulativeCaps + params.accumulationMode ); uint32 roundId = fundingPot.getRoundCount(); @@ -365,7 +365,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { address storedHookContract, bytes memory storedHookFunction, bool storedAutoClosure, - bool storedGlobalAccumulativeCaps + ILM_PC_FundingPot_v1.AccumulationMode storedAccumulationMode ) = fundingPot.getRoundGenericParameters(roundId); // Compare with expected values @@ -375,7 +375,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { assertEq(storedHookContract, params.hookContract); assertEq(storedHookFunction, params.hookFunction); assertEq(storedAutoClosure, params.autoClosure); - assertEq(storedGlobalAccumulativeCaps, params.globalAccumulativeCaps); + assertEq(uint(storedAccumulationMode), uint(params.accumulationMode)); } /* Test editRound() @@ -421,7 +421,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { ├── hookContract should be updated to the new value ├── hookFunction should be updated to the new value ├── autoClosure should be updated to the new value - └── globalAccumulativeCaps should be updated to the new value + └── accumulationMode should be updated to the new value */ function testEditRound_revertsGivenUserIsNotFundingPotAdmin(address user_) @@ -438,7 +438,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { hookContract: address(0x1), hookFunction: bytes("test"), autoClosure: true, - globalAccumulativeCaps: true + accumulationMode: ILM_PC_FundingPot_v1.AccumulationMode.All }); vm.startPrank(user_); @@ -458,7 +458,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { params.hookContract, params.hookFunction, params.autoClosure, - params.globalAccumulativeCaps + params.accumulationMode ); vm.stopPrank(); } @@ -474,7 +474,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { hookContract: address(0x1), hookFunction: bytes("test"), autoClosure: true, - globalAccumulativeCaps: true + accumulationMode: ILM_PC_FundingPot_v1.AccumulationMode.All }); vm.expectRevert( @@ -492,7 +492,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { params.hookContract, params.hookFunction, params.autoClosure, - params.globalAccumulativeCaps + params.accumulationMode ); } @@ -508,7 +508,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { params.hookContract, params.hookFunction, params.autoClosure, - params.globalAccumulativeCaps + params.accumulationMode ) = fundingPot.getRoundGenericParameters(roundId); vm.warp(params.roundStart + 1); @@ -519,7 +519,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { hookContract: address(0x1), hookFunction: bytes("test"), autoClosure: true, - globalAccumulativeCaps: true + accumulationMode: ILM_PC_FundingPot_v1.AccumulationMode.All }); vm.expectRevert( @@ -537,7 +537,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { params_.hookContract, params_.hookFunction, params_.autoClosure, - params_.globalAccumulativeCaps + params_.accumulationMode ); } @@ -566,7 +566,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { _editedRoundParams.hookContract, _editedRoundParams.hookFunction, _editedRoundParams.autoClosure, - _editedRoundParams.globalAccumulativeCaps + _editedRoundParams.accumulationMode ); } @@ -581,7 +581,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { hookContract: address(0x1), hookFunction: bytes("test"), autoClosure: true, - globalAccumulativeCaps: true + accumulationMode: ILM_PC_FundingPot_v1.AccumulationMode.All }); vm.expectRevert( @@ -600,7 +600,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { params.hookContract, params.hookFunction, params.autoClosure, - params.globalAccumulativeCaps + params.accumulationMode ); } @@ -630,7 +630,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { address(0x1), bytes("test"), true, - true + ILM_PC_FundingPot_v1.AccumulationMode.All ); vm.expectRevert( @@ -649,7 +649,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { params.hookContract, params.hookFunction, params.autoClosure, - params.globalAccumulativeCaps + params.accumulationMode ); } @@ -666,7 +666,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { address(1), bytes(""), true, - true + ILM_PC_FundingPot_v1.AccumulationMode.All ); vm.expectRevert( @@ -685,7 +685,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { params.hookContract, params.hookFunction, params.autoClosure, - params.globalAccumulativeCaps + params.accumulationMode ); } @@ -702,7 +702,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { address(0), bytes("test"), true, - true + ILM_PC_FundingPot_v1.AccumulationMode.All ); vm.expectRevert( @@ -721,7 +721,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { params.hookContract, params.hookFunction, params.autoClosure, - params.globalAccumulativeCaps + params.accumulationMode ); } @@ -736,7 +736,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { ├── hookContract should be updated to the new value ├── hookFunction should be updated to the new value ├── autoClosure should be updated to the new value - └── globalAccumulativeCaps should be updated to the new value + └── accumulationMode should be updated to the new value */ function testEditRound() public { @@ -753,7 +753,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { params.hookContract, params.hookFunction, params.autoClosure, - params.globalAccumulativeCaps + params.accumulationMode ); // Retrieve the stored parameters @@ -764,7 +764,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { address storedHookContract, bytes memory storedHookFunction, bool storedAutoClosure, - bool storedGlobalAccumulativeCaps + ILM_PC_FundingPot_v1.AccumulationMode storedAccumulationMode ) = fundingPot.getRoundGenericParameters(uint32(lastRoundId)); // Compare with expected values @@ -774,7 +774,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { assertEq(storedHookContract, params.hookContract); assertEq(storedHookFunction, params.hookFunction); assertEq(storedAutoClosure, params.autoClosure); - assertEq(storedGlobalAccumulativeCaps, params.globalAccumulativeCaps); + assertEq(uint(storedAccumulationMode), uint(params.accumulationMode)); } /* Test setAccessCriteria() @@ -1198,7 +1198,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { _defaultRoundParams.hookContract, _defaultRoundParams.hookFunction, _defaultRoundParams.autoClosure, - _defaultRoundParams.globalAccumulativeCaps + _defaultRoundParams.accumulationMode ); ( @@ -1241,7 +1241,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { _defaultRoundParams.hookContract, _defaultRoundParams.hookFunction, _defaultRoundParams.autoClosure, - _defaultRoundParams.globalAccumulativeCaps + _defaultRoundParams.accumulationMode ); ( @@ -1302,7 +1302,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { params.hookContract, params.hookFunction, params.autoClosure, - params.globalAccumulativeCaps + params.accumulationMode ); ( @@ -1840,7 +1840,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { _defaultRoundParams.hookContract, _defaultRoundParams.hookFunction, _defaultRoundParams.autoClosure, - _defaultRoundParams.globalAccumulativeCaps + _defaultRoundParams.accumulationMode ); ( @@ -2012,7 +2012,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { function testContributeToRoundFor_worksGivenPersonalCapAccumulation() public { - _defaultRoundParams.globalAccumulativeCaps = true; + _defaultRoundParams.accumulationMode = ILM_PC_FundingPot_v1.AccumulationMode.Personal; fundingPot.createRound( _defaultRoundParams.roundStart, _defaultRoundParams.roundEnd, @@ -2020,7 +2020,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { _defaultRoundParams.hookContract, _defaultRoundParams.hookFunction, _defaultRoundParams.autoClosure, - _defaultRoundParams.globalAccumulativeCaps + _defaultRoundParams.accumulationMode ); uint32 round1Id = fundingPot.getRoundCount(); @@ -2055,7 +2055,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { _defaultRoundParams.hookContract, _defaultRoundParams.hookFunction, _defaultRoundParams.autoClosure, - _defaultRoundParams.globalAccumulativeCaps + _defaultRoundParams.accumulationMode ); uint32 round2Id = fundingPot.getRoundCount(); @@ -2118,7 +2118,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { function testContributeToRoundFor_worksGivenTotalRoundCapAccumulation() public { - _defaultRoundParams.globalAccumulativeCaps = true; + _defaultRoundParams.accumulationMode = ILM_PC_FundingPot_v1.AccumulationMode.All; // Create Round 1 fundingPot.createRound( @@ -2128,7 +2128,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { _defaultRoundParams.hookContract, _defaultRoundParams.hookFunction, _defaultRoundParams.autoClosure, - _defaultRoundParams.globalAccumulativeCaps + _defaultRoundParams.accumulationMode ); uint32 round1Id = fundingPot.getRoundCount(); @@ -2156,7 +2156,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { _defaultRoundParams.hookContract, _defaultRoundParams.hookFunction, _defaultRoundParams.autoClosure, - _defaultRoundParams.globalAccumulativeCaps + _defaultRoundParams.accumulationMode ); uint32 round2Id = fundingPot.getRoundCount(); fundingPot.setAccessCriteria( @@ -2327,7 +2327,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { address(failingHook), abi.encodeWithSignature("executeHook()"), _defaultRoundParams.autoClosure, - _defaultRoundParams.globalAccumulativeCaps + _defaultRoundParams.accumulationMode ); ( @@ -2831,7 +2831,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { _defaultRoundParams.hookContract, _defaultRoundParams.hookFunction, _defaultRoundParams.autoClosure, - _defaultRoundParams.globalAccumulativeCaps + ILM_PC_FundingPot_v1.AccumulationMode.Disabled ); // round 2 (with accumulation) @@ -2842,7 +2842,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { _defaultRoundParams.hookContract, _defaultRoundParams.hookFunction, _defaultRoundParams.autoClosure, - true // globalAccumulativeCaps on + ILM_PC_FundingPot_v1.AccumulationMode.All // globalAccumulativeCaps on ); // round 3 @@ -2853,7 +2853,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { _defaultRoundParams.hookContract, _defaultRoundParams.hookFunction, _defaultRoundParams.autoClosure, - true + ILM_PC_FundingPot_v1.AccumulationMode.All ); // Calculate unused capacity @@ -2911,7 +2911,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { params.hookContract, params.hookFunction, params.autoClosure, - params.globalAccumulativeCaps + params.accumulationMode ); // Set access criteria and privileges @@ -2958,7 +2958,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { params.hookContract, params.hookFunction, params.autoClosure, - params.globalAccumulativeCaps + params.accumulationMode ); // Move time past end time @@ -2981,7 +2981,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { params.hookContract, params.hookFunction, params.autoClosure, - params.globalAccumulativeCaps + params.accumulationMode ); assertFalse(fundingPot.exposed_checkRoundClosureConditions(roundId)); @@ -3000,7 +3000,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { params.hookContract, params.hookFunction, params.autoClosure, - params.globalAccumulativeCaps + params.accumulationMode ); // Set access criteria and privileges @@ -3057,7 +3057,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { params.hookContract, params.hookFunction, params.autoClosure, - params.globalAccumulativeCaps + params.accumulationMode ); // Should be false before end time @@ -3149,7 +3149,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { address hookContract_, bytes memory hookFunction_, bool autoClosure_, - bool globalAccumulativeCaps_ + ILM_PC_FundingPot_v1.AccumulationMode accumulationMode_ ) internal pure returns (RoundParams memory) { return RoundParams({ roundStart: roundStart_, @@ -3158,7 +3158,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { hookContract: hookContract_, hookFunction: hookFunction_, autoClosure: autoClosure_, - globalAccumulativeCaps: globalAccumulativeCaps_ + accumulationMode: accumulationMode_ }); } @@ -3263,7 +3263,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { _defaultRoundParams.hookContract, _defaultRoundParams.hookFunction, _defaultRoundParams.autoClosure, - _defaultRoundParams.globalAccumulativeCaps + _defaultRoundParams.accumulationMode ); ( From 8682631116befee989367e87cb123ee993c280b5 Mon Sep 17 00:00:00 2001 From: Fabian Scherer Date: Fri, 16 May 2025 00:00:09 +0200 Subject: [PATCH 112/130] chore: adds tests for accumulation mode behavior --- .../logicModule/LM_PC_FundingPot_v1.t.sol | 342 ++++++++++++++++++ 1 file changed, 342 insertions(+) diff --git a/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol b/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol index 876bf3af2..758a211c5 100644 --- a/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol +++ b/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol @@ -3281,4 +3281,346 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { allowedAddresses ); } + + function testContribute_PersonalMode_AccumulatesPersonalOnly() public { + // 1. Create the first round with AccumulationMode.Personal + _defaultRoundParams.accumulationMode = ILM_PC_FundingPot_v1.AccumulationMode.Personal; + + fundingPot.createRound( + _defaultRoundParams.roundStart, + _defaultRoundParams.roundEnd, + 1000, // Round cap of 1000 + _defaultRoundParams.hookContract, + _defaultRoundParams.hookFunction, + _defaultRoundParams.autoClosure, + _defaultRoundParams.accumulationMode + ); + uint32 round1Id = fundingPot.getRoundCount(); + + // Set up access criteria for round 1 + uint8 accessCriteriaId = 1; // Open access + ( + address nftContract, + bytes32 merkleRoot, + address[] memory allowedAddresses + ) = _helper_createAccessCriteria(uint8(ILM_PC_FundingPot_v1.AccessCriteriaType.OPEN), round1Id); + + fundingPot.setAccessCriteria( + round1Id, + uint8(ILM_PC_FundingPot_v1.AccessCriteriaType.OPEN), // accessCriteriaType + 0, // accessCriteriaId (0 for new) + nftContract, + merkleRoot, + allowedAddresses + ); + + // Set a personal cap of 500 for round 1 + fundingPot.setAccessCriteriaPrivileges( + round1Id, accessCriteriaId, 500, false, 0, 0, 0 + ); + + // 2. Create the second round, also with AccumulationMode.Personal + // Use different start and end times to avoid overlap + RoundParams memory params = _helper_createEditRoundParams( + _defaultRoundParams.roundStart + 3 days, + _defaultRoundParams.roundEnd + 3 days, + 500, // Round cap of 500 + _defaultRoundParams.hookContract, + _defaultRoundParams.hookFunction, + _defaultRoundParams.autoClosure, + ILM_PC_FundingPot_v1.AccumulationMode.Personal + ); + + fundingPot.createRound( + params.roundStart, + params.roundEnd, + params.roundCap, + params.hookContract, + params.hookFunction, + params.autoClosure, + params.accumulationMode + ); + uint32 round2Id = fundingPot.getRoundCount(); + + // Set up access criteria for round 2 + fundingPot.setAccessCriteria( + round2Id, + uint8(ILM_PC_FundingPot_v1.AccessCriteriaType.OPEN), // accessCriteriaType + 0, // accessCriteriaId (0 for new) + nftContract, + merkleRoot, + allowedAddresses + ); + + // Set a personal cap of 400 for round 2 + fundingPot.setAccessCriteriaPrivileges( + round2Id, accessCriteriaId, 400, false, 0, 0, 0 + ); + + // First round contribution: user contributes 200 out of their 500 personal cap + vm.warp(_defaultRoundParams.roundStart + 1); + + vm.startPrank(contributor1_); + _token.approve(address(fundingPot), 1000); + fundingPot.contributeToRoundFor( + contributor1_, round1Id, 200, accessCriteriaId, new bytes32[](0) + ); + vm.stopPrank(); + + // Verify contribution to round 1 + assertEq( + fundingPot.getUserContributionToRound( + round1Id, contributor1_ + ), + 200 + ); + + // Move to round 2 + vm.warp(_defaultRoundParams.roundStart + 3 days + 1); + + // ------------ PART 1: VERIFY PERSONAL CAP ACCUMULATION ------------ + // Create unspent capacity structure + ILM_PC_FundingPot_v1.UnspentPersonalRoundCap[] memory unspentCaps = + new ILM_PC_FundingPot_v1.UnspentPersonalRoundCap[](1); + unspentCaps[0] = ILM_PC_FundingPot_v1.UnspentPersonalRoundCap({ + roundId: round1Id, + accessCriteriaId: accessCriteriaId, + merkleProof: new bytes32[](0) + }); + + // Try to contribute more than the round 2 personal cap (400) + // In Personal mode, this should succeed up to the personal cap (400) + unspent from round 1 (300) = 700 + // But capped by round cap of 500 + vm.startPrank(contributor1_); + fundingPot.contributeToRoundFor( + contributor1_, + round2Id, + 450, // More than the personal cap of round 2 + accessCriteriaId, + new bytes32[](0), + unspentCaps + ); + vm.stopPrank(); + + // Verify contributions to round 2 - should be more than the personal cap of round 2 (400) + // This verifies personal caps DO accumulate + uint contributionAmount = fundingPot.getUserContributionToRound( + round2Id, contributor1_ + ); + assertEq(contributionAmount, 450); + assertTrue(contributionAmount > 400, "Personal cap should accumulate"); + + // ------------ PART 2: VERIFY TOTAL CAP NON-ACCUMULATION ------------ + // Attempt to contribute more than the remaining round cap + vm.startPrank(contributor2_); + _token.approve(address(fundingPot), 200); + + // Contributor 2 attempts to contribute 100. + // Since contributor1 contributed 450 and round cap is 500, only 50 is remaining. + // The contribution should be clamped to 50. + fundingPot.contributeToRoundFor( + contributor2_, round2Id, 100, accessCriteriaId, new bytes32[](0) + ); + // Verify contributor 2's contribution was clamped to the remaining 50. + assertEq(fundingPot.getUserContributionToRound(round2Id, contributor2_), 50); + vm.stopPrank(); + + // Verify total contributions to round 2 is exactly the round cap (450 + 50 = 500). + assertEq(fundingPot.getTotalRoundContribution(round2Id), 500); + + // Additional contributor3 should not be able to contribute anything as the cap is full. + // Attempting to contribute when the cap is already full should revert. + vm.startPrank(contributor3_); + _token.approve(address(fundingPot), 100); + + // Expect revert because the round cap (500) is already met. + vm.expectRevert( + abi.encodeWithSelector( + ILM_PC_FundingPot_v1.Module__LM_PC_FundingPot__RoundCapReached.selector + ) + ); + fundingPot.contributeToRoundFor( + contributor3_, round2Id, 1, accessCriteriaId, new bytes32[](0) + ); + vm.stopPrank(); + + // Final check that total contributions remain at the round cap. + assertEq(fundingPot.getTotalRoundContribution(round2Id), 500); + } + + function testContribute_TotalMode_AccumulatesTotalOnly() public { + // 1. Create the first round with AccumulationMode.Total + _defaultRoundParams.accumulationMode = ILM_PC_FundingPot_v1.AccumulationMode.Total; + + fundingPot.createRound( + _defaultRoundParams.roundStart, + _defaultRoundParams.roundEnd, + 1000, // Round 1 cap of 1000 + _defaultRoundParams.hookContract, + _defaultRoundParams.hookFunction, + _defaultRoundParams.autoClosure, + _defaultRoundParams.accumulationMode + ); + uint32 round1Id = fundingPot.getRoundCount(); + + // Set up access criteria for round 1 (Open) + uint8 accessCriteriaId = 1; + (address nftContract, bytes32 merkleRoot, address[] memory allowedAddresses) = + _helper_createAccessCriteria(uint8(ILM_PC_FundingPot_v1.AccessCriteriaType.OPEN), round1Id); + + fundingPot.setAccessCriteria( + round1Id, + uint8(ILM_PC_FundingPot_v1.AccessCriteriaType.OPEN), // accessCriteriaType + 0, // accessCriteriaId (0 for new) + nftContract, + merkleRoot, + allowedAddresses + ); + + // Set a personal cap of 800 for round 1 + fundingPot.setAccessCriteriaPrivileges( + round1Id, accessCriteriaId, 800, false, 0, 0, 0 + ); + + // 2. Create the second round, also with AccumulationMode.Total + RoundParams memory params = _helper_createEditRoundParams( + _defaultRoundParams.roundStart + 3 days, + _defaultRoundParams.roundEnd + 3 days, + 500, // Round 2 base cap of 500 + _defaultRoundParams.hookContract, + _defaultRoundParams.hookFunction, + _defaultRoundParams.autoClosure, + ILM_PC_FundingPot_v1.AccumulationMode.Total + ); + + fundingPot.createRound( + params.roundStart, + params.roundEnd, + params.roundCap, + params.hookContract, + params.hookFunction, + params.autoClosure, + params.accumulationMode + ); + uint32 round2Id = fundingPot.getRoundCount(); + + // Set up access criteria for round 2 (Open) + fundingPot.setAccessCriteria( + round2Id, + uint8(ILM_PC_FundingPot_v1.AccessCriteriaType.OPEN), // accessCriteriaType + 0, // accessCriteriaId (0 for new) + nftContract, + merkleRoot, + allowedAddresses + ); + + // Set a personal cap of 300 for round 2 + fundingPot.setAccessCriteriaPrivileges( + round2Id, accessCriteriaId, 300, false, 0, 0, 0 + ); + + // Round 1 contribution: contributor1 contributes 600 (less than round cap 1000, less than personal 800) + // Undersubscription: 1000 - 600 = 400 + vm.warp(_defaultRoundParams.roundStart + 1); + vm.startPrank(contributor1_); + _token.approve(address(fundingPot), 1000); + fundingPot.contributeToRoundFor( + contributor1_, round1Id, 600, accessCriteriaId, new bytes32[](0) + ); + vm.stopPrank(); + + // Verify contribution to round 1 + assertEq(fundingPot.getUserContributionToRound(round1Id, contributor1_), 600); + assertEq(fundingPot.getTotalRoundContribution(round1Id), 600); + + // Move to round 2 + vm.warp(_defaultRoundParams.roundStart + 3 days + 1); + + // ------------ PART 1: VERIFY TOTAL CAP ACCUMULATION ------------ + // Effective Round 2 Cap = Base Cap (500) + Unused from Round 1 (400) = 900 + vm.startPrank(contributor2_); + _token.approve(address(fundingPot), 1000); // Approve enough + + // Contributor 2 attempts to contribute 700. + // Personal Cap (R2) is 300. Gets clamped to 300. + fundingPot.contributeToRoundFor( + contributor2_, round2Id, 700, accessCriteriaId, new bytes32[](0) + ); + // Verify contributor 2's contribution was clamped by personal cap. + assertEq(fundingPot.getUserContributionToRound(round2Id, contributor2_), 300, "C2 contribution should be clamped by personal cap"); + vm.stopPrank(); + + // Verify total contributions after C2 is 300 + assertEq(fundingPot.getTotalRoundContribution(round2Id), 300, "Total after C2 should be 300"); + + // ------------ PART 2: VERIFY PERSONAL CAP NON-ACCUMULATION ------------ + // Contributor 1 had 800 personal cap in R1, contributed 600, unused = 200. + // Contributor 1 has 300 personal cap in R2. + // In Total mode, personal cap does NOT roll over. Max contribution is 300. + + // Prepare unspent caps struct (even though it shouldn't work for personal in Total mode) + ILM_PC_FundingPot_v1.UnspentPersonalRoundCap[] memory unspentCaps = + new ILM_PC_FundingPot_v1.UnspentPersonalRoundCap[](1); + unspentCaps[0] = ILM_PC_FundingPot_v1.UnspentPersonalRoundCap({ + roundId: round1Id, + accessCriteriaId: accessCriteriaId, + merkleProof: new bytes32[](0) + }); + + vm.startPrank(contributor1_); + _token.approve(address(fundingPot), 500); + + // Attempt to contribute 400 ( > R2 personal cap 300) + // Total contributions = 300. Effective Round Cap = 900. Remaining Round Cap = 600. + // Personal Cap (R2) = 300. Unspent (R1) = 200, ignored in Total mode. + // Min(Remaining Round Cap, Remaining Personal Cap) = Min(600, 300) = 300. + // Should be clamped to 300. + fundingPot.contributeToRoundFor( + contributor1_, + round2Id, + 400, + accessCriteriaId, + new bytes32[](0), + unspentCaps // Provide unspent caps, although they should be ignored for personal limit + ); + // Verify contributor 1's contribution was clamped to their R2 personal cap. + assertEq(fundingPot.getUserContributionToRound(round2Id, contributor1_), 300, "C1 contribution should be clamped by personal cap"); + vm.stopPrank(); + + // Verify total round contributions: 300 (C2) + 300 (C1) = 600 + assertEq(fundingPot.getTotalRoundContribution(round2Id), 600, "Total after C1 and C2 should be 600"); + // Effective cap 900, current total 600. Remaining = 300. + + // Contributor 3 contributes 300. Personal Cap = 300. Remaining Round Cap = 300. Should succeed. + vm.startPrank(contributor3_); + _token.approve(address(fundingPot), 300); + fundingPot.contributeToRoundFor( + contributor3_, round2Id, 300, accessCriteriaId, new bytes32[](0) + ); + // Verify C3 contributed 300 + assertEq(fundingPot.getUserContributionToRound(round2Id, contributor3_), 300, "C3 contributes remaining 300"); + vm.stopPrank(); + + // Total contributions should now be 900 (300 + 300 + 300), matching the effective cap. + assertEq(fundingPot.getTotalRoundContribution(round2Id), 900, "Total should match effective cap after C3"); + + // Now the effective cap is full. Try contributing 1 again. + vm.startPrank(contributor3_); // Can use C3 or another contributor + _token.approve(address(fundingPot), 1); + + // Try contributing 1, expect revert as cap is full + vm.expectRevert( + abi.encodeWithSelector( + ILM_PC_FundingPot_v1.Module__LM_PC_FundingPot__RoundCapReached.selector + ) + ); + fundingPot.contributeToRoundFor( + contributor3_, round2Id, 1, accessCriteriaId, new bytes32[](0) + ); + vm.stopPrank(); + + // Final total check should remain 900 + assertEq(fundingPot.getTotalRoundContribution(round2Id), 900, "Final total should be effective cap"); + + } } From f36024b4527c5a144959b1c074824ba8d7ac4f0b Mon Sep 17 00:00:00 2001 From: Fabian Scherer Date: Fri, 16 May 2025 00:13:28 +0200 Subject: [PATCH 113/130] feat: globally configurable start for accumulation logic --- .../logicModule/LM_PC_FundingPot_v1.sol | 44 ++++++++++++++++++- .../interfaces/ILM_PC_FundingPot_v1.sol | 30 +++++++++++++ 2 files changed, 72 insertions(+), 2 deletions(-) diff --git a/src/modules/logicModule/LM_PC_FundingPot_v1.sol b/src/modules/logicModule/LM_PC_FundingPot_v1.sol index 022c28f57..aac84f2bc 100644 --- a/src/modules/logicModule/LM_PC_FundingPot_v1.sol +++ b/src/modules/logicModule/LM_PC_FundingPot_v1.sol @@ -151,6 +151,11 @@ contract LM_PC_FundingPot_v1 is /// @notice The next available access criteria ID for each round mapping(uint32 => uint8) private roundIdToNextAccessCriteriaId; + /// @notice The minimum round ID (inclusive, >= 1) to consider for accumulation calculations. + /// @dev Defaults to 1. If a target round's mode allows accumulation, + /// only previous rounds with roundId >= globalAccumulationStartRoundId will be included. + uint32 internal globalAccumulationStartRoundId; + /// @notice Storage gap for future upgrades. uint[50] private __gap; @@ -370,6 +375,15 @@ contract LM_PC_FundingPot_v1 is return roundIdToUserToContribution[roundId_][user_]; } + /// @inheritdoc ILM_PC_FundingPot_v1 + function getGlobalAccumulationStartRoundId() + external + view + returns (uint32) + { + return globalAccumulationStartRoundId; + } + // ------------------------------------------------------------------------- // Public - Mutating @@ -752,6 +766,26 @@ contract LM_PC_FundingPot_v1 is roundIdToNextUnprocessedIndex[roundId_] = endIndex; } + + /// @inheritdoc ILM_PC_FundingPot_v1 + function setGlobalAccumulationStart(uint32 startRoundId_) + external + onlyModuleRole(FUNDING_POT_ADMIN_ROLE) + { + if (startRoundId_ == 0) { + revert Module__LM_PC_FundingPot__StartRoundCannotBeZero(); + } + if (startRoundId_ > roundCount) { + revert Module__LM_PC_FundingPot__StartRoundGreaterThanRoundCount( + startRoundId_, roundCount + ); + } + + globalAccumulationStartRoundId = startRoundId_; + + emit GlobalAccumulationStartSet(startRoundId_); + } + // ------------------------------------------------------------------------- // Internal @@ -1080,8 +1114,14 @@ contract LM_PC_FundingPot_v1 is view returns (uint unusedCapacityFromPrevious) { - // Iterate through all previous rounds (1 to roundId_-1) - for (uint32 i = 1; i < roundId_; ++i) { + uint32 startAccumulationFrom = globalAccumulationStartRoundId; + + if (startAccumulationFrom >= roundId_) { + return 0; // No rounds to consider for accumulation + } + + // Iterate through previous rounds starting from the globalAccumulationStartRoundId + for (uint32 i = startAccumulationFrom; i < roundId_; ++i) { Round storage prevRound = rounds[i]; // Only consider previous rounds that allowed total accumulation if ( diff --git a/src/modules/logicModule/interfaces/ILM_PC_FundingPot_v1.sol b/src/modules/logicModule/interfaces/ILM_PC_FundingPot_v1.sol index ffba3e925..b3fb988ea 100644 --- a/src/modules/logicModule/interfaces/ILM_PC_FundingPot_v1.sol +++ b/src/modules/logicModule/interfaces/ILM_PC_FundingPot_v1.sol @@ -225,6 +225,10 @@ interface ILM_PC_FundingPot_v1 is IERC20PaymentClientBase_v2 { uint32 indexed roundId_, uint startIndex_, uint endIndex_ ); + /// @notice Emitted when the global accumulation start round ID is updated. + /// @param startRoundId The new round ID from which accumulation calculations will begin (inclusive, must be >= 1). + event GlobalAccumulationStartSet(uint32 startRoundId); + // ------------------------------------------------------------------------- // Errors @@ -303,6 +307,16 @@ interface ILM_PC_FundingPot_v1 is IERC20PaymentClientBase_v2 { /// @notice Invalid batch parameters. error Module__LM_PC_FundingPot__InvalidBatchParameters(); + /// @notice Start round ID must be greater than zero. + error Module__LM_PC_FundingPot__StartRoundCannotBeZero(); + + /// @notice Start round ID cannot be greater than the current round count. + /// @param startRoundId_ The provided start round ID. + /// @param currentRoundCount_ The current total number of rounds. + error Module__LM_PC_FundingPot__StartRoundGreaterThanRoundCount( + uint32 startRoundId_, uint32 currentRoundCount_ + ); + // ------------------------------------------------------------------------- // Public - Getters @@ -410,6 +424,16 @@ interface ILM_PC_FundingPot_v1 is IERC20PaymentClientBase_v2 { view returns (uint); + /// @notice Retrieves the globally configured start round ID for accumulation calculations. + /// @dev Accumulation (both personal and total) will only consider previous rounds + /// with IDs greater than or equal to this value, provided the target round's + /// AccumulationMode allows it. Defaults to 1. + /// @return The first round ID (inclusive) to consider for accumulation. + function getGlobalAccumulationStartRoundId() + external + view + returns (uint32); + // ------------------------------------------------------------------------- // Public - Mutating @@ -543,4 +567,10 @@ interface ILM_PC_FundingPot_v1 is IERC20PaymentClientBase_v2 { uint32 roundId_, uint batchSize_ ) external; + + /// @notice Sets the global minimum round ID from which accumulation calculations should begin. + /// @dev Only callable by `FUNDING_POT_ADMIN_ROLE`. This setting affects all future + /// accumulation calculations across the module. The start round must be >= 1 and cannot exceed the current round count. + /// @param startRoundId_ The first round ID (inclusive, >= 1) to consider for accumulation. + function setGlobalAccumulationStart(uint32 startRoundId_) external; } From c44e2060504b1167d9ec2e3bec497fd1583c5ae1 Mon Sep 17 00:00:00 2001 From: Fabian Scherer Date: Fri, 16 May 2025 00:48:28 +0200 Subject: [PATCH 114/130] fix: mode-specific logic & tests --- .../logicModule/LM_PC_FundingPot_v1.sol | 63 +- .../logicModule/LM_PC_FundingPot_v1.t.sol | 1755 ++++++++++++++++- 2 files changed, 1758 insertions(+), 60 deletions(-) diff --git a/src/modules/logicModule/LM_PC_FundingPot_v1.sol b/src/modules/logicModule/LM_PC_FundingPot_v1.sol index aac84f2bc..a9eaa080e 100644 --- a/src/modules/logicModule/LM_PC_FundingPot_v1.sol +++ b/src/modules/logicModule/LM_PC_FundingPot_v1.sol @@ -184,6 +184,8 @@ contract LM_PC_FundingPot_v1 is flags |= bytes32(1 << FLAG_END); __ERC20PaymentClientBase_v2_init(flags); + // Explicitly initialize the global start round ID + globalAccumulationStartRoundId = 1; } // ------------------------------------------------------------------------- @@ -652,27 +654,39 @@ contract LM_PC_FundingPot_v1 is // Process each previous round cap that the user wants to carry over for (uint i = 0; i < unspentPersonalRoundCaps_.length; i++) { - UnspentPersonalRoundCap memory roundCap = + UnspentPersonalRoundCap memory roundCapInfo = unspentPersonalRoundCaps_[i]; - Round storage prevRound = rounds[roundCap.roundId]; - if (prevRound.accumulationMode == AccumulationMode.Disabled) continue; + // Skip if this round is before the global accumulation start round + if (roundCapInfo.roundId < globalAccumulationStartRoundId) { + continue; + } + + // For PERSONAL cap rollover, the PREVIOUS round must have allowed it (Personal or All). + if ( + rounds[roundCapInfo.roundId].accumulationMode + != AccumulationMode.Personal + && rounds[roundCapInfo.roundId].accumulationMode + != AccumulationMode.All + ) { + continue; + } // Verify the user was eligible for this access criteria in the previous round bool isEligible = _checkAccessCriteriaEligibility( - uint32(roundCap.roundId), - roundCap.accessCriteriaId, - roundCap.merkleProof, + roundCapInfo.roundId, + roundCapInfo.accessCriteriaId, + roundCapInfo.merkleProof, user_ ); if (isEligible) { AccessCriteriaPrivileges storage privileges = - roundIdToAccessCriteriaIdToPrivileges[roundCap.roundId][roundCap + roundIdToAccessCriteriaIdToPrivileges[roundCapInfo.roundId][roundCapInfo .accessCriteriaId]; uint userContribution = - roundIdToUserToContribution[roundCap.roundId][user_]; + roundIdToUserToContribution[roundCapInfo.roundId][user_]; uint personalCap = privileges.personalCap; if (userContribution < personalCap) { @@ -766,7 +780,6 @@ contract LM_PC_FundingPot_v1 is roundIdToNextUnprocessedIndex[roundId_] = endIndex; } - /// @inheritdoc ILM_PC_FundingPot_v1 function setGlobalAccumulationStart(uint32 startRoundId_) external @@ -1005,14 +1018,15 @@ contract LM_PC_FundingPot_v1 is if (totalRoundContribution >= effectiveRoundCap) { // If user tries to contribute a non-zero amount when cap is full, revert. - if (amount_ > 0) { + if (amount_ > 0) { revert Module__LM_PC_FundingPot__RoundCapReached(); } - // If user tries to contribute zero when cap is full, allow adjustedAmount = 0. - adjustedAmount = 0; + // If user tries to contribute zero when cap is full, allow adjustedAmount = 0. + adjustedAmount = 0; } else { // Cap is not full, calculate remaining and clamp if necessary - uint remainingRoundCap = effectiveRoundCap - totalRoundContribution; + uint remainingRoundCap = + effectiveRoundCap - totalRoundContribution; if (adjustedAmount > remainingRoundCap) { adjustedAmount = remainingRoundCap; } @@ -1023,18 +1037,19 @@ contract LM_PC_FundingPot_v1 is // If original amount was 0, adjustedAmount is 0, so we can proceed to personal cap check which will also result in 0. // If adjustedAmount > 0, proceed to personal cap check. if (adjustedAmount == 0 && amount_ > 0) { - // This state should ideally not be reached due to the revert above if cap was full. - // But as a safeguard, if amount_ was > 0 and adjustedAmount is now 0, return 0. - return 0; + // This state should ideally not be reached due to the revert above if cap was full. + // But as a safeguard, if amount_ was > 0 and adjustedAmount is now 0, return 0. + return 0; } - // --- Personal Cap Check --- + // --- Personal Cap Check --- // Only proceed if adjustedAmount wasn't already set to 0 by round cap or initial amount. if (adjustedAmount > 0) { - uint userPreviousContribution = roundIdToUserToContribution[roundId_][user_]; + uint userPreviousContribution = + roundIdToUserToContribution[roundId_][user_]; AccessCriteriaPrivileges storage privileges = - roundIdToAccessCriteriaIdToPrivileges[roundId_][accessCriteriaId_]; + roundIdToAccessCriteriaIdToPrivileges[roundId_][accessCriteriaId_]; uint userPersonalCap = privileges.personalCap; // Add unspent personal capacity if personal accumulation is enabled for this round (Personal or All) @@ -1050,14 +1065,16 @@ contract LM_PC_FundingPot_v1 is if (userPreviousContribution + adjustedAmount > userPersonalCap) { // If user hasn't reached personal cap yet, clamp further to remaining personal cap. if (userPreviousContribution < userPersonalCap) { - uint remainingPersonalCap = userPersonalCap - userPreviousContribution; + uint remainingPersonalCap = + userPersonalCap - userPreviousContribution; // Ensure we don't accidentally increase amount, only clamp down. - if (remainingPersonalCap < adjustedAmount) { + if (remainingPersonalCap < adjustedAmount) { adjustedAmount = remainingPersonalCap; } - } else { // User is already at or over personal cap. + } else { + // User is already at or over personal cap. // If they tried to contribute a non-zero amount initially, revert. - if (amount_ > 0) { + if (amount_ > 0) { revert Module__LM_PC_FundingPot__PersonalCapReached(); } // If initial amount was 0, just ensure adjustedAmount remains 0. diff --git a/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol b/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol index 758a211c5..61d6829f3 100644 --- a/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol +++ b/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol @@ -2012,7 +2012,8 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { function testContributeToRoundFor_worksGivenPersonalCapAccumulation() public { - _defaultRoundParams.accumulationMode = ILM_PC_FundingPot_v1.AccumulationMode.Personal; + _defaultRoundParams.accumulationMode = + ILM_PC_FundingPot_v1.AccumulationMode.Personal; fundingPot.createRound( _defaultRoundParams.roundStart, _defaultRoundParams.roundEnd, @@ -2118,7 +2119,8 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { function testContributeToRoundFor_worksGivenTotalRoundCapAccumulation() public { - _defaultRoundParams.accumulationMode = ILM_PC_FundingPot_v1.AccumulationMode.All; + _defaultRoundParams.accumulationMode = + ILM_PC_FundingPot_v1.AccumulationMode.All; // Create Round 1 fundingPot.createRound( @@ -3282,9 +3284,10 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { ); } - function testContribute_PersonalMode_AccumulatesPersonalOnly() public { + function testContribute_PersonalMode_AccumulatesPersonalOnly() public { // 1. Create the first round with AccumulationMode.Personal - _defaultRoundParams.accumulationMode = ILM_PC_FundingPot_v1.AccumulationMode.Personal; + _defaultRoundParams.accumulationMode = + ILM_PC_FundingPot_v1.AccumulationMode.Personal; fundingPot.createRound( _defaultRoundParams.roundStart, @@ -3303,7 +3306,9 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { address nftContract, bytes32 merkleRoot, address[] memory allowedAddresses - ) = _helper_createAccessCriteria(uint8(ILM_PC_FundingPot_v1.AccessCriteriaType.OPEN), round1Id); + ) = _helper_createAccessCriteria( + uint8(ILM_PC_FundingPot_v1.AccessCriteriaType.OPEN), round1Id + ); fundingPot.setAccessCriteria( round1Id, @@ -3369,10 +3374,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { // Verify contribution to round 1 assertEq( - fundingPot.getUserContributionToRound( - round1Id, contributor1_ - ), - 200 + fundingPot.getUserContributionToRound(round1Id, contributor1_), 200 ); // Move to round 2 @@ -3404,11 +3406,10 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { // Verify contributions to round 2 - should be more than the personal cap of round 2 (400) // This verifies personal caps DO accumulate - uint contributionAmount = fundingPot.getUserContributionToRound( - round2Id, contributor1_ - ); + uint contributionAmount = + fundingPot.getUserContributionToRound(round2Id, contributor1_); assertEq(contributionAmount, 450); - assertTrue(contributionAmount > 400, "Personal cap should accumulate"); + assertTrue(contributionAmount > 400, "Personal cap should accumulate"); // ------------ PART 2: VERIFY TOTAL CAP NON-ACCUMULATION ------------ // Attempt to contribute more than the remaining round cap @@ -3422,7 +3423,9 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { contributor2_, round2Id, 100, accessCriteriaId, new bytes32[](0) ); // Verify contributor 2's contribution was clamped to the remaining 50. - assertEq(fundingPot.getUserContributionToRound(round2Id, contributor2_), 50); + assertEq( + fundingPot.getUserContributionToRound(round2Id, contributor2_), 50 + ); vm.stopPrank(); // Verify total contributions to round 2 is exactly the round cap (450 + 50 = 500). @@ -3436,7 +3439,9 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { // Expect revert because the round cap (500) is already met. vm.expectRevert( abi.encodeWithSelector( - ILM_PC_FundingPot_v1.Module__LM_PC_FundingPot__RoundCapReached.selector + ILM_PC_FundingPot_v1 + .Module__LM_PC_FundingPot__RoundCapReached + .selector ) ); fundingPot.contributeToRoundFor( @@ -3450,7 +3455,8 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { function testContribute_TotalMode_AccumulatesTotalOnly() public { // 1. Create the first round with AccumulationMode.Total - _defaultRoundParams.accumulationMode = ILM_PC_FundingPot_v1.AccumulationMode.Total; + _defaultRoundParams.accumulationMode = + ILM_PC_FundingPot_v1.AccumulationMode.Total; fundingPot.createRound( _defaultRoundParams.roundStart, @@ -3464,9 +3470,14 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { uint32 round1Id = fundingPot.getRoundCount(); // Set up access criteria for round 1 (Open) - uint8 accessCriteriaId = 1; - (address nftContract, bytes32 merkleRoot, address[] memory allowedAddresses) = - _helper_createAccessCriteria(uint8(ILM_PC_FundingPot_v1.AccessCriteriaType.OPEN), round1Id); + uint8 accessCriteriaId = 1; + ( + address nftContract, + bytes32 merkleRoot, + address[] memory allowedAddresses + ) = _helper_createAccessCriteria( + uint8(ILM_PC_FundingPot_v1.AccessCriteriaType.OPEN), round1Id + ); fundingPot.setAccessCriteria( round1Id, @@ -3530,7 +3541,9 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { vm.stopPrank(); // Verify contribution to round 1 - assertEq(fundingPot.getUserContributionToRound(round1Id, contributor1_), 600); + assertEq( + fundingPot.getUserContributionToRound(round1Id, contributor1_), 600 + ); assertEq(fundingPot.getTotalRoundContribution(round1Id), 600); // Move to round 2 @@ -3541,17 +3554,25 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { vm.startPrank(contributor2_); _token.approve(address(fundingPot), 1000); // Approve enough - // Contributor 2 attempts to contribute 700. + // Contributor 2 attempts to contribute 700. // Personal Cap (R2) is 300. Gets clamped to 300. fundingPot.contributeToRoundFor( contributor2_, round2Id, 700, accessCriteriaId, new bytes32[](0) ); // Verify contributor 2's contribution was clamped by personal cap. - assertEq(fundingPot.getUserContributionToRound(round2Id, contributor2_), 300, "C2 contribution should be clamped by personal cap"); + assertEq( + fundingPot.getUserContributionToRound(round2Id, contributor2_), + 300, + "C2 contribution should be clamped by personal cap" + ); vm.stopPrank(); // Verify total contributions after C2 is 300 - assertEq(fundingPot.getTotalRoundContribution(round2Id), 300, "Total after C2 should be 300"); + assertEq( + fundingPot.getTotalRoundContribution(round2Id), + 300, + "Total after C2 should be 300" + ); // ------------ PART 2: VERIFY PERSONAL CAP NON-ACCUMULATION ------------ // Contributor 1 had 800 personal cap in R1, contributed 600, unused = 200. @@ -3568,7 +3589,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { }); vm.startPrank(contributor1_); - _token.approve(address(fundingPot), 500); + _token.approve(address(fundingPot), 500); // Attempt to contribute 400 ( > R2 personal cap 300) // Total contributions = 300. Effective Round Cap = 900. Remaining Round Cap = 600. @@ -3578,17 +3599,25 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { fundingPot.contributeToRoundFor( contributor1_, round2Id, - 400, + 400, accessCriteriaId, new bytes32[](0), unspentCaps // Provide unspent caps, although they should be ignored for personal limit ); // Verify contributor 1's contribution was clamped to their R2 personal cap. - assertEq(fundingPot.getUserContributionToRound(round2Id, contributor1_), 300, "C1 contribution should be clamped by personal cap"); + assertEq( + fundingPot.getUserContributionToRound(round2Id, contributor1_), + 300, + "C1 contribution should be clamped by personal cap" + ); vm.stopPrank(); // Verify total round contributions: 300 (C2) + 300 (C1) = 600 - assertEq(fundingPot.getTotalRoundContribution(round2Id), 600, "Total after C1 and C2 should be 600"); + assertEq( + fundingPot.getTotalRoundContribution(round2Id), + 600, + "Total after C1 and C2 should be 600" + ); // Effective cap 900, current total 600. Remaining = 300. // Contributor 3 contributes 300. Personal Cap = 300. Remaining Round Cap = 300. Should succeed. @@ -3598,11 +3627,19 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { contributor3_, round2Id, 300, accessCriteriaId, new bytes32[](0) ); // Verify C3 contributed 300 - assertEq(fundingPot.getUserContributionToRound(round2Id, contributor3_), 300, "C3 contributes remaining 300"); + assertEq( + fundingPot.getUserContributionToRound(round2Id, contributor3_), + 300, + "C3 contributes remaining 300" + ); vm.stopPrank(); // Total contributions should now be 900 (300 + 300 + 300), matching the effective cap. - assertEq(fundingPot.getTotalRoundContribution(round2Id), 900, "Total should match effective cap after C3"); + assertEq( + fundingPot.getTotalRoundContribution(round2Id), + 900, + "Total should match effective cap after C3" + ); // Now the effective cap is full. Try contributing 1 again. vm.startPrank(contributor3_); // Can use C3 or another contributor @@ -3610,17 +3647,1661 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { // Try contributing 1, expect revert as cap is full vm.expectRevert( - abi.encodeWithSelector( - ILM_PC_FundingPot_v1.Module__LM_PC_FundingPot__RoundCapReached.selector - ) - ); - fundingPot.contributeToRoundFor( - contributor3_, round2Id, 1, accessCriteriaId, new bytes32[](0) - ); + abi.encodeWithSelector( + ILM_PC_FundingPot_v1 + .Module__LM_PC_FundingPot__RoundCapReached + .selector + ) + ); + fundingPot.contributeToRoundFor( + contributor3_, round2Id, 1, accessCriteriaId, new bytes32[](0) + ); vm.stopPrank(); // Final total check should remain 900 - assertEq(fundingPot.getTotalRoundContribution(round2Id), 900, "Final total should be effective cap"); + assertEq( + fundingPot.getTotalRoundContribution(round2Id), + 900, + "Final total should be effective cap" + ); + } + + // ------------------------------------------------------------------------- + // Test: Global Accumulation Start Round Settings + // ------------------------------------------------------------------------- + + /* Test getGlobalAccumulationStartRoundId() and setGlobalAccumulationStart() + ├── For getGlobalAccumulationStartRoundId() + │ └── When no explicit value has been set + │ └── Then it should return the default value of 1 + │ + └── For setGlobalAccumulationStart() + ├── Given user does not have FUNDING_POT_ADMIN_ROLE + │ └── When user attempts to set the global accumulation start round + │ └── Then it should revert + │ + ├── Given user has FUNDING_POT_ADMIN_ROLE + │ ├── And the provided start round ID is 0 + │ │ └── When user attempts to set the global accumulation start round + │ │ └── Then it should revert + │ │ + │ ├── And the provided start round ID is greater than the current round count + │ │ └── When user attempts to set the global accumulation start round + │ │ └── Then it should revert + │ │ + │ └── And a valid start round ID is provided + │ └── When user attempts to set the global accumulation start round + │ ├── Then it should update the globalAccumulationStartRoundId state variable + │ ├── Then it should emit a GlobalAccumulationStartSet event + │ └── Then getGlobalAccumulationStartRoundId() should return the new value + */ + + function testGetGlobalAccumulationStartRoundId_returnsDefaultWhenNotSet() + public + { + // Expecting default value to be 1 as per AC + assertEq( + fundingPot.getGlobalAccumulationStartRoundId(), + 1, + "Default start round ID should be 1" + ); + } + + function testSetGlobalAccumulationStart_revertsGivenUserIsNotFundingPotAdmin( + address unauthorizedUser, + uint32 startRoundId + ) public { + vm.assume(unauthorizedUser != address(this)); + vm.assume(startRoundId >= 1); + + bytes32 roleId = _authorizer.generateRoleId( + address(fundingPot), fundingPot.FUNDING_POT_ADMIN_ROLE() + ); + + vm.startPrank(unauthorizedUser); + vm.expectRevert( + abi.encodeWithSelector( + IModule_v1.Module__CallerNotAuthorized.selector, + roleId, + unauthorizedUser + ) + ); + fundingPot.setGlobalAccumulationStart(startRoundId); + vm.stopPrank(); + } + + function testSetGlobalAccumulationStart_revertsGivenStartRoundIsZero() + public + { + // AC: Must revert if startRoundId_ == 0 + vm.expectRevert( + abi.encodeWithSelector( + ILM_PC_FundingPot_v1 + .Module__LM_PC_FundingPot__StartRoundCannotBeZero + .selector + ) + ); + fundingPot.setGlobalAccumulationStart(0); + } + + function testSetGlobalAccumulationStart_revertsGivenStartRoundGreaterThanCurrentRoundCount( + uint32 startRoundIdOffset + ) public { + testCreateRound(); // Ensure roundCount is at least 1 + uint32 currentRoundCount = fundingPot.getRoundCount(); + + vm.assume(startRoundIdOffset > 0); // Ensure invalidStartRoundId will be greater + vm.assume(startRoundIdOffset <= type(uint32).max - currentRoundCount); + + uint32 invalidStartRoundId = currentRoundCount + startRoundIdOffset; + + vm.expectRevert( + abi.encodeWithSelector( + ILM_PC_FundingPot_v1 + .Module__LM_PC_FundingPot__StartRoundGreaterThanRoundCount + .selector, + invalidStartRoundId, + currentRoundCount + ) + ); + fundingPot.setGlobalAccumulationStart(invalidStartRoundId); + } + + function testSetGlobalAccumulationStart_worksGivenValidParameters( + uint32 startRoundId + ) public { + // Ensure we have at least startRoundId rounds created if startRoundId > 0 + if (startRoundId > 0) { + vm.assume(startRoundId <= 10); // Bound the fuzzing + for ( + uint32 i = fundingPot.getRoundCount() + 1; + i <= startRoundId; + i++ + ) { + fundingPot.createRound( + _defaultRoundParams.roundStart + (i * 3 days), + _defaultRoundParams.roundEnd + (i * 3 days), + _defaultRoundParams.roundCap, + _defaultRoundParams.hookContract, + _defaultRoundParams.hookFunction, + _defaultRoundParams.autoClosure, + _defaultRoundParams.accumulationMode + ); + } + vm.assume(startRoundId <= fundingPot.getRoundCount()); // Final check + } else { + vm.assume(startRoundId == 0); + // For startRoundId = 0, test should actually fail based on the revert check above + // However, fuzzing might pass 0. Let's test non-zero valid cases. + vm.assume(false); // This will cause fuzz to skip startRoundId = 0 here + } + + // Expect event emission + vm.expectEmit(true, true, true, true); + emit ILM_PC_FundingPot_v1.GlobalAccumulationStartSet(startRoundId); + + // Set the value + fundingPot.setGlobalAccumulationStart(startRoundId); + + // Verify the value using the getter + assertEq( + fundingPot.getGlobalAccumulationStartRoundId(), + startRoundId, + "Getter should return the set value" + ); + } + + // ------------------------------------------------------------------------- + // Test: Global Accumulation Start Round - Logic Integration + // ------------------------------------------------------------------------- + + /* Test Accumulation Logic with Global Start Round + │ + ├── Scenario: Global start round restricts accumulation + │ ├── Given globalAccumulationStartRoundId is set to 2 (e.g., R2) + │ │ ├── And target round (e.g., R3) uses AccumulationMode.Personal + │ │ │ └── When contributing to R3 with unspent capacity from R1 and R2 + │ │ │ └── Then only unspent personal capacity from R2 should be considered + │ │ ├── And target round (e.g., R3) uses AccumulationMode.Total + │ │ │ └── When contributing to R3 + │ │ │ └── Then only unspent total capacity from R2 should expand R3's effective cap + │ │ └── And target round (e.g., R3) uses AccumulationMode.All + │ │ ├── When contributing to R3 with unspent personal capacity from R1 and R2 + │ │ │ └── Then only unspent personal capacity from R2 should be considered + │ │ └── When calculating R3's effective total cap + │ │ └── Then only unspent total capacity from R2 should expand R3's effective cap + │ + ├── Scenario: Default global start round (1) allows accumulation from all previous applicable rounds + │ ├── Given globalAccumulationStartRoundId is 1 (default) + │ │ ├── And target round (e.g., R2 or R3) uses AccumulationMode.Personal + │ │ │ └── When contributing with unspent capacity from all previous valid rounds (e.g., R1 for R2; R1 & R2 for R3) + │ │ │ └── Then unspent personal capacity from all applicable previous rounds should be considered + │ │ ├── And target round (e.g., R2 or R3) uses AccumulationMode.Total + │ │ │ └── When calculating effective total cap + │ │ │ └── Then unspent total capacity from all applicable previous rounds should expand the effective cap + │ │ └── And target round (e.g., R2 or R3) uses AccumulationMode.All + │ │ ├── When contributing with unspent personal capacity from all previous valid rounds + │ │ │ └── Then unspent personal capacity from all applicable previous rounds should be considered + │ │ └── When calculating effective total cap + │ │ └── Then unspent total capacity from all applicable previous rounds should expand the effective cap + │ + ├── Scenario: Interaction with AccumulationMode.Disabled + │ └── Given target round's AccumulationMode is Disabled + │ └── When globalAccumulationStartRoundId is set to allow previous rounds + │ └── Then no accumulation (personal or total) should occur for the target round + │ + └── Scenario: Global start round equals target round + └── Given globalAccumulationStartRoundId is set to the target round's ID + └── When target round's AccumulationMode would normally allow accumulation + └── Then no accumulation (personal or total) from any *previous* round should occur + */ + + function testContribute_personalMode_globalStartRestrictsAccumulationFromEarlierRounds( + ) public { + // SCENARIO: globalAccumulationStartRoundId = 2 restricts accumulation from Round 1 for Personal mode + // 1. Setup: Round 1, Round 2, Round 3. Partial contributions in R1 & R2. + // 2. Action: setGlobalAccumulationStart(2) + // 3. Verification: For contributions to R3 (Personal mode), only unused personal from R2 rolls over. + + uint initialTimestamp = block.timestamp; + + // --- Setup Rounds --- + uint r1PersonalCap = 500; + uint r1Contribution = 100; + // uint r1UnusedPersonal = r1PersonalCap - r1Contribution; // Not used in this restricted scenario directly for R3 calc + + uint r2PersonalCap = 600; + uint r2Contribution = 200; + uint r2UnusedPersonal = r2PersonalCap - r2Contribution; // This IS used for R3 calculation + + uint r3BasePersonalCap = 300; + + // Round 1 + uint32 round1Id = fundingPot.createRound( + initialTimestamp + 1 days, + initialTimestamp + 2 days, + 10_000, // large round cap + address(0), + bytes(""), + false, + ILM_PC_FundingPot_v1.AccumulationMode.Personal + ); + fundingPot.setAccessCriteria( + round1Id, 1, 0, address(0), bytes32(0), new address[](0) + ); // Open access + fundingPot.setAccessCriteriaPrivileges( + round1Id, 1, r1PersonalCap, false, 0, 0, 0 + ); + + // Round 2 + uint32 round2Id = fundingPot.createRound( + initialTimestamp + 3 days, + initialTimestamp + 4 days, + 10_000, // large round cap + address(0), + bytes(""), + false, + ILM_PC_FundingPot_v1.AccumulationMode.Personal + ); + fundingPot.setAccessCriteria( + round2Id, 1, 0, address(0), bytes32(0), new address[](0) + ); + fundingPot.setAccessCriteriaPrivileges( + round2Id, 1, r2PersonalCap, false, 0, 0, 0 + ); + + // Round 3 + uint32 round3Id = fundingPot.createRound( + initialTimestamp + 5 days, + initialTimestamp + 6 days, + 10_000, // large round cap + address(0), + bytes(""), + false, + ILM_PC_FundingPot_v1.AccumulationMode.Personal + ); + fundingPot.setAccessCriteria( + round3Id, 1, 0, address(0), bytes32(0), new address[](0) + ); + fundingPot.setAccessCriteriaPrivileges( + round3Id, 1, r3BasePersonalCap, false, 0, 0, 0 + ); + + // --- Contributions --- + vm.startPrank(contributor1_); + _token.approve(address(fundingPot), type(uint).max); + + vm.warp(initialTimestamp + 1 days + 1 hours); // Enter Round 1 + fundingPot.contributeToRoundFor( + contributor1_, round1Id, r1Contribution, 1, new bytes32[](0) + ); + + vm.warp(initialTimestamp + 3 days + 1 hours); // Enter Round 2 + fundingPot.contributeToRoundFor( + contributor1_, round2Id, r2Contribution, 1, new bytes32[](0) + ); + vm.stopPrank(); + + // --- Set Global Start --- + fundingPot.setGlobalAccumulationStart(2); + assertEq(fundingPot.getGlobalAccumulationStartRoundId(), 2); + + // --- Attempt Contribution in Round 3 --- + vm.warp(initialTimestamp + 5 days + 1 hours); // Enter Round 3 + + ILM_PC_FundingPot_v1.UnspentPersonalRoundCap[] memory unspentCaps = + new ILM_PC_FundingPot_v1.UnspentPersonalRoundCap[](2); + // User claims unspent from R1 (should be ignored due to global start) + unspentCaps[0] = ILM_PC_FundingPot_v1.UnspentPersonalRoundCap( + round1Id, 1, new bytes32[](0) + ); + // User claims unspent from R2 (should be counted) + unspentCaps[1] = ILM_PC_FundingPot_v1.UnspentPersonalRoundCap( + round2Id, 1, new bytes32[](0) + ); + + uint expectedR3PersonalCap = r3BasePersonalCap + r2Contribution; // Only R2's unused personal cap + + vm.startPrank(contributor1_); + // Attempt to contribute up to the expected new personal cap + fundingPot.contributeToRoundFor( + contributor1_, + round3Id, + expectedR3PersonalCap, + 1, + new bytes32[](0), + unspentCaps + ); + vm.stopPrank(); + + // --- Assertion --- + assertEq( + fundingPot.getUserContributionToRound(round3Id, contributor1_), + expectedR3PersonalCap, + "R3 personal contribution incorrect" + ); + } + + function testContribute_totalMode_globalStartRestrictsAccumulationFromEarlierRounds( + ) public { + // SCENARIO: globalAccumulationStartRoundId = 2 restricts accumulation from Round 1 for Total mode + // 1. Setup: Round 1, Round 2, Round 3. Partial contributions in R1 & R2. + // 2. Action: setGlobalAccumulationStart(2) + // 3. Verification: For contributions to R3 (Total mode), only unused total from R2 expands R3 cap. + + uint initialTimestamp = block.timestamp; + + // --- Setup Rounds --- + uint r1BaseCap = 1000; + uint r1Contribution = 400; + // uint r1UnusedTotal = r1BaseCap - r1Contribution; + + uint r2BaseCap = 1200; + uint r2Contribution = 500; + // uint r2UnusedTotal = r2BaseCap - r2Contribution; + + uint r3BaseCap = 300; + + // Round 1 + uint32 round1Id = fundingPot.createRound( + initialTimestamp + 1 days, + initialTimestamp + 2 days, + r1BaseCap, + address(0), + bytes(""), + false, + ILM_PC_FundingPot_v1.AccumulationMode.Total + ); + fundingPot.setAccessCriteria( + round1Id, 1, 0, address(0), bytes32(0), new address[](0) + ); // Open access + fundingPot.setAccessCriteriaPrivileges( + round1Id, 1, r1BaseCap, false, 0, 0, 0 + ); // Personal cap equals round cap + + // Round 2 + uint32 round2Id = fundingPot.createRound( + initialTimestamp + 3 days, + initialTimestamp + 4 days, + r2BaseCap, + address(0), + bytes(""), + false, + ILM_PC_FundingPot_v1.AccumulationMode.Total + ); + fundingPot.setAccessCriteria( + round2Id, 1, 0, address(0), bytes32(0), new address[](0) + ); + fundingPot.setAccessCriteriaPrivileges( + round2Id, 1, r2BaseCap, false, 0, 0, 0 + ); + + // Round 3 + uint32 round3Id = fundingPot.createRound( + initialTimestamp + 5 days, + initialTimestamp + 6 days, + r3BaseCap, + address(0), + bytes(""), + false, + ILM_PC_FundingPot_v1.AccumulationMode.Total + ); + fundingPot.setAccessCriteria( + round3Id, 1, 0, address(0), bytes32(0), new address[](0) + ); + fundingPot.setAccessCriteriaPrivileges( + round3Id, 1, r3BaseCap + r2Contribution, false, 0, 0, 0 + ); // Allow full contribution for testing effective cap + + // --- Contributions --- + vm.startPrank(contributor1_); + _token.approve(address(fundingPot), type(uint).max); + + vm.warp(initialTimestamp + 1 days + 1 hours); // Enter Round 1 + fundingPot.contributeToRoundFor( + contributor1_, round1Id, r1Contribution, 1, new bytes32[](0) + ); + + vm.warp(initialTimestamp + 3 days + 1 hours); // Enter Round 2 + fundingPot.contributeToRoundFor( + contributor1_, round2Id, r2Contribution, 1, new bytes32[](0) + ); + vm.stopPrank(); + + // --- Set Global Start --- + fundingPot.setGlobalAccumulationStart(2); + assertEq(fundingPot.getGlobalAccumulationStartRoundId(), 2); + + // --- Attempt Contribution in Round 3 --- + vm.warp(initialTimestamp + 5 days + 1 hours); // Enter Round 3 + + uint expectedR3EffectiveCap = r3BaseCap + r2Contribution; // Only R2's unused total cap + + vm.startPrank(contributor1_); + // Attempt to contribute up to the expected new effective cap + fundingPot.contributeToRoundFor( + contributor1_, round3Id, expectedR3EffectiveCap, 1, new bytes32[](0) + ); + vm.stopPrank(); + + // --- Assertion --- + assertEq( + fundingPot.getTotalRoundContribution(round3Id), + expectedR3EffectiveCap, + "R3 total contribution incorrect, effective cap not as expected" + ); + assertEq( + fundingPot.getUserContributionToRound(round3Id, contributor1_), + expectedR3EffectiveCap, + "R3 user contribution incorrect" + ); + } + + function testContribute_personalMode_defaultGlobalStartAllowsAccumulationFromAllPrevious( + ) public { + // SCENARIO: Default globalAccumulationStartRoundId = 1 allows accumulation from R1 for R2 (Personal mode) + // 1. Setup: Round 1 (Personal), Round 2 (Personal). + // Partial contribution by C1 in R1. + // 2. Action: Verify getGlobalAccumulationStartRoundId() == 1 (default). + // 3. Verification: For C1's contribution to R2, unused personal capacity from R1 rolls over. + + uint initialTimestamp = block.timestamp; + uint8 accessId = 1; // Open access + + // --- Round Parameters & Contributions for C1 --- + uint r1PersonalCapC1 = 500; + uint r1ContributionC1 = 100; // C1 leaves 400 personal unused from R1 + + uint r2BasePersonalCapC1 = 300; // C1's base personal cap in R2 + + // --- Approvals --- + vm.startPrank(contributor1_); + _token.approve(address(fundingPot), type(uint).max); + vm.stopPrank(); + + // --- Create Round 1 (Personal Mode) --- + uint32 round1Id = fundingPot.createRound( + initialTimestamp + 1 days, + initialTimestamp + 2 days, + 10_000, // Large round cap, not relevant for personal accumulation focus + address(0), + bytes(""), + false, + ILM_PC_FundingPot_v1.AccumulationMode.Personal + ); + fundingPot.setAccessCriteria( + round1Id, accessId, 0, address(0), bytes32(0), new address[](0) + ); + fundingPot.setAccessCriteriaPrivileges( + round1Id, accessId, r1PersonalCapC1, false, 0, 0, 0 + ); + // --- Create Round 2 (Personal Mode) --- + uint32 round2Id = fundingPot.createRound( + initialTimestamp + 3 days, + initialTimestamp + 4 days, + 10_000, // Large round cap + address(0), + bytes(""), + false, + ILM_PC_FundingPot_v1.AccumulationMode.Personal + ); + fundingPot.setAccessCriteria( + round2Id, accessId, 0, address(0), bytes32(0), new address[](0) + ); + fundingPot.setAccessCriteriaPrivileges( + round2Id, accessId, r2BasePersonalCapC1, false, 0, 0, 0 + ); + + // --- Contribution by C1 to Round 1 --- + vm.startPrank(contributor1_); + vm.warp(initialTimestamp + 1 days + 1 hours); // Enter Round 1 + fundingPot.contributeToRoundFor( + contributor1_, + round1Id, + r1ContributionC1, + accessId, + new bytes32[](0) + ); + vm.stopPrank(); + + // --- Verify Default Global Start Round ID --- + assertEq( + fundingPot.getGlobalAccumulationStartRoundId(), + 1, + "Default global start round ID should be 1" + ); + + // --- Attempt Contribution in Round 2 by C1 --- + vm.warp(initialTimestamp + 3 days + 1 hours); // Enter Round 2 + + ILM_PC_FundingPot_v1.UnspentPersonalRoundCap[] memory unspentCapsC1 = + new ILM_PC_FundingPot_v1.UnspentPersonalRoundCap[](1); + unspentCapsC1[0] = ILM_PC_FundingPot_v1.UnspentPersonalRoundCap( + round1Id, + accessId, + new bytes32[](0) // Should be counted + ); + + uint r1UnusedPersonalC1 = r1PersonalCapC1 - r1ContributionC1; // 400 + + // Expected C1 effective personal cap in R2 = R2_Base (300) + R1_Unused (400) = 700 + uint expectedC1EffectivePersonalCapR2 = + r2BasePersonalCapC1 + r1UnusedPersonalC1; + + uint c1AttemptR2 = expectedC1EffectivePersonalCapR2 + 50; // Try to contribute slightly more + uint expectedC1ContributionR2 = expectedC1EffectivePersonalCapR2; // Should be clamped + + // Ensure the attempt is not clamped by the round cap (which is large) + if (expectedC1ContributionR2 > 10_000) { + // 10_000 is round cap for R2 + expectedC1ContributionR2 = 10_000; + } + + vm.startPrank(contributor1_); + fundingPot.contributeToRoundFor( + contributor1_, + round2Id, + c1AttemptR2, + accessId, + new bytes32[](0), + unspentCapsC1 + ); + vm.stopPrank(); + + // --- Assertion --- + assertEq( + fundingPot.getUserContributionToRound(round2Id, contributor1_), + expectedC1ContributionR2, + "R2 C1 personal contribution incorrect (should use R1 unused)" + ); + } + + function testContribute_totalMode_defaultGlobalStartAllowsAccumulationFromAllPrevious( + ) public { + // SCENARIO: Default globalAccumulationStartRoundId = 1 allows total cap accumulation from R1 to R2 (Total mode) + // Simplified to reduce stack depth. + + uint initialTimestamp = block.timestamp; + uint8 accessId = 1; // Open access + + // --- Round 1 Parameters & Contribution --- + uint r1BaseCap = 1000; + uint r1ContributionC1 = 600; // Leaves 400 unused total from R1 + uint r1PersonalCap = 1000; + + // --- Round 2 Parameters --- + uint r2BaseCap = 500; + + // --- Approvals --- + vm.startPrank(contributor1_); + _token.approve(address(fundingPot), type(uint).max); + vm.stopPrank(); + + // --- Create Round 1 (Total Mode) --- + uint32 round1Id = fundingPot.createRound( + initialTimestamp + 1 days, + initialTimestamp + 2 days, + r1BaseCap, + address(0), + bytes(""), + false, + ILM_PC_FundingPot_v1.AccumulationMode.Total + ); + fundingPot.setAccessCriteria( + round1Id, accessId, 0, address(0), bytes32(0), new address[](0) + ); + fundingPot.setAccessCriteriaPrivileges( + round1Id, accessId, r1PersonalCap, false, 0, 0, 0 + ); + + // --- Contribution by C1 to Round 1 --- + vm.warp(initialTimestamp + 1 days + 1 hours); + vm.startPrank(contributor1_); + fundingPot.contributeToRoundFor( + contributor1_, + round1Id, + r1ContributionC1, + accessId, + new bytes32[](0) + ); + vm.stopPrank(); + uint r1UnusedTotal = r1BaseCap - r1ContributionC1; // Should be 400 + + // --- Create Round 2 (Total Mode) --- + uint32 round2Id = fundingPot.createRound( + initialTimestamp + 3 days, + initialTimestamp + 4 days, + r2BaseCap, + address(0), + bytes(""), + false, + ILM_PC_FundingPot_v1.AccumulationMode.Total + ); + fundingPot.setAccessCriteria( + round2Id, accessId, 0, address(0), bytes32(0), new address[](0) + ); + // Set personal cap for R2 to be at least the expected effective total cap + uint r2ExpectedEffectiveTotalCap = r2BaseCap + r1UnusedTotal; // 500 + 400 = 900 + fundingPot.setAccessCriteriaPrivileges( + round2Id, accessId, r2ExpectedEffectiveTotalCap, false, 0, 0, 0 + ); + + // --- Verify Default Global Start Round ID --- + assertEq( + fundingPot.getGlobalAccumulationStartRoundId(), + 1, + "Default global start round ID should be 1" + ); + + // --- Attempt Contribution in Round 2 by C1 --- + vm.warp(initialTimestamp + 3 days + 1 hours); + + uint c1AttemptR2 = r2ExpectedEffectiveTotalCap - 100; // e.g., 900 - 100 = 800. Utilizes expanded cap. + assertTrue( + c1AttemptR2 > r2BaseCap, "C1 R2 attempt should be > R2 base cap" + ); + assertTrue( + c1AttemptR2 <= r2ExpectedEffectiveTotalCap, + "C1 R2 attempt should be <= R2 effective cap" + ); + + vm.startPrank(contributor1_); + fundingPot.contributeToRoundFor( + contributor1_, round2Id, c1AttemptR2, accessId, new bytes32[](0) + ); + vm.stopPrank(); + + // --- Assertions --- + assertEq( + fundingPot.getUserContributionToRound(round2Id, contributor1_), + c1AttemptR2, + "R2 C1 contribution incorrect" + ); + assertEq( + fundingPot.getTotalRoundContribution(round2Id), + c1AttemptR2, + "R2 Total contributions after C1 incorrect" + ); + + // Verify that the total contributions possible is indeed the effective cap + uint remainingToFill = r2ExpectedEffectiveTotalCap - c1AttemptR2; + if (remainingToFill > 0) { + vm.startPrank(contributor1_); + fundingPot.contributeToRoundFor( + contributor1_, + round2Id, + remainingToFill, + accessId, + new bytes32[](0) + ); + vm.stopPrank(); + } + + assertEq( + fundingPot.getTotalRoundContribution(round2Id), + r2ExpectedEffectiveTotalCap, + "R2 final total contributions should match effective total cap" + ); + } + + function testContribute_disabledMode_ignoresGlobalStartAndPreventsAccumulation( + ) public { + // SCENARIO: AccumulationMode.Disabled on a target round (R2) prevents any accumulation + // from a previous round (R1), even if globalAccumulationStartRoundId would allow it. + + uint initialTimestamp = block.timestamp; + uint8 accessId = 1; // Open access + + // --- Round 1 Parameters --- + uint r1PersonalCapC1 = 500; + uint r1ContributionC1 = 100; + uint r1BaseCap = 1000; + + // --- Round 2 Parameters (Disabled Mode) --- + uint r2BasePersonalCapC1 = 50; + uint r2BaseCap = 200; + + // --- Approvals --- + vm.startPrank(contributor1_); + _token.approve(address(fundingPot), type(uint).max); + vm.stopPrank(); + + // --- Create Round 1 (Personal Mode to generate unused personal capacity) --- + uint32 round1Id = fundingPot.createRound( + initialTimestamp + 1 days, + initialTimestamp + 2 days, + r1BaseCap, + address(0), + bytes(""), + false, + ILM_PC_FundingPot_v1.AccumulationMode.Personal + ); + fundingPot.setAccessCriteria( + round1Id, accessId, 0, address(0), bytes32(0), new address[](0) + ); + fundingPot.setAccessCriteriaPrivileges( + round1Id, accessId, r1PersonalCapC1, false, 0, 0, 0 + ); + + // --- Contribution by C1 to Round 1 --- + vm.warp(initialTimestamp + 1 days + 1 hours); + vm.startPrank(contributor1_); + fundingPot.contributeToRoundFor( + contributor1_, + round1Id, + r1ContributionC1, + accessId, + new bytes32[](0) + ); + vm.stopPrank(); + + // --- Create Round 2 (Disabled Mode) --- + uint32 round2Id = fundingPot.createRound( + initialTimestamp + 3 days, + initialTimestamp + 4 days, + r2BaseCap, + address(0), + bytes(""), + false, + ILM_PC_FundingPot_v1.AccumulationMode.Disabled + ); + fundingPot.setAccessCriteria( + round2Id, accessId, 0, address(0), bytes32(0), new address[](0) + ); + fundingPot.setAccessCriteriaPrivileges( + round2Id, accessId, r2BasePersonalCapC1, false, 0, 0, 0 + ); + + // --- Set Global Start Round ID to allow R1 (to show it's ignored by R2's Disabled mode) --- + fundingPot.setGlobalAccumulationStart(1); + assertEq( + fundingPot.getGlobalAccumulationStartRoundId(), + 1, + "Global start round ID should be 1" + ); + + // --- Attempt Contribution in Round 2 by C1 --- + vm.warp(initialTimestamp + 3 days + 1 hours); + + uint c1AttemptR2 = r2BasePersonalCapC1 + 100; + + ILM_PC_FundingPot_v1.UnspentPersonalRoundCap[] memory unspentCapsC1 = + new ILM_PC_FundingPot_v1.UnspentPersonalRoundCap[](1); + unspentCapsC1[0] = ILM_PC_FundingPot_v1.UnspentPersonalRoundCap( + round1Id, accessId, new bytes32[](0) + ); + + vm.startPrank(contributor1_); + fundingPot.contributeToRoundFor( + contributor1_, + round2Id, + c1AttemptR2, + accessId, + new bytes32[](0), + unspentCapsC1 + ); + vm.stopPrank(); + + // --- Assertions --- + assertEq( + fundingPot.getUserContributionToRound(round2Id, contributor1_), + r2BasePersonalCapC1, + "R2 C1 personal contribution should be clamped by R2's base personal cap (Disabled mode)" + ); + assertEq( + fundingPot.getTotalRoundContribution(round2Id), + r2BasePersonalCapC1, + "R2 Total contributions should not be expanded by R1 (Disabled mode)" + ); + assertTrue( + fundingPot.getTotalRoundContribution(round2Id) <= r2BaseCap, + "R2 Total contributions exceeded R2's original base cap (Disabled mode)" + ); + } + + function testContribute_anyAccumulativeMode_noAccumulationWhenGlobalStartEqualsTargetRound( + ) public { + // SCENARIO: If globalAccumulationStartRoundId is set to the target round's ID (R2), + // no accumulation from any previous round (R1) occurs for R2, even if R2's mode would allow it. + + uint initialTimestamp = block.timestamp; + uint8 accessId = 1; // Open access + + // --- Round 1 Parameters --- + uint r1PersonalCapC1 = 500; + uint r1ContributionC1 = 100; + uint r1BaseCap = 1000; + + // --- Round 2 Parameters (Mode that would normally allow accumulation, e.g., Personal) --- + uint r2BasePersonalCapC1 = 50; + uint r2BaseCap = 200; + + // --- Approvals --- + vm.startPrank(contributor1_); + _token.approve(address(fundingPot), type(uint).max); + vm.stopPrank(); + + // --- Create Round 1 (Personal Mode to generate unused personal capacity) --- + uint32 round1Id = fundingPot.createRound( + initialTimestamp + 1 days, + initialTimestamp + 2 days, + r1BaseCap, + address(0), + bytes(""), + false, + ILM_PC_FundingPot_v1.AccumulationMode.Personal + ); + fundingPot.setAccessCriteria( + round1Id, accessId, 0, address(0), bytes32(0), new address[](0) + ); + fundingPot.setAccessCriteriaPrivileges( + round1Id, accessId, r1PersonalCapC1, false, 0, 0, 0 + ); + + // --- Contribution by C1 to Round 1 --- + vm.warp(initialTimestamp + 1 days + 1 hours); + vm.startPrank(contributor1_); + fundingPot.contributeToRoundFor( + contributor1_, + round1Id, + r1ContributionC1, + accessId, + new bytes32[](0) + ); + vm.stopPrank(); + + // --- Create Round 2 (Personal Mode - would normally allow accumulation from R1) --- + uint32 round2Id = fundingPot.createRound( + initialTimestamp + 3 days, + initialTimestamp + 4 days, + r2BaseCap, + address(0), + bytes(""), + false, + ILM_PC_FundingPot_v1.AccumulationMode.Personal + ); + fundingPot.setAccessCriteria( + round2Id, accessId, 0, address(0), bytes32(0), new address[](0) + ); + fundingPot.setAccessCriteriaPrivileges( + round2Id, accessId, r2BasePersonalCapC1, false, 0, 0, 0 + ); + + // --- Set Global Start Round ID to be Round 2's ID --- + fundingPot.setGlobalAccumulationStart(round2Id); + assertEq( + fundingPot.getGlobalAccumulationStartRoundId(), + round2Id, + "Global start round ID not set to R2 ID" + ); + + // --- Attempt Contribution in Round 2 by C1 --- + vm.warp(initialTimestamp + 3 days + 1 hours); + + uint c1AttemptR2 = r2BasePersonalCapC1 + 100; + + ILM_PC_FundingPot_v1.UnspentPersonalRoundCap[] memory unspentCapsC1 = + new ILM_PC_FundingPot_v1.UnspentPersonalRoundCap[](1); + unspentCapsC1[0] = ILM_PC_FundingPot_v1.UnspentPersonalRoundCap( + round1Id, accessId, new bytes32[](0) + ); + + vm.startPrank(contributor1_); + fundingPot.contributeToRoundFor( + contributor1_, + round2Id, + c1AttemptR2, + accessId, + new bytes32[](0), + unspentCapsC1 + ); + vm.stopPrank(); + + // --- Assertions --- + assertEq( + fundingPot.getUserContributionToRound(round2Id, contributor1_), + r2BasePersonalCapC1, + "R2 C1 personal contribution should be clamped by R2's base personal cap (global start = R2)" + ); + assertEq( + fundingPot.getTotalRoundContribution(round2Id), + r2BasePersonalCapC1, + "R2 Total contributions should not be expanded by R1 (global start = R2)" + ); + assertTrue( + fundingPot.getTotalRoundContribution(round2Id) <= r2BaseCap, + "R2 Total contributions exceeded R2's original base cap (global start = R2)" + ); + } + + // ------------------------------------------------------------------------- + // Test: Global Accumulation Start Round - Logic Integration - Remaining Tests + // (Covers multi-round accumulation where global start allows all previous) + // ------------------------------------------------------------------------- + + function testContribute_personalMode_defaultGlobalStartAllowsAccumulationFromMultiplePreviousRounds( + ) public { + // SCENARIO: globalAccumulationStartRoundId = 1 allows personal cap accumulation from R1 AND R2 + // for contributions to R3, when all rounds are in Personal mode. + // 1. Setup: R1, R2, R3 in Personal mode. C1 makes partial contributions in R1 & R2. + // 2. Action: Verify globalAccumulationStartRoundId = 1. C1 contributes to R3. + // 3. Verification: C1's effective personal cap in R3 includes unused from R1 and R2. + + uint initialTimestamp = block.timestamp; + + // --- Round Parameters, Personal Caps, and Contributions for contributor1_ --- + uint r1PersonalCapC1 = 500; + uint r1ContributionC1 = 200; + + uint r2PersonalCapC1 = 600; + uint r2ContributionC1 = 250; + + uint r3BasePersonalCapC1 = 300; + + uint largeRoundCap = 1_000_000; + + // --- Approvals --- + vm.startPrank(contributor1_); + _token.approve(address(fundingPot), type(uint).max); + vm.stopPrank(); + + // --- Create Round 1 (Personal Mode) --- + uint32 round1Id = fundingPot.createRound( + initialTimestamp + 1 days, + initialTimestamp + 2 days, + largeRoundCap, + address(0), + bytes(""), + false, + ILM_PC_FundingPot_v1.AccumulationMode.Personal + ); + fundingPot.setAccessCriteria( + round1Id, 1, 0, address(0), bytes32(0), new address[](0) + ); + fundingPot.setAccessCriteriaPrivileges( + round1Id, 1, r1PersonalCapC1, false, 0, 0, 0 + ); + + // --- Contribution by C1 to Round 1 --- + vm.warp(initialTimestamp + 1 days + 1 hours); + vm.startPrank(contributor1_); + fundingPot.contributeToRoundFor( + contributor1_, round1Id, r1ContributionC1, 1, new bytes32[](0) + ); + vm.stopPrank(); + assertEq( + fundingPot.getUserContributionToRound(round1Id, contributor1_), + r1ContributionC1 + ); + + // --- Create Round 2 (Personal Mode) --- + uint32 round2Id = fundingPot.createRound( + initialTimestamp + 3 days, + initialTimestamp + 4 days, + largeRoundCap, + address(0), + bytes(""), + false, + ILM_PC_FundingPot_v1.AccumulationMode.Personal + ); + fundingPot.setAccessCriteria( + round2Id, 1, 0, address(0), bytes32(0), new address[](0) + ); + fundingPot.setAccessCriteriaPrivileges( + round2Id, 1, r2PersonalCapC1, false, 0, 0, 0 + ); + + // --- Contribution by C1 to Round 2 --- + vm.warp(initialTimestamp + 3 days + 1 hours); + vm.startPrank(contributor1_); + fundingPot.contributeToRoundFor( + contributor1_, round2Id, r2ContributionC1, 1, new bytes32[](0) + ); + vm.stopPrank(); + assertEq( + fundingPot.getUserContributionToRound(round2Id, contributor1_), + r2ContributionC1 + ); + + // --- Create Round 3 (Personal Mode) --- + uint32 round3Id = fundingPot.createRound( + initialTimestamp + 5 days, + initialTimestamp + 6 days, + largeRoundCap, + address(0), + bytes(""), + false, + ILM_PC_FundingPot_v1.AccumulationMode.Personal + ); + fundingPot.setAccessCriteria( + round3Id, 1, 0, address(0), bytes32(0), new address[](0) + ); + fundingPot.setAccessCriteriaPrivileges( + round3Id, 1, r3BasePersonalCapC1, false, 0, 0, 0 + ); + + // --- Verify Global Start Round ID --- + assertEq( + fundingPot.getGlobalAccumulationStartRoundId(), + 1, + "Default global start round ID should be 1" + ); + + // --- Attempt Contribution in Round 3 by C1 --- + vm.warp(initialTimestamp + 5 days + 1 hours); + + ILM_PC_FundingPot_v1.UnspentPersonalRoundCap[] memory unspentCapsC1 = + new ILM_PC_FundingPot_v1.UnspentPersonalRoundCap[](2); + unspentCapsC1[0] = ILM_PC_FundingPot_v1.UnspentPersonalRoundCap( + round1Id, 1, new bytes32[](0) + ); + unspentCapsC1[1] = ILM_PC_FundingPot_v1.UnspentPersonalRoundCap( + round2Id, 1, new bytes32[](0) + ); + + uint expectedR3PersonalCapC1 = r3BasePersonalCapC1 + + (r1PersonalCapC1 - r1ContributionC1) + + (r2PersonalCapC1 - r2ContributionC1); + + uint c1AttemptR3 = expectedR3PersonalCapC1; + + vm.startPrank(contributor1_); + fundingPot.contributeToRoundFor( + contributor1_, + round3Id, + c1AttemptR3, + 1, + new bytes32[](0), + unspentCapsC1 + ); + vm.stopPrank(); + + // --- Assertions --- + assertEq( + fundingPot.getUserContributionToRound(round3Id, contributor1_), + expectedR3PersonalCapC1, + "R3 C1 personal contribution incorrect (should use R1 & R2 unused)" + ); + assertEq( + fundingPot.getTotalRoundContribution(round3Id), + expectedR3PersonalCapC1, + "R3 total contributions incorrect after C1" + ); + + vm.startPrank(contributor1_); + _token.approve(address(fundingPot), 1); + vm.expectRevert( + abi.encodeWithSelector( + ILM_PC_FundingPot_v1 + .Module__LM_PC_FundingPot__PersonalCapReached + .selector + ) + ); + fundingPot.contributeToRoundFor( + contributor1_, round3Id, 1, 1, new bytes32[](0), unspentCapsC1 + ); + vm.stopPrank(); + } + + function testContribute_totalMode_defaultGlobalStartAllowsAccumulationFromMultiplePreviousRounds( + ) public { + // SCENARIO: globalAccumulationStartRoundId = 1 allows accumulation from Round 1 AND Round 2 for Total mode + // 1. Setup: Round 1, Round 2, Round 3. Partial total contributions in R1 & R2. All in Total mode. + // 2. Action: setGlobalAccumulationStart(1) (or verify default). + // 3. Verification: For contributions to R3 (Total mode), unused total from R1 AND R2 rolls over, expanding R3's effective cap. + + uint initialTimestamp = block.timestamp; + + // --- Round Parameters & Contributions --- + uint r1BaseCap = 1000; + uint r1ContributionC1 = 400; + + uint r2BaseCap = 1200; + uint r2ContributionC2 = 700; + + uint r3BaseCap = 300; + + // --- Approvals --- + vm.startPrank(contributor1_); + _token.approve(address(fundingPot), type(uint).max); + vm.stopPrank(); + + vm.startPrank(contributor2_); + _token.approve(address(fundingPot), type(uint).max); + vm.stopPrank(); + + vm.startPrank(contributor3_); + _token.approve(address(fundingPot), type(uint).max); + vm.stopPrank(); + + // --- Create Round 1 (Total Mode) --- + uint32 round1Id = fundingPot.createRound( + initialTimestamp + 1 days, + initialTimestamp + 2 days, + r1BaseCap, + address(0), + bytes(""), + false, + ILM_PC_FundingPot_v1.AccumulationMode.Total + ); + fundingPot.setAccessCriteria( + round1Id, 1, 0, address(0), bytes32(0), new address[](0) + ); + fundingPot.setAccessCriteriaPrivileges( + round1Id, 1, r1BaseCap, false, 0, 0, 0 + ); + + // --- Contribution by C1 to Round 1 --- + vm.warp(initialTimestamp + 1 days + 1 hours); + vm.startPrank(contributor1_); + fundingPot.contributeToRoundFor( + contributor1_, round1Id, r1ContributionC1, 1, new bytes32[](0) + ); + vm.stopPrank(); + assertEq( + fundingPot.getTotalRoundContribution(round1Id), r1ContributionC1 + ); + + // --- Create Round 2 (Total Mode) --- + uint32 round2Id = fundingPot.createRound( + initialTimestamp + 3 days, + initialTimestamp + 4 days, + r2BaseCap, + address(0), + bytes(""), + false, + ILM_PC_FundingPot_v1.AccumulationMode.Total + ); + fundingPot.setAccessCriteria( + round2Id, 1, 0, address(0), bytes32(0), new address[](0) + ); + fundingPot.setAccessCriteriaPrivileges( + round2Id, 1, r2BaseCap, false, 0, 0, 0 + ); + + // --- Contribution by C2 to Round 2 --- + vm.warp(initialTimestamp + 3 days + 1 hours); + vm.startPrank(contributor2_); + fundingPot.contributeToRoundFor( + contributor2_, round2Id, r2ContributionC2, 1, new bytes32[](0) + ); + vm.stopPrank(); + assertEq( + fundingPot.getTotalRoundContribution(round2Id), r2ContributionC2 + ); + + // --- Create Round 3 (Total Mode) --- + uint r3ExpectedEffectiveCap = r3BaseCap + (r1BaseCap - r1ContributionC1) + + (r2BaseCap - r2ContributionC2); + uint32 round3Id = fundingPot.createRound( + initialTimestamp + 5 days, + initialTimestamp + 6 days, + r3BaseCap, + address(0), + bytes(""), + false, + ILM_PC_FundingPot_v1.AccumulationMode.Total + ); + (address nftR3, bytes32 merkleR3, address[] memory allowedR3) = + _helper_createAccessCriteria(1, round3Id); + + // TODO + fundingPot.setAccessCriteria(round3Id, 1, 0, nftR3, merkleR3, allowedR3); + fundingPot.setAccessCriteriaPrivileges( + round3Id, 1, r3ExpectedEffectiveCap, false, 0, 0, 0 + ); + + assertEq( + fundingPot.getGlobalAccumulationStartRoundId(), + 1, + "Default global start round ID should be 1" + ); + + // --- Attempt Contribution in Round 3 by C3 --- + vm.warp(initialTimestamp + 5 days + 1 hours); + + vm.startPrank(contributor3_); + fundingPot.contributeToRoundFor( + contributor3_, round3Id, r3ExpectedEffectiveCap, 1, new bytes32[](0) + ); + vm.stopPrank(); + + // --- Assertions --- + assertEq( + fundingPot.getTotalRoundContribution(round3Id), + r3ExpectedEffectiveCap, + "R3 total contributions should match effective cap with rollover from R1 and R2" + ); + assertEq( + fundingPot.getUserContributionToRound(round3Id, contributor3_), + r3ExpectedEffectiveCap, + "R3 C3 contribution incorrect" + ); + + vm.startPrank(contributor1_); + _token.approve(address(fundingPot), 1); + vm.expectRevert( + abi.encodeWithSelector( + ILM_PC_FundingPot_v1 + .Module__LM_PC_FundingPot__RoundCapReached + .selector + ) + ); + fundingPot.contributeToRoundFor( + contributor1_, round3Id, 1, 1, new bytes32[](0) + ); + vm.stopPrank(); + } + + function testContribute_allMode_defaultGlobalStartAllowsPersonalCapAccumulationFromAllPrevious( + ) public { + // SCENARIO: globalAccumulationStartRoundId = 1 allows personal cap + // accumulation from R1 to R2, when both are in All mode. (Simplified for stack) + + uint initialTimestamp = block.timestamp; + uint8 accessId = 1; + + // --- Round 1: Setup & C1 Contribution --- + uint r1PersonalCapC1 = 500; + uint r1ContributionC1 = 100; + uint32 round1Id = fundingPot.createRound( + initialTimestamp + 1 days, + initialTimestamp + 2 days, + 1000, + address(0), + bytes(""), + false, + ILM_PC_FundingPot_v1.AccumulationMode.All + ); + fundingPot.setAccessCriteria( + round1Id, accessId, 0, address(0), bytes32(0), new address[](0) + ); + fundingPot.setAccessCriteriaPrivileges( + round1Id, accessId, r1PersonalCapC1, false, 0, 0, 0 + ); + + vm.warp(initialTimestamp + 1 days + 1 hours); + vm.startPrank(contributor1_); + _token.approve(address(fundingPot), type(uint).max); + fundingPot.contributeToRoundFor( + contributor1_, + round1Id, + r1ContributionC1, + accessId, + new bytes32[](0) + ); + vm.stopPrank(); + uint r1UnusedPersonalForC1 = r1PersonalCapC1 - r1ContributionC1; + + // --- Round 2: Setup --- + uint r2BasePersonalCapC1 = 200; + uint32 round2Id = fundingPot.createRound( + initialTimestamp + 3 days, + initialTimestamp + 4 days, + 2000, + address(0), + bytes(""), + false, + ILM_PC_FundingPot_v1.AccumulationMode.All + ); + fundingPot.setAccessCriteria( + round2Id, accessId, 0, address(0), bytes32(0), new address[](0) + ); + fundingPot.setAccessCriteriaPrivileges( + round2Id, accessId, r2BasePersonalCapC1, false, 0, 0, 0 + ); + + // --- Global Start ID Check --- + assertEq( + fundingPot.getGlobalAccumulationStartRoundId(), + 1, + "Default global start ID is 1" + ); + + // --- C1 Contribution to Round 2 (Testing Personal Cap Rollover) --- + vm.warp(initialTimestamp + 3 days + 1 hours); + uint expectedEffectivePersonalCapC1R2 = + r2BasePersonalCapC1 + r1UnusedPersonalForC1; + uint c1AttemptR2 = expectedEffectivePersonalCapC1R2 + 50; + + ILM_PC_FundingPot_v1.UnspentPersonalRoundCap[] memory unspentCapsC1 = + new ILM_PC_FundingPot_v1.UnspentPersonalRoundCap[](1); + unspentCapsC1[0] = ILM_PC_FundingPot_v1.UnspentPersonalRoundCap( + round1Id, accessId, new bytes32[](0) + ); + + vm.startPrank(contributor1_); + fundingPot.contributeToRoundFor( + contributor1_, + round2Id, + c1AttemptR2, + accessId, + new bytes32[](0), + unspentCapsC1 + ); + vm.stopPrank(); + + // --- Assertions --- + assertEq( + fundingPot.getUserContributionToRound(round2Id, contributor1_), + expectedEffectivePersonalCapC1R2, + "R2 C1 personal contribution incorrect" + ); + assertEq( + fundingPot.getTotalRoundContribution(round2Id), + expectedEffectivePersonalCapC1R2, + "R2 Total contributions incorrect" + ); + } + + function testContribute_allMode_defaultGlobalStartAllowsTotalCapAccumulationFromAllPrevious( + ) public { + // SCENARIO: globalAccumulationStartRoundId = 1 (default or set) allows total cap + // accumulation from R1 to R2, when both are in All mode. + + uint initialTimestamp = block.timestamp; + uint8 accessId = 1; // Open access + + // --- Round 1 Parameters (All Mode) --- + uint r1BaseTotalCap = 1000; + uint r1C1PersonalCap = 800; + uint r1C1Contribution = 600; + + // --- Round 2 Parameters (All Mode) --- + uint r2BaseTotalCap = 500; + + // --- Approvals --- + vm.startPrank(contributor1_); + _token.approve(address(fundingPot), type(uint).max); + vm.stopPrank(); + + // --- Create Round 1 (All Mode) --- + uint32 round1Id = fundingPot.createRound( + initialTimestamp + 1 days, + initialTimestamp + 2 days, + r1BaseTotalCap, + address(0), + bytes(""), + false, + ILM_PC_FundingPot_v1.AccumulationMode.All + ); + fundingPot.setAccessCriteria( + round1Id, accessId, 0, address(0), bytes32(0), new address[](0) + ); + fundingPot.setAccessCriteriaPrivileges( + round1Id, accessId, r1C1PersonalCap, false, 0, 0, 0 + ); + + // --- Contribution by C1 to Round 1 --- + vm.warp(initialTimestamp + 1 days + 1 hours); + vm.startPrank(contributor1_); + fundingPot.contributeToRoundFor( + contributor1_, + round1Id, + r1C1Contribution, + accessId, + new bytes32[](0) + ); + vm.stopPrank(); + uint r1UnusedTotal = r1BaseTotalCap - r1C1Contribution; + + // --- Create Round 2 (All Mode) --- + uint32 round2Id = fundingPot.createRound( + initialTimestamp + 3 days, + initialTimestamp + 4 days, + r2BaseTotalCap, + address(0), + bytes(""), + false, + ILM_PC_FundingPot_v1.AccumulationMode.All + ); + fundingPot.setAccessCriteria( + round2Id, accessId, 0, address(0), bytes32(0), new address[](0) + ); + uint r2ExpectedEffectiveTotalCap = r2BaseTotalCap + r1UnusedTotal; + fundingPot.setAccessCriteriaPrivileges( + round2Id, accessId, r2ExpectedEffectiveTotalCap, false, 0, 0, 0 + ); + + // --- Ensure Global Start Round ID is 1 --- + assertEq( + fundingPot.getGlobalAccumulationStartRoundId(), + 1, + "Global start round ID should be 1 by default" + ); + + // --- Attempt Contribution in Round 2 by C1 to fill effective total cap --- + vm.warp(initialTimestamp + 3 days + 1 hours); + + uint c1AttemptR2 = r2ExpectedEffectiveTotalCap; + + vm.startPrank(contributor1_); + fundingPot.contributeToRoundFor( + contributor1_, round2Id, c1AttemptR2, accessId, new bytes32[](0) + ); + vm.stopPrank(); + + // --- Assertions --- + assertEq( + fundingPot.getUserContributionToRound(round2Id, contributor1_), + c1AttemptR2, + "R2 C1 contribution should match attempt (filling effective total cap)" + ); + assertEq( + fundingPot.getTotalRoundContribution(round2Id), + r2ExpectedEffectiveTotalCap, + "R2 Total contributions should match effective total cap (All mode, global_start=1)" + ); + } + + function testContribute_allMode_globalStartRestrictsPersonalCapAccumulationFromEarlierRounds( + ) public { + // SCENARIO: globalAccumulationStartRoundId = 2 restricts personal cap accumulation + // from R1 for R2, when both are in All mode. + + uint initialTimestamp = block.timestamp; + uint8 accessId = 1; // Open access + + // --- Round 1 Parameters (All Mode) --- + uint r1PersonalCapC1 = 500; + uint r1ContributionC1 = 100; + uint r1BaseTotalCap = 1000; + + // --- Round 2 Parameters (All Mode) --- + uint r2BasePersonalCapC1 = 50; + uint r2BaseTotalCap = 1000; + + // --- Approvals --- + vm.startPrank(contributor1_); + _token.approve(address(fundingPot), type(uint).max); + vm.stopPrank(); + + // --- Create Round 1 (All Mode) --- + uint32 round1Id = fundingPot.createRound( + initialTimestamp + 1 days, + initialTimestamp + 2 days, + r1BaseTotalCap, + address(0), + bytes(""), + false, + ILM_PC_FundingPot_v1.AccumulationMode.All + ); + fundingPot.setAccessCriteria( + round1Id, accessId, 0, address(0), bytes32(0), new address[](0) + ); + fundingPot.setAccessCriteriaPrivileges( + round1Id, accessId, r1PersonalCapC1, false, 0, 0, 0 + ); + + // --- Contribution by C1 to Round 1 --- + vm.warp(initialTimestamp + 1 days + 1 hours); + vm.startPrank(contributor1_); + fundingPot.contributeToRoundFor( + contributor1_, + round1Id, + r1ContributionC1, + accessId, + new bytes32[](0) + ); + vm.stopPrank(); + + // --- Create Round 2 (All Mode) --- + uint32 round2Id = fundingPot.createRound( + initialTimestamp + 3 days, + initialTimestamp + 4 days, + r2BaseTotalCap, + address(0), + bytes(""), + false, + ILM_PC_FundingPot_v1.AccumulationMode.All + ); + fundingPot.setAccessCriteria( + round2Id, accessId, 0, address(0), bytes32(0), new address[](0) + ); + fundingPot.setAccessCriteriaPrivileges( + round2Id, accessId, r2BasePersonalCapC1, false, 0, 0, 0 + ); + + // --- Set Global Start Round ID to Round 2's ID --- + fundingPot.setGlobalAccumulationStart(round2Id); + assertEq( + fundingPot.getGlobalAccumulationStartRoundId(), + round2Id, + "Global start ID not set to R2 ID" + ); + + // --- Attempt Contribution in Round 2 by C1 --- + vm.warp(initialTimestamp + 3 days + 1 hours); + + uint c1AttemptR2 = r2BasePersonalCapC1 + 100; + + ILM_PC_FundingPot_v1.UnspentPersonalRoundCap[] memory unspentCapsC1 = + new ILM_PC_FundingPot_v1.UnspentPersonalRoundCap[](1); + unspentCapsC1[0] = ILM_PC_FundingPot_v1.UnspentPersonalRoundCap( + round1Id, accessId, new bytes32[](0) + ); + + vm.startPrank(contributor1_); + fundingPot.contributeToRoundFor( + contributor1_, + round2Id, + c1AttemptR2, + accessId, + new bytes32[](0), + unspentCapsC1 + ); + vm.stopPrank(); + + // --- Assertions --- + assertEq( + fundingPot.getUserContributionToRound(round2Id, contributor1_), + r2BasePersonalCapC1, + "R2 C1 personal contribution should be clamped by R2 base personal cap (All mode, global_start=R2)" + ); + assertEq( + fundingPot.getTotalRoundContribution(round2Id), + r2BasePersonalCapC1, + "R2 Total contributions should be C1's clamped amount (All mode, global_start=R2)" + ); + } + + function testContribute_allMode_globalStartRestrictsTotalCapAccumulationFromEarlierRounds( + ) public { + // SCENARIO: globalAccumulationStartRoundId = 2 restricts total cap accumulation + // from R1 for R2, when both are in All mode. + + uint initialTimestamp = block.timestamp; + uint8 accessId = 1; // Open access + + // --- Round 1 Parameters (All Mode) --- + uint r1BaseTotalCap = 1000; + uint r1C1PersonalCap = 800; + uint r1C1Contribution = 400; + + // --- Round 2 Parameters (All Mode) --- + uint r2BaseTotalCap = 200; + + // --- Approvals --- + vm.startPrank(contributor1_); + _token.approve(address(fundingPot), type(uint).max); + vm.stopPrank(); + + // --- Create Round 1 (All Mode) --- + uint32 round1Id = fundingPot.createRound( + initialTimestamp + 1 days, + initialTimestamp + 2 days, + r1BaseTotalCap, + address(0), + bytes(""), + false, + ILM_PC_FundingPot_v1.AccumulationMode.All + ); + fundingPot.setAccessCriteria( + round1Id, accessId, 0, address(0), bytes32(0), new address[](0) + ); + fundingPot.setAccessCriteriaPrivileges( + round1Id, accessId, r1C1PersonalCap, false, 0, 0, 0 + ); + + // --- Contribution by C1 to Round 1 --- + vm.warp(initialTimestamp + 1 days + 1 hours); + vm.startPrank(contributor1_); + fundingPot.contributeToRoundFor( + contributor1_, + round1Id, + r1C1Contribution, + accessId, + new bytes32[](0) + ); + vm.stopPrank(); + + // --- Create Round 2 (All Mode) --- + uint32 round2Id = fundingPot.createRound( + initialTimestamp + 3 days, + initialTimestamp + 4 days, + r2BaseTotalCap, + address(0), + bytes(""), + false, + ILM_PC_FundingPot_v1.AccumulationMode.All + ); + fundingPot.setAccessCriteria( + round2Id, accessId, 0, address(0), bytes32(0), new address[](0) + ); + fundingPot.setAccessCriteriaPrivileges( + round2Id, accessId, r2BaseTotalCap, false, 0, 0, 0 + ); + + // --- Set Global Start Round ID to Round 2's ID --- + fundingPot.setGlobalAccumulationStart(round2Id); + assertEq( + fundingPot.getGlobalAccumulationStartRoundId(), + round2Id, + "Global start ID not set to R2 ID" + ); + + // --- Attempt Contribution in Round 2 by C1 --- + vm.warp(initialTimestamp + 3 days + 1 hours); + + uint c1AttemptR2 = r2BaseTotalCap + 100; + + vm.startPrank(contributor1_); + fundingPot.contributeToRoundFor( + contributor1_, round2Id, c1AttemptR2, accessId, new bytes32[](0) + ); + vm.stopPrank(); + + // --- Assertions --- + assertEq( + fundingPot.getUserContributionToRound(round2Id, contributor1_), + r2BaseTotalCap, + "R2 C1 contribution should be clamped by R2 base total cap (All mode, global_start=R2)" + ); + assertEq( + fundingPot.getTotalRoundContribution(round2Id), + r2BaseTotalCap, + "R2 Total contributions should be R2 base total cap (All mode, global_start=R2)" + ); } } From f033a5394af31f2c12d8abbb2afa5d8e30d26fa9 Mon Sep 17 00:00:00 2001 From: Fabian Scherer Date: Fri, 16 May 2025 14:42:03 +0200 Subject: [PATCH 115/130] chore: contract size excess 879 -> 239 --- .../logicModule/LM_PC_FundingPot_v1.sol | 189 ++++++++++-------- 1 file changed, 101 insertions(+), 88 deletions(-) diff --git a/src/modules/logicModule/LM_PC_FundingPot_v1.sol b/src/modules/logicModule/LM_PC_FundingPot_v1.sol index a9eaa080e..9cb63e0a6 100644 --- a/src/modules/logicModule/LM_PC_FundingPot_v1.sol +++ b/src/modules/logicModule/LM_PC_FundingPot_v1.sol @@ -16,6 +16,7 @@ import { } from "@lm/abstracts/ERC20PaymentClientBase_v2.sol"; import {IBondingCurveBase_v1} from "@fm/bondingCurve/interfaces/IBondingCurveBase_v1.sol"; +import {IFundingManager_v1} from "@fm/IFundingManager_v1.sol"; // External import {IERC20} from "@oz/token/ERC20/IERC20.sol"; @@ -228,33 +229,24 @@ contract LM_PC_FundingPot_v1 is bool isList_ ) { - Round storage round = rounds[roundId_]; AccessCriteria storage accessCriteria = - round.accessCriterias[accessCriteriaId_]; + rounds[roundId_].accessCriterias[accessCriteriaId_]; + ILM_PC_FundingPot_v1.AccessCriteriaType acType = + accessCriteria.accessCriteriaType; + + bool isRoundOpen = + (acType == ILM_PC_FundingPot_v1.AccessCriteriaType.OPEN); + bool isList = ( + acType == ILM_PC_FundingPot_v1.AccessCriteriaType.OPEN + || acType == ILM_PC_FundingPot_v1.AccessCriteriaType.LIST + ); - if (accessCriteria.accessCriteriaType == AccessCriteriaType.OPEN) { - return ( - true, - accessCriteria.nftContract, - accessCriteria.merkleRoot, - true - ); - } else if (accessCriteria.accessCriteriaType == AccessCriteriaType.LIST) - { - return ( - false, - accessCriteria.nftContract, - accessCriteria.merkleRoot, - true - ); - } else { - return ( - false, - accessCriteria.nftContract, - accessCriteria.merkleRoot, - false - ); - } + return ( + isRoundOpen, + accessCriteria.nftContract, + accessCriteria.merkleRoot, + isList + ); } /// @inheritdoc ILM_PC_FundingPot_v1 @@ -406,15 +398,16 @@ contract LM_PC_FundingPot_v1 is uint32 roundId = roundCount; Round storage round = rounds[roundId]; - round.roundStart = roundStart_; - round.roundEnd = roundEnd_; - round.roundCap = roundCap_; - round.hookContract = hookContract_; - round.hookFunction = hookFunction_; - round.autoClosure = autoClosure_; - round.accumulationMode = accumulationMode_; - - _validateRoundParameters(round); + _setAndValidateRoundParameters( + round, + roundStart_, + roundEnd_, + roundCap_, + hookContract_, + hookFunction_, + autoClosure_, + accumulationMode_ + ); emit RoundCreated( roundId, @@ -445,15 +438,16 @@ contract LM_PC_FundingPot_v1 is _validateEditRoundParameters(round); - round.roundStart = roundStart_; - round.roundEnd = roundEnd_; - round.roundCap = roundCap_; - round.hookContract = hookContract_; - round.hookFunction = hookFunction_; - round.autoClosure = autoClosure_; - round.accumulationMode = accumulationMode_; - - _validateRoundParameters(round); + _setAndValidateRoundParameters( + round, + roundStart_, + roundEnd_, + roundCap_, + hookContract_, + hookFunction_, + autoClosure_, + accumulationMode_ + ); emit RoundEdited( roundId_, @@ -1033,15 +1027,6 @@ contract LM_PC_FundingPot_v1 is } } - // If round cap check clamped adjustedAmount to 0, and original amount was > 0, we already reverted. - // If original amount was 0, adjustedAmount is 0, so we can proceed to personal cap check which will also result in 0. - // If adjustedAmount > 0, proceed to personal cap check. - if (adjustedAmount == 0 && amount_ > 0) { - // This state should ideally not be reached due to the revert above if cap was full. - // But as a safeguard, if amount_ was > 0 and adjustedAmount is now 0, return 0. - return 0; - } - // --- Personal Cap Check --- // Only proceed if adjustedAmount wasn't already set to 0 by round cap or initial amount. if (adjustedAmount > 0) { @@ -1172,10 +1157,7 @@ contract LM_PC_FundingPot_v1 is } try IERC721(nftContract_).balanceOf(user_) returns (uint balance) { - if (balance == 0) { - return false; - } - return true; + return balance > 0; } catch { return false; } @@ -1196,11 +1178,7 @@ contract LM_PC_FundingPot_v1 is ) internal pure returns (bool) { bytes32 leaf = keccak256(abi.encodePacked(user_, roundId_)); - if (!MerkleProof.verify(merkleProof_, root_, leaf)) { - return false; - } - - return true; + return MerkleProof.verify(merkleProof_, root_, leaf); } /// @notice Handles round closure logic. @@ -1211,7 +1189,7 @@ contract LM_PC_FundingPot_v1 is roundIdToClosedStatus[roundId_] = true; - if (round.hookContract != address(0) && round.hookFunction.length > 0) { + if (round.hookContract != address(0)) { (bool success,) = round.hookContract.call(round.hookFunction); if (!success) { revert Module__LM_PC_FundingPot__HookExecutionFailed(); @@ -1264,7 +1242,7 @@ contract LM_PC_FundingPot_v1 is if (contributorTotal == 0) continue; for ( - uint8 accessCriteriaId = 0; + uint8 accessCriteriaId = 1; accessCriteriaId <= MAX_ACCESS_CRITERIA_ID; accessCriteriaId++ ) { @@ -1306,40 +1284,38 @@ contract LM_PC_FundingPot_v1 is returns (bytes32 flags, bytes32[] memory finalData) { if (start_ == 0) start_ = block.timestamp; - if (end_ == 0) end_ = block.timestamp; + if (end_ == 0) end_ = block.timestamp; // Note: cliff_ is not defaulted here. flags = 0; - bytes32[] memory data = new bytes32[](3); // For start, cliff, and end uint8 flagCount = 0; + bytes32[3] memory tempData; // Fixed-size array on stack for intermediate values - if (start_ > 0) { - flags |= bytes32(uint(1) << FLAG_START); - data[flagCount] = bytes32(start_); - unchecked { - flagCount++; - } + // Start time + flags |= bytes32(uint(1) << FLAG_START); + tempData[flagCount] = bytes32(start_); + unchecked { + flagCount++; } if (cliff_ > 0) { flags |= bytes32(uint(1) << FLAG_CLIFF); - data[flagCount] = bytes32(cliff_); + tempData[flagCount] = bytes32(cliff_); unchecked { flagCount++; } } - if (end_ > 0) { - flags |= bytes32(uint(1) << FLAG_END); - data[flagCount] = bytes32(end_); - unchecked { - flagCount++; - } + // End time + flags |= bytes32(uint(1) << FLAG_END); + tempData[flagCount] = bytes32(end_); + unchecked { + flagCount++; } finalData = new bytes32[](flagCount); for (uint8 j = 0; j < flagCount; ++j) { unchecked { - finalData[j] = data[j]; + finalData[j] = tempData[j]; } } @@ -1399,15 +1375,22 @@ contract LM_PC_FundingPot_v1 is if (totalContributions == 0) { revert Module__LM_PC_FundingPot__NoContributions(); } - // approve the fundingManager to spend the contribution token - IERC20(__Module_orchestrator.fundingManager().token()).approve( - address(__Module_orchestrator.fundingManager()), totalContributions - ); - uint minAmountOut = IBondingCurveBase_v1( - address(__Module_orchestrator.fundingManager()) - ).calculatePurchaseReturn(totalContributions); - IBondingCurveBase_v1(address(__Module_orchestrator.fundingManager())) - .buyFor(address(this), totalContributions, minAmountOut); + + // Cache the funding manager instance and its address + IFundingManager_v1 fundingManager = + __Module_orchestrator.fundingManager(); + + // Get the contribution token from the cached funding manager instance and approve it + IERC20 contributionToken = fundingManager.token(); + contributionToken.approve(address(fundingManager), totalContributions); + + // Cast the cached funding manager address to the bonding curve interface + IBondingCurveBase_v1 bondingCurve = + IBondingCurveBase_v1(address(fundingManager)); + + uint minAmountOut = + bondingCurve.calculatePurchaseReturn(totalContributions); + bondingCurve.buyFor(address(this), totalContributions, minAmountOut); roundTokensBought[roundId_] = minAmountOut; } @@ -1427,4 +1410,34 @@ contract LM_PC_FundingPot_v1 is bool timeEnded = round.roundEnd > 0 && block.timestamp >= round.roundEnd; return capReached || timeEnded; } + + /// @notice Sets and validates the round parameters. + /// @param roundToSet_ The round storage object to set parameters for. + /// @param roundStart_ Start timestamp for the round. + /// @param roundEnd_ End timestamp for the round. + /// @param roundCap_ Maximum contribution cap. + /// @param hookContract_ Address of contract to call after round closure. + /// @param hookFunction_ Encoded function call for the hook. + /// @param autoClosure_ Whether hook closure coincides with contribution span end. + /// @param accumulationMode_ Defines how caps accumulate. + function _setAndValidateRoundParameters( + Round storage roundToSet_, + uint roundStart_, + uint roundEnd_, + uint roundCap_, + address hookContract_, + bytes memory hookFunction_, + bool autoClosure_, + AccumulationMode accumulationMode_ + ) internal { + roundToSet_.roundStart = roundStart_; + roundToSet_.roundEnd = roundEnd_; + roundToSet_.roundCap = roundCap_; + roundToSet_.hookContract = hookContract_; + roundToSet_.hookFunction = hookFunction_; + roundToSet_.autoClosure = autoClosure_; + roundToSet_.accumulationMode = accumulationMode_; + + _validateRoundParameters(roundToSet_); + } } From fd065cae4cb3d086754ad63ce4e6c475bcf958b9 Mon Sep 17 00:00:00 2001 From: Fabian Scherer Date: Sun, 18 May 2025 21:19:44 +0200 Subject: [PATCH 116/130] chore: custom compiler settings for FP --- foundry.toml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/foundry.toml b/foundry.toml index bed77f705..6c3ee62e3 100644 --- a/foundry.toml +++ b/foundry.toml @@ -10,6 +10,10 @@ optimizer = true optimizer_runs = 750 via_ir = false +compilation_restrictions = [ + { paths = "src/modules/logicModule/LM_PC_FundingPot_v1.sol", optimizer = true, optimizer_runs = 200 }, +] + [fuzz] runs = 256 From 881630cb0202ccca7243a683e108840dbe5cee86 Mon Sep 17 00:00:00 2001 From: Fabian Scherer Date: Sun, 18 May 2025 21:26:02 +0200 Subject: [PATCH 117/130] fix: adds missing additional_compiler_profiles --- foundry.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/foundry.toml b/foundry.toml index 6c3ee62e3..d25e19906 100644 --- a/foundry.toml +++ b/foundry.toml @@ -10,6 +10,7 @@ optimizer = true optimizer_runs = 750 via_ir = false +additional_compiler_profiles = [ { name = "large-contracts", optimizer = true, optimizer_runs = 200 } ] compilation_restrictions = [ { paths = "src/modules/logicModule/LM_PC_FundingPot_v1.sol", optimizer = true, optimizer_runs = 200 }, ] From 4145835dac79a381edbed9c67b65c7c3d85258cb Mon Sep 17 00:00:00 2001 From: Fabian Scherer Date: Mon, 19 May 2025 10:24:20 +0200 Subject: [PATCH 118/130] fix: max personal cap defined by highest access criteria --- foundry.toml | 4 +- .../logicModule/LM_PC_FundingPot_v1.sol | 45 ++++-- .../interfaces/ILM_PC_FundingPot_v1.sol | 3 + .../logicModule/LM_PC_FundingPot_v1.t.sol | 143 ++++++++++++++++++ 4 files changed, 177 insertions(+), 18 deletions(-) diff --git a/foundry.toml b/foundry.toml index d25e19906..ac537950a 100644 --- a/foundry.toml +++ b/foundry.toml @@ -10,9 +10,9 @@ optimizer = true optimizer_runs = 750 via_ir = false -additional_compiler_profiles = [ { name = "large-contracts", optimizer = true, optimizer_runs = 200 } ] +additional_compiler_profiles = [ { name = "large-contracts", optimizer = true, optimizer_runs = 100 } ] compilation_restrictions = [ - { paths = "src/modules/logicModule/LM_PC_FundingPot_v1.sol", optimizer = true, optimizer_runs = 200 }, + { paths = "src/modules/logicModule/LM_PC_FundingPot_v1.sol", optimizer = true, optimizer_runs = 100 }, ] [fuzz] diff --git a/src/modules/logicModule/LM_PC_FundingPot_v1.sol b/src/modules/logicModule/LM_PC_FundingPot_v1.sol index 9cb63e0a6..280ff2d69 100644 --- a/src/modules/logicModule/LM_PC_FundingPot_v1.sol +++ b/src/modules/logicModule/LM_PC_FundingPot_v1.sol @@ -644,49 +644,62 @@ contract LM_PC_FundingPot_v1 is bytes32[] memory merkleProof_, UnspentPersonalRoundCap[] calldata unspentPersonalRoundCaps_ ) external { - uint unspentPersonalCap; + uint unspentPersonalCap = 0; // Initialize to 0 + uint32 lastSeenRoundId = 0; // Tracks the last seen roundId to ensure strictly increasing order // Process each previous round cap that the user wants to carry over for (uint i = 0; i < unspentPersonalRoundCaps_.length; i++) { UnspentPersonalRoundCap memory roundCapInfo = unspentPersonalRoundCaps_[i]; + uint32 currentProcessingRoundId = roundCapInfo.roundId; + + // Enforcement: Round IDs in the array must be strictly increasing. + if (currentProcessingRoundId <= lastSeenRoundId) { + revert + Module__LM_PC_FundingPot__UnspentCapsRoundIdsNotStrictlyIncreasing( + ); + } // Skip if this round is before the global accumulation start round - if (roundCapInfo.roundId < globalAccumulationStartRoundId) { + if (currentProcessingRoundId < globalAccumulationStartRoundId) { + lastSeenRoundId = currentProcessingRoundId; // Update lastSeenRoundId before continuing continue; } // For PERSONAL cap rollover, the PREVIOUS round must have allowed it (Personal or All). if ( - rounds[roundCapInfo.roundId].accumulationMode + rounds[currentProcessingRoundId].accumulationMode != AccumulationMode.Personal - && rounds[roundCapInfo.roundId].accumulationMode + && rounds[currentProcessingRoundId].accumulationMode != AccumulationMode.All ) { + lastSeenRoundId = currentProcessingRoundId; // Update lastSeenRoundId before continuing continue; } - // Verify the user was eligible for this access criteria in the previous round - bool isEligible = _checkAccessCriteriaEligibility( - roundCapInfo.roundId, - roundCapInfo.accessCriteriaId, - roundCapInfo.merkleProof, - user_ - ); - - if (isEligible) { + if ( + _checkAccessCriteriaEligibility( + currentProcessingRoundId, + roundCapInfo.accessCriteriaId, + roundCapInfo.merkleProof, + user_ + ) + ) { AccessCriteriaPrivileges storage privileges = - roundIdToAccessCriteriaIdToPrivileges[roundCapInfo.roundId][roundCapInfo + roundIdToAccessCriteriaIdToPrivileges[currentProcessingRoundId][roundCapInfo .accessCriteriaId]; uint userContribution = - roundIdToUserToContribution[roundCapInfo.roundId][user_]; + roundIdToUserToContribution[currentProcessingRoundId][user_]; uint personalCap = privileges.personalCap; + uint unspentForThisEntry = 0; if (userContribution < personalCap) { - unspentPersonalCap += (personalCap - userContribution); + unspentForThisEntry = personalCap - userContribution; } + unspentPersonalCap += unspentForThisEntry; } + lastSeenRoundId = currentProcessingRoundId; // Update after processing or skipping } _contributeToRoundFor( diff --git a/src/modules/logicModule/interfaces/ILM_PC_FundingPot_v1.sol b/src/modules/logicModule/interfaces/ILM_PC_FundingPot_v1.sol index b3fb988ea..915a61dfd 100644 --- a/src/modules/logicModule/interfaces/ILM_PC_FundingPot_v1.sol +++ b/src/modules/logicModule/interfaces/ILM_PC_FundingPot_v1.sol @@ -317,6 +317,9 @@ interface ILM_PC_FundingPot_v1 is IERC20PaymentClientBase_v2 { uint32 startRoundId_, uint32 currentRoundCount_ ); + /// @notice Thrown when round IDs in UnspentPersonalRoundCap array are not strictly increasing. + error Module__LM_PC_FundingPot__UnspentCapsRoundIdsNotStrictlyIncreasing(); + // ------------------------------------------------------------------------- // Public - Getters diff --git a/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol b/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol index 61d6829f3..0604d87eb 100644 --- a/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol +++ b/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol @@ -5304,4 +5304,147 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { "R2 Total contributions should be R2 base total cap (All mode, global_start=R2)" ); } + + function testContributeToRoundFor_revertsGivenUnspentCapsRoundIdsNotStrictlyIncreasing( + ) public { + // Setup: Round 1 (Personal), Round 2 (Personal), Round 3 (Personal for contribution) + uint initialTimestamp = block.timestamp; + uint8 accessId = 1; // Open access + uint personalCap = 500; + uint roundCap = 10_000; + + vm.startPrank(contributor1_); + _token.approve(address(fundingPot), type(uint).max); + vm.stopPrank(); + + // Round 1 + uint32 round1Id = fundingPot.createRound( + initialTimestamp + 1 days, + initialTimestamp + 2 days, + roundCap, + address(0), + bytes(""), + false, + ILM_PC_FundingPot_v1.AccumulationMode.Personal + ); + fundingPot.setAccessCriteria( + round1Id, accessId, 0, address(0), bytes32(0), new address[](0) + ); + fundingPot.setAccessCriteriaPrivileges( + round1Id, accessId, personalCap, false, 0, 0, 0 + ); + + // Round 2 + uint32 round2Id = fundingPot.createRound( + initialTimestamp + 3 days, + initialTimestamp + 4 days, + roundCap, + address(0), + bytes(""), + false, + ILM_PC_FundingPot_v1.AccumulationMode.Personal + ); + fundingPot.setAccessCriteria( + round2Id, accessId, 0, address(0), bytes32(0), new address[](0) + ); + fundingPot.setAccessCriteriaPrivileges( + round2Id, accessId, personalCap, false, 0, 0, 0 + ); + + // Round 3 (target for contribution) + uint32 round3Id = fundingPot.createRound( + initialTimestamp + 5 days, + initialTimestamp + 6 days, + roundCap, + address(0), + bytes(""), + false, + ILM_PC_FundingPot_v1.AccumulationMode.Personal + ); + fundingPot.setAccessCriteria( + round3Id, accessId, 0, address(0), bytes32(0), new address[](0) + ); + fundingPot.setAccessCriteriaPrivileges( + round3Id, accessId, personalCap, false, 0, 0, 0 + ); + + vm.warp(initialTimestamp + 5 days + 1 hours); // Enter Round 3 + + // Case 1: Out of order + ILM_PC_FundingPot_v1.UnspentPersonalRoundCap[] memory + unspentCapsOutOfOrder = + new ILM_PC_FundingPot_v1.UnspentPersonalRoundCap[](2); + unspentCapsOutOfOrder[0] = ILM_PC_FundingPot_v1.UnspentPersonalRoundCap( + round2Id, accessId, new bytes32[](0) + ); + unspentCapsOutOfOrder[1] = ILM_PC_FundingPot_v1.UnspentPersonalRoundCap( + round1Id, accessId, new bytes32[](0) + ); + + vm.startPrank(contributor1_); + vm.expectRevert( + ILM_PC_FundingPot_v1 + .Module__LM_PC_FundingPot__UnspentCapsRoundIdsNotStrictlyIncreasing + .selector + ); + fundingPot.contributeToRoundFor( + contributor1_, + round3Id, + 100, + accessId, + new bytes32[](0), + unspentCapsOutOfOrder + ); + vm.stopPrank(); + + // Case 2: Duplicate roundId + ILM_PC_FundingPot_v1.UnspentPersonalRoundCap[] memory + unspentCapsDuplicate = + new ILM_PC_FundingPot_v1.UnspentPersonalRoundCap[](2); + unspentCapsDuplicate[0] = ILM_PC_FundingPot_v1.UnspentPersonalRoundCap( + round1Id, accessId, new bytes32[](0) + ); + unspentCapsDuplicate[1] = ILM_PC_FundingPot_v1.UnspentPersonalRoundCap( + round1Id, accessId, new bytes32[](0) + ); + + vm.startPrank(contributor1_); + vm.expectRevert( + ILM_PC_FundingPot_v1 + .Module__LM_PC_FundingPot__UnspentCapsRoundIdsNotStrictlyIncreasing + .selector + ); + fundingPot.contributeToRoundFor( + contributor1_, + round3Id, + 100, + accessId, + new bytes32[](0), + unspentCapsDuplicate + ); + vm.stopPrank(); + + // Case 3: Correct order but first element's roundId is 0 (if lastSeenRoundId starts at 0) + // This specific case won't be hit if round IDs must be >0, but good to be aware. + // Assuming valid round IDs start from 1, this case might not be directly testable if 0 isn't a valid roundId. + // The current check `currentProcessingRoundId <= lastSeenRoundId` covers this if roundId can be 0. + // If round IDs are always >= 1, then an initial lastSeenRoundId=0 is fine. + + // Case 4: Empty array (should not revert with this specific error, but pass) + ILM_PC_FundingPot_v1.UnspentPersonalRoundCap[] memory unspentCapsEmpty = + new ILM_PC_FundingPot_v1.UnspentPersonalRoundCap[](0); + vm.startPrank(contributor1_); + fundingPot.contributeToRoundFor( // This should pass (or revert with a different error if amount is 0 etc.) + contributor1_, + round3Id, + 100, + accessId, + new bytes32[](0), + unspentCapsEmpty + ); + vm.stopPrank(); + assertEq( + fundingPot.getUserContributionToRound(round3Id, contributor1_), 100 + ); + } } From a943aefcec82247281efb62eb58cb545df8f9bdd Mon Sep 17 00:00:00 2001 From: Fabian Scherer Date: Mon, 19 May 2025 11:39:17 +0200 Subject: [PATCH 119/130] chore: test structure cleanup --- .../logicModule/LM_PC_FundingPot_v1.t.sol | 4911 ++++++++--------- 1 file changed, 2399 insertions(+), 2512 deletions(-) diff --git a/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol b/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol index 0604d87eb..a308c3de2 100644 --- a/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol +++ b/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol @@ -1635,19 +1635,19 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { │ └── Then the funds are transferred to the funding pot │ And the contribution is recorded │ - ├── Given the access criteria is NFT - │ And the user fulfills the access criteria - │ └── When the user contributes to the round - │ └── Then the funds are transferred to the funding pot - │ And the contribution is recorded + ├── Given the access criteria is NFT + │ And the user fulfills the access criteria + │ └── When the user contributes to the round + │ └── Then the funds are transferred to the funding pot + │ And the contribution is recorded │ - ├── Given the access criteria is MERKLE + ├── Given the access criteria is MERKLE │ And the user fulfills the access criteria │ └── When the user contributes to the round │ └── Then the funds are transferred to the funding pot │ And the contribution is recorded │ - ├── Given the access criteria is LIST + ├── Given the access criteria is LIST │ And the user fulfills the access criteria │ └── When the user contributes to the round │ └── Then the funds are transferred to the funding pot @@ -1660,16 +1660,16 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { │ And round closure is initiated │ ├── Given the user fulfills the access criteria - │ └── And the user has already contributed their personal cap partially - │ └── When the user attempts to contribute more than their personal cap - │ └── Then only the amount up to the cap is accepted as contribution - │ And the contribution is recorded + │ And the user has already contributed their personal cap partially + │ └── When the user attempts to contribute more than their personal cap + │ └── Then only the amount up to the cap is accepted as contribution + │ And the contribution is recorded │ ├── Given the user fulfills the access criteria - │ └── And their access criteria has the privilege to override the contribution span - │ └── When - │ └── And the user attempts to contribute - │ └── Then the contribution is still recorded + │ And their access criteria has the privilege to override the contribution span + │ └── When + │ └── And the user attempts to contribute + │ └── Then the contribution is still recorded │ ├── Given the user fulfills the access criteria │ And the round is set to have global accumulative caps @@ -1685,8 +1685,85 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { │ └── Then they can in total contribute Z + X - Y │ And the funds are transferred into the funding pot │ + ├── Given globalAccumulationStartRoundId is set to 2 (e.g., R2) + │ ├── And target round (e.g., R3) uses AccumulationMode.Personal + │ │ └── When contributing to R3 with unspent capacity from R1 and R2 + │ │ └── Then only unspent personal capacity from R2 should be considered + │ ├── And target round (e.g., R3) uses AccumulationMode.Total + │ │ └── When contributing to R3 + │ │ └── Then only unspent total capacity from R2 should expand R3's effective cap + │ └── And target round (e.g., R3) uses AccumulationMode.All + │ ├── When contributing to R3 with unspent personal capacity from R1 and R2 + │ │ └── Then only unspent personal capacity from R2 should be considered + │ └── When calculating R3's effective total cap + │ └── Then only unspent total capacity from R2 should expand R3's effective cap + │ + ├── Given globalAccumulationStartRoundId is 1 (default) + │ ├── And target round (e.g., R2 or R3) uses AccumulationMode.Personal + │ │ └── When contributing with unspent capacity from all previous valid rounds (e.g., R1 for R2; R1 & R2 for R3) + │ │ └── Then unspent personal capacity from all applicable previous rounds should be considered + │ ├── And target round (e.g., R2 or R3) uses AccumulationMode.Total + │ │ └── When calculating effective total cap + │ │ └── Then unspent total capacity from all applicable previous rounds should expand the effective cap + │ └── And target round (e.g., R2 or R3) uses AccumulationMode.All + │ ├── When contributing with unspent personal capacity from all previous valid rounds + │ │ └── Then unspent personal capacity from all applicable previous rounds should be considered + │ └── When calculating effective total cap + │ └── Then unspent total capacity from all applicable previous rounds should expand the effective cap + │ + ├── Given target round's AccumulationMode is Disabled + │ └── When globalAccumulationStartRoundId is set to allow previous rounds + │ └── Then no accumulation (personal or total) should occur for the target round + │ + ├── Given globalAccumulationStartRoundId is set to the target round's ID + │ └── When target round's AccumulationMode would normally allow accumulation + │ └── Then no accumulation (personal or total) from any previous round should occur + │ + ├── Given globalAccumulationStartRoundId is set to R2 (or later) + │ ├── When target round (R3) uses AccumulationMode.Personal + │ │ And contributing to R3 with unspent capacity from R1 and R2 + │ │ └── Then only unspent personal capacity from R2 (and subsequent allowed rounds) should be considered + │ ├── When target round (R3) uses AccumulationMode.Total + │ │ And calculating R3's effective total cap + │ │ └── Then only unspent total capacity from R2 (and subsequent allowed rounds) should expand R3's cap + │ └── When target round (R3) uses AccumulationMode.All + │ ├── And contributing to R3 with unspent personal capacity from R1 and R2 + │ │ └── Then only unspent personal capacity from R2 (and subsequent) should be considered for personal limit + │ └── And calculating R3's effective total cap + │ └── Then only unspent total capacity from R2 (and subsequent) should expand R3's cap + │ + ├── Given globalAccumulationStartRoundId is 1 (default) + │ ├── When target round (e.g., R2 or R3) uses AccumulationMode.Personal + │ │ And contributing with unspent capacity from all previous valid rounds (e.g., R1 for R2; R1 & R2 for R3) + │ │ └── Then unspent personal capacity from all applicable previous rounds (>= global start) should be considered + │ ├── When target round (e.g., R2 or R3) uses AccumulationMode.Total + │ │ And calculating effective total cap + │ │ └── Then unspent total capacity from all applicable previous rounds (>= global start) should expand the cap + │ └── When target round (e.g., R2 or R3) uses AccumulationMode.All + │ ├── And contributing with unspent personal capacity from all previous valid rounds + │ │ └── Then unspent personal capacity from all applicable previous rounds (>= global start) should be considered + │ └── And calculating effective total cap + │ └── Then unspent total capacity from all applicable previous rounds (>= global start) should expand the cap + │ + ├── Given target round's AccumulationMode is Disabled + │ └── When globalAccumulationStartRoundId is set to allow previous rounds (e.g. 1) + │ └── Then no accumulation (personal or total) should occur for the target round from any previous round + │ + ├── Given globalAccumulationStartRoundId is set to the target round's ID (or a later round ID) + │ └── When target round's AccumulationMode would normally allow accumulation + │ └── Then no accumulation (personal or total) from any previous round should occur + │ + ├── Given rounds use AccumulationMode.Personal + │ └── When unspent capacity from previous rounds is available + │ └── Then only personal caps accumulate, while total caps do not accumulate + │ + └── Given rounds use AccumulationMode.Total + └── When unspent capacity from previous rounds is available + └── Then only total caps accumulate, while personal caps do not accumulate */ - function testcontributeToRoundFor_worksGivenAllConditionsMet() public { + function testContributeToRoundFor_worksGivenGenericConfigAndAccessCriteria() + public + { testCreateRound(); uint8 accessCriteriaId = 1; @@ -1742,7 +1819,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { assertEq(personalContributions, amount); } - function testcontributeToRoundFor_worksGivenUserCurrentContributionExceedsTheRoundCap( + function testContributeToRoundFor_worksGivenAccessCriteriaNFT( uint8 accessCriteriaEnumOld, uint8 accessCriteriaEnumNew ) public { @@ -2221,1222 +2298,1029 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { assertEq(fundingPot.getTotalRoundContribution(round2Id), 700); } - // ------------------------------------------------------------------------- - // Test: closeRound() - - /* - ├── Given user does not have FUNDING_POT_ADMIN_ROLE - │ └── When user attempts to close a round - │ └── Then it should revert with Module__CallerNotAuthorized - │ - ├── Given round does not exist - │ └── When user attempts to close the round - │ └── Then it should revert with Module__LM_PC_FundingPot__RoundNotCreated - │ - ├── Given hook execution fails - │ └── When user attempts to close the round - │ └── Then it should revert with Module__LM_PC_FundingPot__HookExecutionFailed - │ - ├── Given closure conditions are not met - │ └── When user attempts to close the round - │ └── Then it should revert with Module__LM_PC_FundingPot__ClosureConditionsNotMet - │ - ├── Given round has started but not ended - ├── Given round is already closed - │ └── When user attempts to close the round again - │ └── Then it should revert with Module__LM_PC_FundingPot__RoundHasEnded - │ - ├── Given round has started but not ended - │ └── And round cap has not been reached - │ └── And user has contributed successfully - │ └── When user attempts to close the round - │ └── Then it should not revert and round should be closed - │ └── And payment orders should be created correctly - │ - ├── Given round has ended (by time) - │ └── And user has contributed during active round - │ └── When user attempts to close the round - │ └── Then it should not revert and round should be closed - │ └── And payment orders should be created correctly - │ - ├── Given round cap has been reached - │ └── And user has contributed up to the cap - │ └── When user attempts to close the round - │ └── Then it should not revert and round should be closed - │ └── And payment orders should be created correctly - -── Given round cap has been reached - │ └── And the round is set up for autoclosure - │ └── And user has contributed up to the cap - │ └── Then it should not revert and round should be closed - │ └── And payment orders should be created correctly - └── Given multiple users contributed before round ended or cap reached - └── When round is closed - └── Then it should not revert and round should be closed - └── And payment orders should be created for all contributors - */ - function testCloseRound_revertsGivenUserIsNotFundingPotAdmin(address user_) + function testContributeToRoundFor_globalStartRestrictsPersonalAccumulation() public { - vm.assume(user_ != address(0) && user_ != address(this)); - - testCreateRound(); - uint32 roundId = fundingPot.getRoundCount(); + // SCENARIO: globalAccumulationStartRoundId = 2 restricts accumulation from Round 1 for Personal mode + // 1. Setup: Round 1, Round 2, Round 3. Partial contributions in R1 & R2. + // 2. Action: setGlobalAccumulationStart(2) + // 3. Verification: For contributions to R3 (Personal mode), only unused personal from R2 rolls over. - vm.startPrank(user_); - bytes32 roleId = _authorizer.generateRoleId( - address(fundingPot), fundingPot.FUNDING_POT_ADMIN_ROLE() - ); - vm.expectRevert( - abi.encodeWithSelector( - IModule_v1.Module__CallerNotAuthorized.selector, roleId, user_ - ) - ); - fundingPot.closeRound(roundId); - vm.stopPrank(); - } + uint initialTimestamp = block.timestamp; - function testFuzzCloseRound_revertsGivenRoundDoesNotExist( - uint8 accessCriteriaEnum - ) public { - vm.assume(accessCriteriaEnum >= 0 && accessCriteriaEnum <= 4); + // --- Setup Rounds --- + uint r1PersonalCap = 500; + uint r1Contribution = 100; + // uint r1UnusedPersonal = r1PersonalCap - r1Contribution; // Not used in this restricted scenario directly for R3 calc - uint32 roundId = fundingPot.getRoundCount(); + uint r2PersonalCap = 600; + uint r2Contribution = 200; + uint r2UnusedPersonal = r2PersonalCap - r2Contribution; // This IS used for R3 calculation - ( - address nftContract, - bytes32 merkleRoot, - address[] memory allowedAddresses - ) = _helper_createAccessCriteria(accessCriteriaEnum, roundId); + uint r3BasePersonalCap = 300; - vm.expectRevert( - abi.encodeWithSelector( - ILM_PC_FundingPot_v1 - .Module__LM_PC_FundingPot__RoundNotCreated - .selector - ) + // Round 1 + uint32 round1Id = fundingPot.createRound( + initialTimestamp + 1 days, + initialTimestamp + 2 days, + 10_000, // large round cap + address(0), + bytes(""), + false, + ILM_PC_FundingPot_v1.AccumulationMode.Personal ); - fundingPot.closeRound(roundId); - } - - function testCloseRound_revertsGivenHookExecutionFails() public { - uint8 accessCriteriaId = - uint8(ILM_PC_FundingPot_v1.AccessCriteriaType.OPEN); - - uint32 roundId = fundingPot.createRound( - _defaultRoundParams.roundStart, - _defaultRoundParams.roundEnd, - _defaultRoundParams.roundCap, - address(failingHook), - abi.encodeWithSignature("executeHook()"), - _defaultRoundParams.autoClosure, - _defaultRoundParams.accumulationMode + fundingPot.setAccessCriteria( + round1Id, 1, 0, address(0), bytes32(0), new address[](0) + ); // Open access + fundingPot.setAccessCriteriaPrivileges( + round1Id, 1, r1PersonalCap, false, 0, 0, 0 ); - ( - address nftContract, - bytes32 merkleRoot, - address[] memory allowedAddresses - ) = _helper_createAccessCriteria(accessCriteriaId, roundId); - + // Round 2 + uint32 round2Id = fundingPot.createRound( + initialTimestamp + 3 days, + initialTimestamp + 4 days, + 10_000, // large round cap + address(0), + bytes(""), + false, + ILM_PC_FundingPot_v1.AccumulationMode.Personal + ); fundingPot.setAccessCriteria( - roundId, - accessCriteriaId, - 0, - nftContract, - merkleRoot, - allowedAddresses + round2Id, 1, 0, address(0), bytes32(0), new address[](0) + ); + fundingPot.setAccessCriteriaPrivileges( + round2Id, 1, r2PersonalCap, false, 0, 0, 0 ); - fundingPot.setAccessCriteriaPrivileges(roundId, 0, 1000, false, 0, 0, 0); + // Round 3 + uint32 round3Id = fundingPot.createRound( + initialTimestamp + 5 days, + initialTimestamp + 6 days, + 10_000, // large round cap + address(0), + bytes(""), + false, + ILM_PC_FundingPot_v1.AccumulationMode.Personal + ); + fundingPot.setAccessCriteria( + round3Id, 1, 0, address(0), bytes32(0), new address[](0) + ); + fundingPot.setAccessCriteriaPrivileges( + round3Id, 1, r3BasePersonalCap, false, 0, 0, 0 + ); - vm.warp(_defaultRoundParams.roundEnd + 1); + // --- Contributions --- + vm.startPrank(contributor1_); + _token.approve(address(fundingPot), type(uint).max); - vm.expectRevert( - ILM_PC_FundingPot_v1 - .Module__LM_PC_FundingPot__HookExecutionFailed - .selector + vm.warp(initialTimestamp + 1 days + 1 hours); // Enter Round 1 + fundingPot.contributeToRoundFor( + contributor1_, round1Id, r1Contribution, 1, new bytes32[](0) ); - fundingPot.closeRound(roundId); - } - function testCloseRound_revertsGivenClosureConditionsNotMet() public { - uint8 accessCriteriaId = - uint8(ILM_PC_FundingPot_v1.AccessCriteriaType.OPEN); + vm.warp(initialTimestamp + 3 days + 1 hours); // Enter Round 2 + fundingPot.contributeToRoundFor( + contributor1_, round2Id, r2Contribution, 1, new bytes32[](0) + ); + vm.stopPrank(); - _helper_setupRoundWithAccessCriteria(accessCriteriaId); - uint32 roundId = fundingPot.getRoundCount(); + // --- Set Global Start --- + fundingPot.setGlobalAccumulationStart(2); + assertEq(fundingPot.getGlobalAccumulationStartRoundId(), 2); - fundingPot.setAccessCriteriaPrivileges(roundId, 0, 1000, false, 0, 0, 0); + // --- Attempt Contribution in Round 3 --- + vm.warp(initialTimestamp + 5 days + 1 hours); // Enter Round 3 - vm.expectRevert( - ILM_PC_FundingPot_v1 - .Module__LM_PC_FundingPot__ClosureConditionsNotMet - .selector + ILM_PC_FundingPot_v1.UnspentPersonalRoundCap[] memory unspentCaps = + new ILM_PC_FundingPot_v1.UnspentPersonalRoundCap[](2); + // User claims unspent from R1 (should be ignored due to global start) + unspentCaps[0] = ILM_PC_FundingPot_v1.UnspentPersonalRoundCap( + round1Id, 1, new bytes32[](0) ); - fundingPot.closeRound(roundId); - } - - function testCloseRound_revertsGivenRoundHasAlreadyBeenClosed() public { - uint8 accessCriteriaId = - uint8(ILM_PC_FundingPot_v1.AccessCriteriaType.OPEN); - - _helper_setupRoundWithAccessCriteria(accessCriteriaId); - uint32 roundId = fundingPot.getRoundCount(); - fundingPot.setAccessCriteriaPrivileges( - roundId, accessCriteriaId, 1000, false, 0, 0, 0 + // User claims unspent from R2 (should be counted) + unspentCaps[1] = ILM_PC_FundingPot_v1.UnspentPersonalRoundCap( + round2Id, 1, new bytes32[](0) ); - (uint roundStart,,,,,,) = fundingPot.getRoundGenericParameters(roundId); - - vm.warp(roundStart + 1); + uint expectedR3PersonalCap = r3BasePersonalCap + r2Contribution; // Only R2's unused personal cap vm.startPrank(contributor1_); - _token.approve(address(fundingPot), 1000); + // Attempt to contribute up to the expected new personal cap fundingPot.contributeToRoundFor( - contributor1_, roundId, 1000, accessCriteriaId, new bytes32[](0) + contributor1_, + round3Id, + expectedR3PersonalCap, + 1, + new bytes32[](0), + unspentCaps ); vm.stopPrank(); - fundingPot.closeRound(roundId); - vm.expectRevert( - ILM_PC_FundingPot_v1 - .Module__LM_PC_FundingPot__RoundHasEnded - .selector + // --- Assertion --- + assertEq( + fundingPot.getUserContributionToRound(round3Id, contributor1_), + expectedR3PersonalCap, + "R3 personal contribution incorrect" ); - fundingPot.closeRound(roundId); } - function testCloseRound_worksGivenRoundHasStartedButNotEnded() public { - testCreateRound(); - uint32 roundId = fundingPot.getRoundCount(); + function testContributeToRoundFor_globalStartRestrictsTotalAccumulation() + public + { + // SCENARIO: globalAccumulationStartRoundId = 2 restricts accumulation from Round 1 for Total mode + // 1. Setup: Round 1, Round 2, Round 3. Partial contributions in R1 & R2. + // 2. Action: setGlobalAccumulationStart(2) + // 3. Verification: For contributions to R3 (Total mode), only unused total from R2 expands R3 cap. - uint8 accessCriteriaId = 1; - uint8 accessType = uint8(ILM_PC_FundingPot_v1.AccessCriteriaType.OPEN); - ( - address nftContract, - bytes32 merkleRoot, - address[] memory allowedAddresses - ) = _helper_createAccessCriteria(accessType, roundId); + uint initialTimestamp = block.timestamp; + + // --- Setup Rounds --- + uint r1BaseCap = 1000; + uint r1Contribution = 400; + // uint r1UnusedTotal = r1BaseCap - r1Contribution; + + uint r2BaseCap = 1200; + uint r2Contribution = 500; + // uint r2UnusedTotal = r2BaseCap - r2Contribution; + + uint r3BaseCap = 300; + // Round 1 + uint32 round1Id = fundingPot.createRound( + initialTimestamp + 1 days, + initialTimestamp + 2 days, + r1BaseCap, + address(0), + bytes(""), + false, + ILM_PC_FundingPot_v1.AccumulationMode.Total + ); fundingPot.setAccessCriteria( - roundId, accessType, 0, nftContract, merkleRoot, allowedAddresses + round1Id, 1, 0, address(0), bytes32(0), new address[](0) + ); // Open access + fundingPot.setAccessCriteriaPrivileges( + round1Id, 1, r1BaseCap, false, 0, 0, 0 + ); // Personal cap equals round cap + + // Round 2 + uint32 round2Id = fundingPot.createRound( + initialTimestamp + 3 days, + initialTimestamp + 4 days, + r2BaseCap, + address(0), + bytes(""), + false, + ILM_PC_FundingPot_v1.AccumulationMode.Total + ); + fundingPot.setAccessCriteria( + round2Id, 1, 0, address(0), bytes32(0), new address[](0) ); fundingPot.setAccessCriteriaPrivileges( - roundId, accessCriteriaId, 1000, false, 0, 0, 0 + round2Id, 1, r2BaseCap, false, 0, 0, 0 ); - // Warp to round start - (uint roundStart,,,,,,) = fundingPot.getRoundGenericParameters(roundId); - vm.warp(roundStart + 1); + // Round 3 + uint32 round3Id = fundingPot.createRound( + initialTimestamp + 5 days, + initialTimestamp + 6 days, + r3BaseCap, + address(0), + bytes(""), + false, + ILM_PC_FundingPot_v1.AccumulationMode.Total + ); + fundingPot.setAccessCriteria( + round3Id, 1, 0, address(0), bytes32(0), new address[](0) + ); + fundingPot.setAccessCriteriaPrivileges( + round3Id, 1, r3BaseCap + r2Contribution, false, 0, 0, 0 + ); // Allow full contribution for testing effective cap - // Make a contribution + // --- Contributions --- vm.startPrank(contributor1_); - _token.approve(address(fundingPot), 1000); + _token.approve(address(fundingPot), type(uint).max); + + vm.warp(initialTimestamp + 1 days + 1 hours); // Enter Round 1 fundingPot.contributeToRoundFor( - contributor1_, roundId, 1000, accessCriteriaId, new bytes32[](0) + contributor1_, round1Id, r1Contribution, 1, new bytes32[](0) ); - vm.stopPrank(); - - // Close the round - fundingPot.closeRound(roundId); - - // Verify round is closed - assertEq(fundingPot.isRoundClosed(roundId), true); - } - - function testCloseRound_worksGivenRoundHasEnded() public { - testCreateRound(); - uint32 roundId = fundingPot.getRoundCount(); - uint8 accessCriteriaId = 1; - uint8 accessType = uint8(ILM_PC_FundingPot_v1.AccessCriteriaType.OPEN); + vm.warp(initialTimestamp + 3 days + 1 hours); // Enter Round 2 + fundingPot.contributeToRoundFor( + contributor1_, round2Id, r2Contribution, 1, new bytes32[](0) + ); + vm.stopPrank(); - ( - address nftContract, - bytes32 merkleRoot, - address[] memory allowedAddresses - ) = _helper_createAccessCriteria(accessType, roundId); + // --- Set Global Start --- + fundingPot.setGlobalAccumulationStart(2); + assertEq(fundingPot.getGlobalAccumulationStartRoundId(), 2); - fundingPot.setAccessCriteria( - roundId, accessType, 0, nftContract, merkleRoot, allowedAddresses - ); - fundingPot.setAccessCriteriaPrivileges( - roundId, accessCriteriaId, 1000, false, 0, 0, 0 - ); + // --- Attempt Contribution in Round 3 --- + vm.warp(initialTimestamp + 5 days + 1 hours); // Enter Round 3 - // Make a contribution - (uint roundStart, uint roundEnd,,,,,) = - fundingPot.getRoundGenericParameters(roundId); - vm.warp(roundStart + 1); + uint expectedR3EffectiveCap = r3BaseCap + r2Contribution; // Only R2's unused total cap vm.startPrank(contributor1_); - _token.approve(address(fundingPot), 500); + // Attempt to contribute up to the expected new effective cap fundingPot.contributeToRoundFor( - contributor1_, roundId, 500, accessCriteriaId, new bytes32[](0) + contributor1_, round3Id, expectedR3EffectiveCap, 1, new bytes32[](0) ); vm.stopPrank(); - // Warp to after round end - vm.warp(roundEnd + 1); + // --- Assertion --- + assertEq( + fundingPot.getTotalRoundContribution(round3Id), + expectedR3EffectiveCap, + "R3 total contribution incorrect, effective cap not as expected" + ); + assertEq( + fundingPot.getUserContributionToRound(round3Id, contributor1_), + expectedR3EffectiveCap, + "R3 user contribution incorrect" + ); + } - // Close the round - fundingPot.closeRound(roundId); + function testContributeToRoundFor_defaultGlobalStartAllowsPersonalAccumulation( + ) public { + // SCENARIO: Default globalAccumulationStartRoundId = 1 allows accumulation from R1 for R2 (Personal mode) + // 1. Setup: Round 1 (Personal), Round 2 (Personal). + // Partial contribution by C1 in R1. + // 2. Action: Verify getGlobalAccumulationStartRoundId() == 1 (default). + // 3. Verification: For C1's contribution to R2, unused personal capacity from R1 rolls over. - // Verify round is closed - assertEq(fundingPot.isRoundClosed(roundId), true); - } + uint initialTimestamp = block.timestamp; + uint8 accessId = 1; // Open access - function testCloseRound_worksGivenRoundCapHasBeenReached() public { - testCreateRound(); + // --- Round Parameters & Contributions for C1 --- + uint r1PersonalCapC1 = 500; + uint r1ContributionC1 = 100; // C1 leaves 400 personal unused from R1 - uint32 roundId = fundingPot.getRoundCount(); - uint8 accessCriteriaId = 1; - uint8 accessType = uint8(ILM_PC_FundingPot_v1.AccessCriteriaType.NFT); + uint r2BasePersonalCapC1 = 300; // C1's base personal cap in R2 - uint amount = 1000; + // --- Approvals --- + vm.startPrank(contributor1_); + _token.approve(address(fundingPot), type(uint).max); + vm.stopPrank(); - ( - address nftContract, - bytes32 merkleRoot, - address[] memory allowedAddresses - ) = _helper_createAccessCriteria(accessType, roundId); + // --- Create Round 1 (Personal Mode) --- + uint32 round1Id = fundingPot.createRound( + initialTimestamp + 1 days, + initialTimestamp + 2 days, + 10_000, // Large round cap, not relevant for personal accumulation focus + address(0), + bytes(""), + false, + ILM_PC_FundingPot_v1.AccumulationMode.Personal + ); + fundingPot.setAccessCriteria( + round1Id, accessId, 0, address(0), bytes32(0), new address[](0) + ); + fundingPot.setAccessCriteriaPrivileges( + round1Id, accessId, r1PersonalCapC1, false, 0, 0, 0 + ); + // --- Create Round 2 (Personal Mode) --- + uint32 round2Id = fundingPot.createRound( + initialTimestamp + 3 days, + initialTimestamp + 4 days, + 10_000, // Large round cap + address(0), + bytes(""), + false, + ILM_PC_FundingPot_v1.AccumulationMode.Personal + ); fundingPot.setAccessCriteria( - roundId, accessType, 0, nftContract, merkleRoot, allowedAddresses + round2Id, accessId, 0, address(0), bytes32(0), new address[](0) ); fundingPot.setAccessCriteriaPrivileges( - roundId, accessCriteriaId, 1000, false, 0, 0, 0 + round2Id, accessId, r2BasePersonalCapC1, false, 0, 0, 0 ); - mockNFTContract.mint(contributor1_); + // --- Contribution by C1 to Round 1 --- + vm.startPrank(contributor1_); + vm.warp(initialTimestamp + 1 days + 1 hours); // Enter Round 1 + fundingPot.contributeToRoundFor( + contributor1_, + round1Id, + r1ContributionC1, + accessId, + new bytes32[](0) + ); + vm.stopPrank(); - (uint roundStart,,,,,,) = fundingPot.getRoundGenericParameters(roundId); - vm.warp(roundStart + 1); + // --- Verify Default Global Start Round ID --- + assertEq( + fundingPot.getGlobalAccumulationStartRoundId(), + 1, + "Default global start round ID should be 1" + ); - // Approve - vm.prank(contributor1_); - _token.approve(address(fundingPot), 1000); + // --- Attempt Contribution in Round 2 by C1 --- + vm.warp(initialTimestamp + 3 days + 1 hours); // Enter Round 2 - vm.prank(contributor1_); - fundingPot.contributeToRoundFor( - contributor1_, roundId, amount, accessCriteriaId, new bytes32[](0) + ILM_PC_FundingPot_v1.UnspentPersonalRoundCap[] memory unspentCapsC1 = + new ILM_PC_FundingPot_v1.UnspentPersonalRoundCap[](1); + unspentCapsC1[0] = ILM_PC_FundingPot_v1.UnspentPersonalRoundCap( + round1Id, + accessId, + new bytes32[](0) // Should be counted ); - assertEq(fundingPot.isRoundClosed(roundId), false); - fundingPot.closeRound(roundId); - assertEq(fundingPot.isRoundClosed(roundId), true); - } - - function testCloseRound_worksGivenRoundisAutoClosure() public { - testEditRound(); + uint r1UnusedPersonalC1 = r1PersonalCapC1 - r1ContributionC1; // 400 - uint32 roundId = fundingPot.getRoundCount(); - uint8 accessCriteriaId = 1; - uint8 accessType = uint8(ILM_PC_FundingPot_v1.AccessCriteriaType.OPEN); + // Expected C1 effective personal cap in R2 = R2_Base (300) + R1_Unused (400) = 700 + uint expectedC1EffectivePersonalCapR2 = + r2BasePersonalCapC1 + r1UnusedPersonalC1; - uint amount = 2000; + uint c1AttemptR2 = expectedC1EffectivePersonalCapR2 + 50; // Try to contribute slightly more + uint expectedC1ContributionR2 = expectedC1EffectivePersonalCapR2; // Should be clamped - ( - address nftContract, - bytes32 merkleRoot, - address[] memory allowedAddresses - ) = _helper_createAccessCriteria(accessType, roundId); + // Ensure the attempt is not clamped by the round cap (which is large) + if (expectedC1ContributionR2 > 10_000) { + // 10_000 is round cap for R2 + expectedC1ContributionR2 = 10_000; + } - fundingPot.setAccessCriteria( - roundId, accessType, 0, nftContract, merkleRoot, allowedAddresses + vm.startPrank(contributor1_); + fundingPot.contributeToRoundFor( + contributor1_, + round2Id, + c1AttemptR2, + accessId, + new bytes32[](0), + unspentCapsC1 ); + vm.stopPrank(); - fundingPot.setAccessCriteriaPrivileges( - roundId, accessCriteriaId, 2000, false, 0, 0, 0 + // --- Assertion --- + assertEq( + fundingPot.getUserContributionToRound(round2Id, contributor1_), + expectedC1ContributionR2, + "R2 C1 personal contribution incorrect (should use R1 unused)" ); - mockNFTContract.mint(contributor1_); - - (uint roundStart,,,,,,) = fundingPot.getRoundGenericParameters(roundId); - vm.warp(roundStart + 1); - - // Approve - vm.prank(contributor1_); - _token.approve(address(fundingPot), 2000); + } - vm.prank(contributor1_); - fundingPot.contributeToRoundFor( - contributor1_, roundId, amount, accessCriteriaId, new bytes32[](0) - ); + function testContributeToRoundFor_defaultGlobalStartAllowsTotalAccumulation( + ) public { + // SCENARIO: Default globalAccumulationStartRoundId = 1 allows total cap accumulation from R1 to R2 (Total mode) + // Simplified to reduce stack depth. - assertEq(fundingPot.isRoundClosed(roundId), true); - } + uint initialTimestamp = block.timestamp; + uint8 accessId = 1; // Open access - function testCloseRound_worksWithMultipleContributors() public { - testCreateRound(); - uint32 roundId = fundingPot.getRoundCount(); + // --- Round 1 Parameters & Contribution --- + uint r1BaseCap = 1000; + uint r1ContributionC1 = 600; // Leaves 400 unused total from R1 + uint r1PersonalCap = 1000; - // Set up access criteria - uint8 accessCriteriaId = 1; - uint8 accessType = uint8(ILM_PC_FundingPot_v1.AccessCriteriaType.OPEN); + // --- Round 2 Parameters --- + uint r2BaseCap = 500; - ( - address nftContract, - bytes32 merkleRoot, - address[] memory allowedAddresses - ) = _helper_createAccessCriteria(accessType, roundId); + // --- Approvals --- + vm.startPrank(contributor1_); + _token.approve(address(fundingPot), type(uint).max); + vm.stopPrank(); + // --- Create Round 1 (Total Mode) --- + uint32 round1Id = fundingPot.createRound( + initialTimestamp + 1 days, + initialTimestamp + 2 days, + r1BaseCap, + address(0), + bytes(""), + false, + ILM_PC_FundingPot_v1.AccumulationMode.Total + ); fundingPot.setAccessCriteria( - roundId, accessType, 0, nftContract, merkleRoot, allowedAddresses + round1Id, accessId, 0, address(0), bytes32(0), new address[](0) ); fundingPot.setAccessCriteriaPrivileges( - roundId, accessCriteriaId, 1000, false, 0, 0, 0 + round1Id, accessId, r1PersonalCap, false, 0, 0, 0 ); - // Warp to round start - (uint roundStart,,,,,,) = fundingPot.getRoundGenericParameters(roundId); - vm.warp(roundStart + 1); - - // Multiple contributors + // --- Contribution by C1 to Round 1 --- + vm.warp(initialTimestamp + 1 days + 1 hours); vm.startPrank(contributor1_); - _token.approve(address(fundingPot), 500); fundingPot.contributeToRoundFor( - contributor1_, roundId, 500, accessCriteriaId, new bytes32[](0) + contributor1_, + round1Id, + r1ContributionC1, + accessId, + new bytes32[](0) ); vm.stopPrank(); + uint r1UnusedTotal = r1BaseCap - r1ContributionC1; // Should be 400 - vm.startPrank(contributor2_); - _token.approve(address(fundingPot), 200); - fundingPot.contributeToRoundFor( - contributor2_, roundId, 200, accessCriteriaId, new bytes32[](0) + // --- Create Round 2 (Total Mode) --- + uint32 round2Id = fundingPot.createRound( + initialTimestamp + 3 days, + initialTimestamp + 4 days, + r2BaseCap, + address(0), + bytes(""), + false, + ILM_PC_FundingPot_v1.AccumulationMode.Total ); - vm.stopPrank(); - - vm.startPrank(contributor3_); - _token.approve(address(fundingPot), 300); - fundingPot.contributeToRoundFor( - contributor3_, roundId, 300, accessCriteriaId, new bytes32[](0) + fundingPot.setAccessCriteria( + round2Id, accessId, 0, address(0), bytes32(0), new address[](0) + ); + // Set personal cap for R2 to be at least the expected effective total cap + uint r2ExpectedEffectiveTotalCap = r2BaseCap + r1UnusedTotal; // 500 + 400 = 900 + fundingPot.setAccessCriteriaPrivileges( + round2Id, accessId, r2ExpectedEffectiveTotalCap, false, 0, 0, 0 ); - vm.stopPrank(); - - // Close the round - fundingPot.closeRound(roundId); - - // Verify round is closed - assertEq(fundingPot.isRoundClosed(roundId), true); - } - - //------------------------------------------------------------------------- - /* Test createPaymentOrdersForContributorsBatch() - ├── Given round does not exist - │ └── When user attempts to create payment orders in batch - │ └── Then it should revert with Module__LM_PC_FundingPot__RoundNotCreated - │ - ├── Given round is not closed - │ └── When user attempts to create payment orders in batch - │ └── Then it should revert with Module__LM_PC_FundingPot__RoundNotClosed - │ - ├── Given start index is greater than the number of contributors - │ └── When user attempts to create payment orders in batch - │ └── Then it should revert with Module__LM_PC_FundingPot__InvalidBatchParameters - │ - ├── Given batch size is zero - │ └── When user attempts to create payment orders in batch - │ └── Then it should revert with Module__LM_PC_FundingPot__InvalidBatchParameters - │ - ├── Given user does not have FUNDING_POT_ADMIN_ROLE - │ └── Given the round is configured with autoClosure - │ └── When user attempts to create payment orders in batch - │ └── Then it should revert with Module__CallerNotAuthorized - │ - ├── Given a closed round with autoClosure - │ └── When user attempts to create payment orders in batch - │ └── Then it should not revert and payment orders should be created - │ └── And the payment orders should have correct token amounts - │ - ├── Given a closed round with manualClosure - │ └── When funding pot admin attempts to create payment orders in batch - │ └── Then it should not revert and payment orders should be created - │ └── And the payment orders should have correct token amounts - */ + // --- Verify Default Global Start Round ID --- + assertEq( + fundingPot.getGlobalAccumulationStartRoundId(), + 1, + "Default global start round ID should be 1" + ); - function testCreatePaymentOrdersForContributorsBatch_revertsGivenRoundDoesNotExist( - ) public { - uint32 nonExistentRoundId = 999; + // --- Attempt Contribution in Round 2 by C1 --- + vm.warp(initialTimestamp + 3 days + 1 hours); - vm.expectRevert( - abi.encodeWithSelector( - ILM_PC_FundingPot_v1 - .Module__LM_PC_FundingPot__RoundNotCreated - .selector - ) + uint c1AttemptR2 = r2ExpectedEffectiveTotalCap - 100; // e.g., 900 - 100 = 800. Utilizes expanded cap. + assertTrue( + c1AttemptR2 > r2BaseCap, "C1 R2 attempt should be > R2 base cap" ); - fundingPot.createPaymentOrdersForContributorsBatch( - nonExistentRoundId, 1 + assertTrue( + c1AttemptR2 <= r2ExpectedEffectiveTotalCap, + "C1 R2 attempt should be <= R2 effective cap" ); - } - function testCreatePaymentOrdersForContributorsBatch_revertsGivenRoundIsNotClosed( - ) public { - testcontributeToRoundFor_worksGivenAllConditionsMet(); - uint32 roundId = fundingPot.getRoundCount(); + vm.startPrank(contributor1_); + fundingPot.contributeToRoundFor( + contributor1_, round2Id, c1AttemptR2, accessId, new bytes32[](0) + ); + vm.stopPrank(); - vm.expectRevert( - abi.encodeWithSelector( - ILM_PC_FundingPot_v1 - .Module__LM_PC_FundingPot__RoundNotClosed - .selector - ) + // --- Assertions --- + assertEq( + fundingPot.getUserContributionToRound(round2Id, contributor1_), + c1AttemptR2, + "R2 C1 contribution incorrect" + ); + assertEq( + fundingPot.getTotalRoundContribution(round2Id), + c1AttemptR2, + "R2 Total contributions after C1 incorrect" ); - fundingPot.createPaymentOrdersForContributorsBatch(roundId, 1); - } - function testCreatePaymentOrdersForContributorsBatch_revertsGivenBatchSizeIsGreaterThanContributorCount( - ) public { - testCloseRound_worksWithMultipleContributors(); - uint32 roundId = fundingPot.getRoundCount(); + // Verify that the total contributions possible is indeed the effective cap + uint remainingToFill = r2ExpectedEffectiveTotalCap - c1AttemptR2; + if (remainingToFill > 0) { + vm.startPrank(contributor1_); + fundingPot.contributeToRoundFor( + contributor1_, + round2Id, + remainingToFill, + accessId, + new bytes32[](0) + ); + vm.stopPrank(); + } - vm.expectRevert( - abi.encodeWithSelector( - ILM_PC_FundingPot_v1 - .Module__LM_PC_FundingPot__InvalidBatchParameters - .selector - ) + assertEq( + fundingPot.getTotalRoundContribution(round2Id), + r2ExpectedEffectiveTotalCap, + "R2 final total contributions should match effective total cap" ); - fundingPot.createPaymentOrdersForContributorsBatch(roundId, 999); } - function testCreatePaymentOrdersForContributorsBatch_revertsGivenBatchSizeIsZero( - ) public { - testCloseRound_worksWithMultipleContributors(); - uint32 roundId = fundingPot.getRoundCount(); + function testContributeToRoundFor_disabledModeIgnoresAccumulation() + public + { + // SCENARIO: AccumulationMode.Disabled on a target round (R2) prevents any accumulation + // from a previous round (R1), even if globalAccumulationStartRoundId would allow it. - vm.expectRevert( - abi.encodeWithSelector( - ILM_PC_FundingPot_v1 - .Module__LM_PC_FundingPot__InvalidBatchParameters - .selector - ) - ); - fundingPot.createPaymentOrdersForContributorsBatch(roundId, 0); - } + uint initialTimestamp = block.timestamp; + uint8 accessId = 1; // Open access - function testCreatePaymentOrdersForContributorsBatch_revertsGivenUserDoesNotHaveFundingPotAdminRole( - ) public { - testCloseRound_worksWithMultipleContributors(); - uint32 roundId = fundingPot.getRoundCount(); + // --- Round 1 Parameters --- + uint r1PersonalCapC1 = 500; + uint r1ContributionC1 = 100; + uint r1BaseCap = 1000; + + // --- Round 2 Parameters (Disabled Mode) --- + uint r2BasePersonalCapC1 = 50; + uint r2BaseCap = 200; + // --- Approvals --- vm.startPrank(contributor1_); - bytes32 roleId = _authorizer.generateRoleId( - address(fundingPot), fundingPot.FUNDING_POT_ADMIN_ROLE() + _token.approve(address(fundingPot), type(uint).max); + vm.stopPrank(); + + // --- Create Round 1 (Personal Mode to generate unused personal capacity) --- + uint32 round1Id = fundingPot.createRound( + initialTimestamp + 1 days, + initialTimestamp + 2 days, + r1BaseCap, + address(0), + bytes(""), + false, + ILM_PC_FundingPot_v1.AccumulationMode.Personal ); - vm.expectRevert( - abi.encodeWithSelector( - IModule_v1.Module__CallerNotAuthorized.selector, - roleId, - contributor1_ - ) + fundingPot.setAccessCriteria( + round1Id, accessId, 0, address(0), bytes32(0), new address[](0) + ); + fundingPot.setAccessCriteriaPrivileges( + round1Id, accessId, r1PersonalCapC1, false, 0, 0, 0 + ); + + // --- Contribution by C1 to Round 1 --- + vm.warp(initialTimestamp + 1 days + 1 hours); + vm.startPrank(contributor1_); + fundingPot.contributeToRoundFor( + contributor1_, + round1Id, + r1ContributionC1, + accessId, + new bytes32[](0) ); - fundingPot.createPaymentOrdersForContributorsBatch(roundId, 1); vm.stopPrank(); - } - function testCreatePaymentOrdersForContributorsBatch_worksGivenRoundIsAutoClosure( - ) public { - testCloseRound_worksGivenRoundisAutoClosure(); - uint32 roundId = fundingPot.getRoundCount(); + // --- Create Round 2 (Disabled Mode) --- + uint32 round2Id = fundingPot.createRound( + initialTimestamp + 3 days, + initialTimestamp + 4 days, + r2BaseCap, + address(0), + bytes(""), + false, + ILM_PC_FundingPot_v1.AccumulationMode.Disabled + ); + fundingPot.setAccessCriteria( + round2Id, accessId, 0, address(0), bytes32(0), new address[](0) + ); + fundingPot.setAccessCriteriaPrivileges( + round2Id, accessId, r2BasePersonalCapC1, false, 0, 0, 0 + ); - fundingPot.createPaymentOrdersForContributorsBatch(roundId, 1); - assertEq(fundingPot.paymentOrders().length, 1); - } + // --- Set Global Start Round ID to allow R1 (to show it's ignored by R2's Disabled mode) --- + fundingPot.setGlobalAccumulationStart(1); + assertEq( + fundingPot.getGlobalAccumulationStartRoundId(), + 1, + "Global start round ID should be 1" + ); - function testCreatePaymentOrdersForContributorsBatch_worksGivenRoundIsManualClosure( - ) public { - testCloseRound_worksWithMultipleContributors(); - uint32 roundId = fundingPot.getRoundCount(); + // --- Attempt Contribution in Round 2 by C1 --- + vm.warp(initialTimestamp + 3 days + 1 hours); - fundingPot.createPaymentOrdersForContributorsBatch(roundId, 3); - assertEq(fundingPot.paymentOrders().length, 3); - } - // ------------------------------------------------------------------------- + uint c1AttemptR2 = r2BasePersonalCapC1 + 100; - // Internal Functions - function testFuzz_validateAndAdjustCapsWithUnspentCap( - uint32 roundId_, - uint amount_, - uint8 accessCriteriaId_, - bool canOverrideContributionSpan_, - uint unspentPersonalCap_ - ) external { - vm.assume(roundId_ > 0 && roundId_ >= fundingPot.getRoundCount()); - vm.assume(amount_ <= 1000); - vm.assume(accessCriteriaId_ <= 4); - vm.assume(unspentPersonalCap_ >= 0); + ILM_PC_FundingPot_v1.UnspentPersonalRoundCap[] memory unspentCapsC1 = + new ILM_PC_FundingPot_v1.UnspentPersonalRoundCap[](1); + unspentCapsC1[0] = ILM_PC_FundingPot_v1.UnspentPersonalRoundCap( + round1Id, accessId, new bytes32[](0) + ); - try fundingPot.exposed_validateAndAdjustCapsWithUnspentCap( + vm.startPrank(contributor1_); + fundingPot.contributeToRoundFor( contributor1_, - roundId_, - amount_, - accessCriteriaId_, - canOverrideContributionSpan_, - unspentPersonalCap_ - ) returns (uint adjustedAmount) { - assertLe( - adjustedAmount, amount_, "Adjusted amount should be <= amount_" - ); - assertGe(adjustedAmount, 0, "Adjusted amount should be >= 0"); - } catch (bytes memory reason) { - bytes32 roundCapReachedSelector = keccak256( - abi.encodeWithSignature( - "Module__LM_PC_FundingPot__RoundCapReached()" - ) - ); - bytes32 personalCapReachedSelector = keccak256( - abi.encodeWithSignature( - "Module__LM_PC_FundingPot__PersonalCapReached()" - ) - ); + round2Id, + c1AttemptR2, + accessId, + new bytes32[](0), + unspentCapsC1 + ); + vm.stopPrank(); - if (keccak256(reason) == roundCapReachedSelector) { - assertTrue( - !canOverrideContributionSpan_, - "Should not revert RoundCapReached when canOverrideContributionSpan is true" - ); - } else if (keccak256(reason) == personalCapReachedSelector) { - assertTrue(true, "Personal cap reached as expected"); - } else { - assertTrue(false, "Unexpected revert reason"); - } - } + // --- Assertions --- + assertEq( + fundingPot.getUserContributionToRound(round2Id, contributor1_), + r2BasePersonalCapC1, + "R2 C1 personal contribution should be clamped by R2's base personal cap (Disabled mode)" + ); + assertEq( + fundingPot.getTotalRoundContribution(round2Id), + r2BasePersonalCapC1, + "R2 Total contributions should not be expanded by R1 (Disabled mode)" + ); + assertTrue( + fundingPot.getTotalRoundContribution(round2Id) <= r2BaseCap, + "R2 Total contributions exceeded R2's original base cap (Disabled mode)" + ); } - function testFuzz_ValidTimes(uint start, uint cliff, uint end) public { - vm.assume(cliff <= type(uint).max - start); - - bool isValid = fundingPot.exposed_validTimes(start, cliff, end); + function testContributeToRoundFor_noAccumulationWhenGlobalStartEqualsTargetRound( + ) public { + // SCENARIO: If globalAccumulationStartRoundId is set to the target round's ID (R2), + // no accumulation from any previous round (R1) occurs for R2, even if R2's mode would allow it. - assertEq(isValid, start + cliff <= end); + uint initialTimestamp = block.timestamp; + uint8 accessId = 1; // Open access - if (start > end) { - assertFalse(isValid); - } + // --- Round 1 Parameters --- + uint r1PersonalCapC1 = 500; + uint r1ContributionC1 = 100; + uint r1BaseCap = 1000; - if (start == end) { - assertEq(isValid, cliff == 0); - } - } + // --- Round 2 Parameters (Mode that would normally allow accumulation, e.g., Personal) --- + uint r2BasePersonalCapC1 = 50; + uint r2BaseCap = 200; - // ------------------------------------------------------------------------- - // Test: _calculateUnusedCapacityFromPreviousRounds + // --- Approvals --- + vm.startPrank(contributor1_); + _token.approve(address(fundingPot), type(uint).max); + vm.stopPrank(); - function test_calculateUnusedCapacityFromPreviousRounds() public { - // round 1 (no accumulation) - fundingPot.createRound( - _defaultRoundParams.roundStart, - _defaultRoundParams.roundEnd, - _defaultRoundParams.roundCap, - _defaultRoundParams.hookContract, - _defaultRoundParams.hookFunction, - _defaultRoundParams.autoClosure, - ILM_PC_FundingPot_v1.AccumulationMode.Disabled + // --- Create Round 1 (Personal Mode to generate unused personal capacity) --- + uint32 round1Id = fundingPot.createRound( + initialTimestamp + 1 days, + initialTimestamp + 2 days, + r1BaseCap, + address(0), + bytes(""), + false, + ILM_PC_FundingPot_v1.AccumulationMode.Personal ); - - // round 2 (with accumulation) - fundingPot.createRound( - _defaultRoundParams.roundStart + 300, - _defaultRoundParams.roundEnd + 400, - _defaultRoundParams.roundCap, - _defaultRoundParams.hookContract, - _defaultRoundParams.hookFunction, - _defaultRoundParams.autoClosure, - ILM_PC_FundingPot_v1.AccumulationMode.All // globalAccumulativeCaps on + fundingPot.setAccessCriteria( + round1Id, accessId, 0, address(0), bytes32(0), new address[](0) ); - - // round 3 - fundingPot.createRound( - _defaultRoundParams.roundStart + 500, - _defaultRoundParams.roundEnd + 600, - _defaultRoundParams.roundCap, - _defaultRoundParams.hookContract, - _defaultRoundParams.hookFunction, - _defaultRoundParams.autoClosure, - ILM_PC_FundingPot_v1.AccumulationMode.All + fundingPot.setAccessCriteriaPrivileges( + round1Id, accessId, r1PersonalCapC1, false, 0, 0, 0 ); - // Calculate unused capacity - uint actualUnusedCapacity = - fundingPot.exposed_calculateUnusedCapacityFromPreviousRounds(3); - assertEq(actualUnusedCapacity, 1000); - } - - // ------------------------------------------------------------------------- - // Test: _contributeToRoundFor() - - function testFuzz_contributeToRoundFor_revertsGivenInvalidAccessCriteria( - uint8 accessCriteriaEnum - ) public { - vm.assume(accessCriteriaEnum > 4); - - testCreateRound(); - uint32 roundId = fundingPot.getRoundCount(); - (uint roundStart,,,,,,) = fundingPot.getRoundGenericParameters(roundId); - vm.warp(roundStart + 1); - - vm.prank(contributor1_); - _token.approve(address(fundingPot), 1000); - - vm.expectRevert( - abi.encodeWithSelector( - ILM_PC_FundingPot_v1 - .Module__LM_PC_FundingPot__InvalidAccessCriteriaId - .selector - ) - ); - - fundingPot.exposed_contributeToRoundFor( + // --- Contribution by C1 to Round 1 --- + vm.warp(initialTimestamp + 1 days + 1 hours); + vm.startPrank(contributor1_); + fundingPot.contributeToRoundFor( contributor1_, - roundId, - 1000, - accessCriteriaEnum, - new bytes32[](0), - 0 + round1Id, + r1ContributionC1, + accessId, + new bytes32[](0) ); - } - // ------------------------------------------------------------------------- - // Test: _checkRoundClosureConditions - - function test_checkRoundClosureConditions_whenCapReached() public { - RoundParams memory params = _defaultRoundParams; - params.roundStart = block.timestamp + 1 days; - params.roundEnd = block.timestamp + 2 days; - params.roundCap = 1000; + vm.stopPrank(); - uint32 roundId = fundingPot.createRound( - params.roundStart, - params.roundEnd, - params.roundCap, - params.hookContract, - params.hookFunction, - params.autoClosure, - params.accumulationMode + // --- Create Round 2 (Personal Mode - would normally allow accumulation from R1) --- + uint32 round2Id = fundingPot.createRound( + initialTimestamp + 3 days, + initialTimestamp + 4 days, + r2BaseCap, + address(0), + bytes(""), + false, + ILM_PC_FundingPot_v1.AccumulationMode.Personal ); - - // Set access criteria and privileges - uint8 accessCriteriaId = 1; - uint8 accessType = uint8(ILM_PC_FundingPot_v1.AccessCriteriaType.OPEN); - - ( - address nftContract, - bytes32 merkleRoot, - address[] memory allowedAddresses - ) = _helper_createAccessCriteria(accessType, roundId); fundingPot.setAccessCriteria( - roundId, accessType, 0, nftContract, merkleRoot, allowedAddresses + round2Id, accessId, 0, address(0), bytes32(0), new address[](0) ); fundingPot.setAccessCriteriaPrivileges( - roundId, accessCriteriaId, 1000, false, 0, 0, 0 + round2Id, accessId, r2BasePersonalCapC1, false, 0, 0, 0 + ); + + // --- Set Global Start Round ID to be Round 2's ID --- + fundingPot.setGlobalAccumulationStart(round2Id); + assertEq( + fundingPot.getGlobalAccumulationStartRoundId(), + round2Id, + "Global start round ID not set to R2 ID" + ); + + // --- Attempt Contribution in Round 2 by C1 --- + vm.warp(initialTimestamp + 3 days + 1 hours); + + uint c1AttemptR2 = r2BasePersonalCapC1 + 100; + + ILM_PC_FundingPot_v1.UnspentPersonalRoundCap[] memory unspentCapsC1 = + new ILM_PC_FundingPot_v1.UnspentPersonalRoundCap[](1); + unspentCapsC1[0] = ILM_PC_FundingPot_v1.UnspentPersonalRoundCap( + round1Id, accessId, new bytes32[](0) ); - vm.warp(params.roundStart + 1); vm.startPrank(contributor1_); - _token.approve(address(fundingPot), params.roundCap); fundingPot.contributeToRoundFor( contributor1_, - roundId, - params.roundCap, - accessCriteriaId, - new bytes32[](0) + round2Id, + c1AttemptR2, + accessId, + new bytes32[](0), + unspentCapsC1 ); vm.stopPrank(); - assertTrue(fundingPot.exposed_checkRoundClosureConditions(roundId)); + // --- Assertions --- + assertEq( + fundingPot.getUserContributionToRound(round2Id, contributor1_), + r2BasePersonalCapC1, + "R2 C1 personal contribution should be clamped by R2's base personal cap (global start = R2)" + ); + assertEq( + fundingPot.getTotalRoundContribution(round2Id), + r2BasePersonalCapC1, + "R2 Total contributions should not be expanded by R1 (global start = R2)" + ); + assertTrue( + fundingPot.getTotalRoundContribution(round2Id) <= r2BaseCap, + "R2 Total contributions exceeded R2's original base cap (global start = R2)" + ); } - function test_checkRoundClosureConditions_whenEndTimeReached() public { - RoundParams memory params = _defaultRoundParams; - params.roundStart = block.timestamp + 1 days; - params.roundEnd = block.timestamp + 2 days; - params.roundCap = 1000; + function testContributeToRoundFor_defaultGlobalStartAllowsPersonalAccumulationFromMultipleRounds( + ) public { + // SCENARIO: globalAccumulationStartRoundId = 1 allows personal cap accumulation from R1 AND R2 + // for contributions to R3, when all rounds are in Personal mode. + // 1. Setup: R1, R2, R3 in Personal mode. C1 makes partial contributions in R1 & R2. + // 2. Action: Verify globalAccumulationStartRoundId = 1. C1 contributes to R3. + // 3. Verification: C1's effective personal cap in R3 includes unused from R1 and R2. - uint32 roundId = fundingPot.createRound( - params.roundStart, - params.roundEnd, - params.roundCap, - params.hookContract, - params.hookFunction, - params.autoClosure, - params.accumulationMode - ); + uint initialTimestamp = block.timestamp; - // Move time past end time - vm.warp(params.roundEnd + 1); - assertTrue(fundingPot.exposed_checkRoundClosureConditions(roundId)); - } + // --- Round Parameters, Personal Caps, and Contributions for contributor1_ --- + uint r1PersonalCapC1 = 500; + uint r1ContributionC1 = 200; - function test_checkRoundClosureConditions_whenNeitherConditionMet() - public - { - RoundParams memory params = _defaultRoundParams; - params.roundStart = block.timestamp + 1 days; - params.roundEnd = block.timestamp + 2 days; - params.roundCap = 1000; + uint r2PersonalCapC1 = 600; + uint r2ContributionC1 = 250; - uint32 roundId = fundingPot.createRound( - params.roundStart, - params.roundEnd, - params.roundCap, - params.hookContract, - params.hookFunction, - params.autoClosure, - params.accumulationMode - ); + uint r3BasePersonalCapC1 = 300; - assertFalse(fundingPot.exposed_checkRoundClosureConditions(roundId)); - } + uint largeRoundCap = 1_000_000; - function test_checkRoundClosureConditions_withNoEndTime() public { - RoundParams memory params = _defaultRoundParams; - params.roundStart = block.timestamp + 1 days; - params.roundEnd = 0; // No end time - params.roundCap = 1000; + // --- Approvals --- + vm.startPrank(contributor1_); + _token.approve(address(fundingPot), type(uint).max); + vm.stopPrank(); - uint32 roundId = fundingPot.createRound( - params.roundStart, - params.roundEnd, - params.roundCap, - params.hookContract, - params.hookFunction, - params.autoClosure, - params.accumulationMode + // --- Create Round 1 (Personal Mode) --- + uint32 round1Id = fundingPot.createRound( + initialTimestamp + 1 days, + initialTimestamp + 2 days, + largeRoundCap, + address(0), + bytes(""), + false, + ILM_PC_FundingPot_v1.AccumulationMode.Personal ); - - // Set access criteria and privileges - uint8 accessCriteriaId = 1; - uint8 accessType = uint8(ILM_PC_FundingPot_v1.AccessCriteriaType.OPEN); - - ( - address nftContract, - bytes32 merkleRoot, - address[] memory allowedAddresses - ) = _helper_createAccessCriteria(accessType, roundId); fundingPot.setAccessCriteria( - roundId, accessType, 0, nftContract, merkleRoot, allowedAddresses + round1Id, 1, 0, address(0), bytes32(0), new address[](0) ); fundingPot.setAccessCriteriaPrivileges( - roundId, - accessCriteriaId, - 1000, - false, - 0, // no start - 0, // no cliff - 0 // no end + round1Id, 1, r1PersonalCapC1, false, 0, 0, 0 ); - // Should be false initially - assertFalse(fundingPot.exposed_checkRoundClosureConditions(roundId)); - - // Should be true when cap is reached - vm.warp(params.roundStart + 1); + // --- Contribution by C1 to Round 1 --- + vm.warp(initialTimestamp + 1 days + 1 hours); vm.startPrank(contributor1_); - _token.approve(address(fundingPot), params.roundCap); fundingPot.contributeToRoundFor( - contributor1_, - roundId, - params.roundCap, - accessCriteriaId, - new bytes32[](0) + contributor1_, round1Id, r1ContributionC1, 1, new bytes32[](0) ); vm.stopPrank(); + assertEq( + fundingPot.getUserContributionToRound(round1Id, contributor1_), + r1ContributionC1 + ); - assertTrue(fundingPot.exposed_checkRoundClosureConditions(roundId)); - } - - function test_checkRoundClosureConditions_withNoCap() public { - RoundParams memory params = _defaultRoundParams; - params.roundStart = block.timestamp + 1 days; - params.roundEnd = block.timestamp + 2 days; - params.roundCap = 0; // No cap - - uint32 roundId = fundingPot.createRound( - params.roundStart, - params.roundEnd, - params.roundCap, - params.hookContract, - params.hookFunction, - params.autoClosure, - params.accumulationMode + // --- Create Round 2 (Personal Mode) --- + uint32 round2Id = fundingPot.createRound( + initialTimestamp + 3 days, + initialTimestamp + 4 days, + largeRoundCap, + address(0), + bytes(""), + false, + ILM_PC_FundingPot_v1.AccumulationMode.Personal ); - - // Should be false before end time - assertFalse( - fundingPot.exposed_checkRoundClosureConditions(uint32(roundId)) + fundingPot.setAccessCriteria( + round2Id, 1, 0, address(0), bytes32(0), new address[](0) ); - - // Should be true after end time - vm.warp(params.roundEnd + 1); - assertTrue( - fundingPot.exposed_checkRoundClosureConditions(uint32(roundId)) + fundingPot.setAccessCriteriaPrivileges( + round2Id, 1, r2PersonalCapC1, false, 0, 0, 0 ); - } - - function test_closeRound_worksGivenCapReached() public { - testCreateRound(); - uint32 roundId = fundingPot.getRoundCount(); - uint8 accessCriteriaId = 1; - uint8 accessType = uint8(ILM_PC_FundingPot_v1.AccessCriteriaType.OPEN); - - uint amount = 1000; - - ( - address nftContract, - bytes32 merkleRoot, - address[] memory allowedAddresses - ) = _helper_createAccessCriteria(accessType, roundId); + // --- Contribution by C1 to Round 2 --- + vm.warp(initialTimestamp + 3 days + 1 hours); + vm.startPrank(contributor1_); + fundingPot.contributeToRoundFor( + contributor1_, round2Id, r2ContributionC1, 1, new bytes32[](0) + ); + vm.stopPrank(); + assertEq( + fundingPot.getUserContributionToRound(round2Id, contributor1_), + r2ContributionC1 + ); + // --- Create Round 3 (Personal Mode) --- + uint32 round3Id = fundingPot.createRound( + initialTimestamp + 5 days, + initialTimestamp + 6 days, + largeRoundCap, + address(0), + bytes(""), + false, + ILM_PC_FundingPot_v1.AccumulationMode.Personal + ); fundingPot.setAccessCriteria( - roundId, accessType, 0, nftContract, merkleRoot, allowedAddresses + round3Id, 1, 0, address(0), bytes32(0), new address[](0) ); fundingPot.setAccessCriteriaPrivileges( - roundId, accessCriteriaId, 1000, false, 0, 0, 0 + round3Id, 1, r3BasePersonalCapC1, false, 0, 0, 0 ); - mockNFTContract.mint(contributor1_); + // --- Verify Global Start Round ID --- + assertEq( + fundingPot.getGlobalAccumulationStartRoundId(), + 1, + "Default global start round ID should be 1" + ); - (uint roundStart,,,,,,) = fundingPot.getRoundGenericParameters(roundId); - vm.warp(roundStart + 1); + // --- Attempt Contribution in Round 3 by C1 --- + vm.warp(initialTimestamp + 5 days + 1 hours); - // Approve - vm.prank(contributor1_); - _token.approve(address(fundingPot), 1000); + ILM_PC_FundingPot_v1.UnspentPersonalRoundCap[] memory unspentCapsC1 = + new ILM_PC_FundingPot_v1.UnspentPersonalRoundCap[](2); + unspentCapsC1[0] = ILM_PC_FundingPot_v1.UnspentPersonalRoundCap( + round1Id, 1, new bytes32[](0) + ); + unspentCapsC1[1] = ILM_PC_FundingPot_v1.UnspentPersonalRoundCap( + round2Id, 1, new bytes32[](0) + ); - vm.prank(contributor1_); + uint expectedR3PersonalCapC1 = r3BasePersonalCapC1 + + (r1PersonalCapC1 - r1ContributionC1) + + (r2PersonalCapC1 - r2ContributionC1); + + uint c1AttemptR3 = expectedR3PersonalCapC1; + + vm.startPrank(contributor1_); fundingPot.contributeToRoundFor( - contributor1_, roundId, amount, accessCriteriaId, new bytes32[](0) + contributor1_, + round3Id, + c1AttemptR3, + 1, + new bytes32[](0), + unspentCapsC1 ); + vm.stopPrank(); - assertTrue( - fundingPot.exposed_checkRoundClosureConditions(uint32(roundId)) + // --- Assertions --- + assertEq( + fundingPot.getUserContributionToRound(round3Id, contributor1_), + expectedR3PersonalCapC1, + "R3 C1 personal contribution incorrect (should use R1 & R2 unused)" ); - - uint startIndex = 0; - uint batchSize = 1; - fundingPot.exposed_closeRound(uint32(roundId)); - fundingPot.exposed_buyBondingCurveToken(uint32(roundId)); - fundingPot.exposed_createPaymentOrdersForContributors( - uint32(roundId), startIndex, batchSize + assertEq( + fundingPot.getTotalRoundContribution(round3Id), + expectedR3PersonalCapC1, + "R3 total contributions incorrect after C1" ); - assertTrue(fundingPot.isRoundClosed(roundId)); - } - - // ------------------------------------------------------------------------- - // Test: _buyBondingCurveToken - function test_buyBondingCurveToken_revertsGivenNoContributions() public { - testCreateRound(); - uint32 roundId = fundingPot.getRoundCount(); - (uint roundStart,,,,,,) = fundingPot.getRoundGenericParameters(roundId); - vm.warp(roundStart + 1); - + vm.startPrank(contributor1_); + _token.approve(address(fundingPot), 1); vm.expectRevert( - ILM_PC_FundingPot_v1 - .Module__LM_PC_FundingPot__NoContributions - .selector + abi.encodeWithSelector( + ILM_PC_FundingPot_v1 + .Module__LM_PC_FundingPot__PersonalCapReached + .selector + ) ); - fundingPot.exposed_buyBondingCurveToken(roundId); + fundingPot.contributeToRoundFor( + contributor1_, round3Id, 1, 1, new bytes32[](0), unspentCapsC1 + ); + vm.stopPrank(); } - // ------------------------------------------------------------------------- - // Helper Functions - // @notice Creates edit round parameters with customizable values - function _helper_createEditRoundParams( - uint roundStart_, - uint roundEnd_, - uint roundCap_, - address hookContract_, - bytes memory hookFunction_, - bool autoClosure_, - ILM_PC_FundingPot_v1.AccumulationMode accumulationMode_ - ) internal pure returns (RoundParams memory) { - return RoundParams({ - roundStart: roundStart_, - roundEnd: roundEnd_, - roundCap: roundCap_, - hookContract: hookContract_, - hookFunction: hookFunction_, - autoClosure: autoClosure_, - accumulationMode: accumulationMode_ - }); - } + function testContributeToRoundFor_defaultGlobalStartAllowsTotalAccumulationFromMultipleRounds( + ) public { + // SCENARIO: globalAccumulationStartRoundId = 1 allows accumulation from Round 1 AND Round 2 for Total mode + // 1. Setup: Round 1, Round 2, Round 3. Partial total contributions in R1 & R2. All in Total mode. + // 2. Action: setGlobalAccumulationStart(1) (or verify default). + // 3. Verification: For contributions to R3 (Total mode), unused total from R1 AND R2 rolls over, expanding R3's effective cap. - function _helper_generateMerkleTreeForTwoLeaves( - address contributorA, - address contributorB, - uint32 roundId - ) - internal - pure - returns ( - bytes32 root, - bytes32 leafA, - bytes32 leafB, - bytes32[] memory proofA, - bytes32[] memory proofB - ) - { - leafA = keccak256(abi.encodePacked(contributorA, roundId)); - leafB = keccak256(abi.encodePacked(contributorB, roundId)); + uint initialTimestamp = block.timestamp; - proofA = new bytes32[](1); - proofB = new bytes32[](1); + // --- Round Parameters & Contributions --- + uint r1BaseCap = 1000; + uint r1ContributionC1 = 400; - // Ensure consistent ordering for root calculation - if (leafA < leafB) { - root = keccak256(abi.encodePacked(leafA, leafB)); - proofA[0] = leafB; // Proof for A is B - proofB[0] = leafA; // Proof for B is A - } else { - root = keccak256(abi.encodePacked(leafB, leafA)); - proofA[0] = leafB; // Proof for A is still B - proofB[0] = leafA; // Proof for B is still A - } - } + uint r2BaseCap = 1200; + uint r2ContributionC2 = 700; - function _helper_createAccessCriteria( - uint8 accessCriteriaEnum, - uint32 roundId - ) - internal - view - returns ( - address nftContract_, - bytes32 merkleRoot_, - address[] memory allowedAddresses_ - ) - { - { - if ( - accessCriteriaEnum - == uint8(ILM_PC_FundingPot_v1.AccessCriteriaType.OPEN) - ) { - nftContract_ = address(0x0); - merkleRoot_ = bytes32(uint(0x0)); - allowedAddresses_ = new address[](0); - } else if ( - accessCriteriaEnum - == uint8(ILM_PC_FundingPot_v1.AccessCriteriaType.NFT) - ) { - address nftContract = address(mockNFTContract); + uint r3BaseCap = 300; - nftContract_ = nftContract; - merkleRoot_ = bytes32(uint(0x0)); - allowedAddresses_ = new address[](0); - } else if ( - accessCriteriaEnum - == uint8(ILM_PC_FundingPot_v1.AccessCriteriaType.MERKLE) - ) { - (bytes32 merkleRoot,,,,) = - _helper_generateMerkleTreeForTwoLeaves( - contributor1_, contributor2_, roundId - ); + // --- Approvals --- + vm.startPrank(contributor1_); + _token.approve(address(fundingPot), type(uint).max); + vm.stopPrank(); - nftContract_ = address(0x0); - merkleRoot_ = merkleRoot; - allowedAddresses_ = new address[](0); - } else if ( - accessCriteriaEnum - == uint8(ILM_PC_FundingPot_v1.AccessCriteriaType.LIST) - ) { - address[] memory allowedAddresses = new address[](4); - allowedAddresses[0] = address(this); - allowedAddresses[1] = address(0x2); - allowedAddresses[2] = address(0x3); - allowedAddresses[3] = contributor2_; - nftContract_ = address(0x0); - merkleRoot_ = bytes32(uint(0x0)); - allowedAddresses_ = allowedAddresses; - } - } - } - - // Helper function to set up a round with access criteria - function _helper_setupRoundWithAccessCriteria(uint8 accessCriteriaEnum) - internal - { - uint32 roundId = fundingPot.createRound( - _defaultRoundParams.roundStart, - _defaultRoundParams.roundEnd, - _defaultRoundParams.roundCap, - _defaultRoundParams.hookContract, - _defaultRoundParams.hookFunction, - _defaultRoundParams.autoClosure, - _defaultRoundParams.accumulationMode - ); + vm.startPrank(contributor2_); + _token.approve(address(fundingPot), type(uint).max); + vm.stopPrank(); - ( - address nftContract, - bytes32 merkleRoot, - address[] memory allowedAddresses - ) = _helper_createAccessCriteria(accessCriteriaEnum, roundId); + vm.startPrank(contributor3_); + _token.approve(address(fundingPot), type(uint).max); + vm.stopPrank(); + // --- Create Round 1 (Total Mode) --- + uint32 round1Id = fundingPot.createRound( + initialTimestamp + 1 days, + initialTimestamp + 2 days, + r1BaseCap, + address(0), + bytes(""), + false, + ILM_PC_FundingPot_v1.AccumulationMode.Total + ); fundingPot.setAccessCriteria( - roundId, - accessCriteriaEnum, - 0, - nftContract, - merkleRoot, - allowedAddresses + round1Id, 1, 0, address(0), bytes32(0), new address[](0) ); - } - - function testContribute_PersonalMode_AccumulatesPersonalOnly() public { - // 1. Create the first round with AccumulationMode.Personal - _defaultRoundParams.accumulationMode = - ILM_PC_FundingPot_v1.AccumulationMode.Personal; - - fundingPot.createRound( - _defaultRoundParams.roundStart, - _defaultRoundParams.roundEnd, - 1000, // Round cap of 1000 - _defaultRoundParams.hookContract, - _defaultRoundParams.hookFunction, - _defaultRoundParams.autoClosure, - _defaultRoundParams.accumulationMode + fundingPot.setAccessCriteriaPrivileges( + round1Id, 1, r1BaseCap, false, 0, 0, 0 ); - uint32 round1Id = fundingPot.getRoundCount(); - // Set up access criteria for round 1 - uint8 accessCriteriaId = 1; // Open access - ( - address nftContract, - bytes32 merkleRoot, - address[] memory allowedAddresses - ) = _helper_createAccessCriteria( - uint8(ILM_PC_FundingPot_v1.AccessCriteriaType.OPEN), round1Id + // --- Contribution by C1 to Round 1 --- + vm.warp(initialTimestamp + 1 days + 1 hours); + vm.startPrank(contributor1_); + fundingPot.contributeToRoundFor( + contributor1_, round1Id, r1ContributionC1, 1, new bytes32[](0) + ); + vm.stopPrank(); + assertEq( + fundingPot.getTotalRoundContribution(round1Id), r1ContributionC1 ); + // --- Create Round 2 (Total Mode) --- + uint32 round2Id = fundingPot.createRound( + initialTimestamp + 3 days, + initialTimestamp + 4 days, + r2BaseCap, + address(0), + bytes(""), + false, + ILM_PC_FundingPot_v1.AccumulationMode.Total + ); fundingPot.setAccessCriteria( - round1Id, - uint8(ILM_PC_FundingPot_v1.AccessCriteriaType.OPEN), // accessCriteriaType - 0, // accessCriteriaId (0 for new) - nftContract, - merkleRoot, - allowedAddresses + round2Id, 1, 0, address(0), bytes32(0), new address[](0) ); - - // Set a personal cap of 500 for round 1 fundingPot.setAccessCriteriaPrivileges( - round1Id, accessCriteriaId, 500, false, 0, 0, 0 + round2Id, 1, r2BaseCap, false, 0, 0, 0 ); - // 2. Create the second round, also with AccumulationMode.Personal - // Use different start and end times to avoid overlap - RoundParams memory params = _helper_createEditRoundParams( - _defaultRoundParams.roundStart + 3 days, - _defaultRoundParams.roundEnd + 3 days, - 500, // Round cap of 500 - _defaultRoundParams.hookContract, - _defaultRoundParams.hookFunction, - _defaultRoundParams.autoClosure, - ILM_PC_FundingPot_v1.AccumulationMode.Personal + // --- Contribution by C2 to Round 2 --- + vm.warp(initialTimestamp + 3 days + 1 hours); + vm.startPrank(contributor2_); + fundingPot.contributeToRoundFor( + contributor2_, round2Id, r2ContributionC2, 1, new bytes32[](0) ); - - fundingPot.createRound( - params.roundStart, - params.roundEnd, - params.roundCap, - params.hookContract, - params.hookFunction, - params.autoClosure, - params.accumulationMode + vm.stopPrank(); + assertEq( + fundingPot.getTotalRoundContribution(round2Id), r2ContributionC2 ); - uint32 round2Id = fundingPot.getRoundCount(); - // Set up access criteria for round 2 - fundingPot.setAccessCriteria( - round2Id, - uint8(ILM_PC_FundingPot_v1.AccessCriteriaType.OPEN), // accessCriteriaType - 0, // accessCriteriaId (0 for new) - nftContract, - merkleRoot, - allowedAddresses + // --- Create Round 3 (Total Mode) --- + uint r3ExpectedEffectiveCap = r3BaseCap + (r1BaseCap - r1ContributionC1) + + (r2BaseCap - r2ContributionC2); + uint32 round3Id = fundingPot.createRound( + initialTimestamp + 5 days, + initialTimestamp + 6 days, + r3BaseCap, + address(0), + bytes(""), + false, + ILM_PC_FundingPot_v1.AccumulationMode.Total ); + (address nftR3, bytes32 merkleR3, address[] memory allowedR3) = + _helper_createAccessCriteria(1, round3Id); - // Set a personal cap of 400 for round 2 + // TODO + fundingPot.setAccessCriteria(round3Id, 1, 0, nftR3, merkleR3, allowedR3); fundingPot.setAccessCriteriaPrivileges( - round2Id, accessCriteriaId, 400, false, 0, 0, 0 - ); - - // First round contribution: user contributes 200 out of their 500 personal cap - vm.warp(_defaultRoundParams.roundStart + 1); - - vm.startPrank(contributor1_); - _token.approve(address(fundingPot), 1000); - fundingPot.contributeToRoundFor( - contributor1_, round1Id, 200, accessCriteriaId, new bytes32[](0) + round3Id, 1, r3ExpectedEffectiveCap, false, 0, 0, 0 ); - vm.stopPrank(); - // Verify contribution to round 1 assertEq( - fundingPot.getUserContributionToRound(round1Id, contributor1_), 200 + fundingPot.getGlobalAccumulationStartRoundId(), + 1, + "Default global start round ID should be 1" ); - // Move to round 2 - vm.warp(_defaultRoundParams.roundStart + 3 days + 1); - - // ------------ PART 1: VERIFY PERSONAL CAP ACCUMULATION ------------ - // Create unspent capacity structure - ILM_PC_FundingPot_v1.UnspentPersonalRoundCap[] memory unspentCaps = - new ILM_PC_FundingPot_v1.UnspentPersonalRoundCap[](1); - unspentCaps[0] = ILM_PC_FundingPot_v1.UnspentPersonalRoundCap({ - roundId: round1Id, - accessCriteriaId: accessCriteriaId, - merkleProof: new bytes32[](0) - }); + // --- Attempt Contribution in Round 3 by C3 --- + vm.warp(initialTimestamp + 5 days + 1 hours); - // Try to contribute more than the round 2 personal cap (400) - // In Personal mode, this should succeed up to the personal cap (400) + unspent from round 1 (300) = 700 - // But capped by round cap of 500 - vm.startPrank(contributor1_); + vm.startPrank(contributor3_); fundingPot.contributeToRoundFor( - contributor1_, - round2Id, - 450, // More than the personal cap of round 2 - accessCriteriaId, - new bytes32[](0), - unspentCaps + contributor3_, round3Id, r3ExpectedEffectiveCap, 1, new bytes32[](0) ); vm.stopPrank(); - // Verify contributions to round 2 - should be more than the personal cap of round 2 (400) - // This verifies personal caps DO accumulate - uint contributionAmount = - fundingPot.getUserContributionToRound(round2Id, contributor1_); - assertEq(contributionAmount, 450); - assertTrue(contributionAmount > 400, "Personal cap should accumulate"); - - // ------------ PART 2: VERIFY TOTAL CAP NON-ACCUMULATION ------------ - // Attempt to contribute more than the remaining round cap - vm.startPrank(contributor2_); - _token.approve(address(fundingPot), 200); - - // Contributor 2 attempts to contribute 100. - // Since contributor1 contributed 450 and round cap is 500, only 50 is remaining. - // The contribution should be clamped to 50. - fundingPot.contributeToRoundFor( - contributor2_, round2Id, 100, accessCriteriaId, new bytes32[](0) + // --- Assertions --- + assertEq( + fundingPot.getTotalRoundContribution(round3Id), + r3ExpectedEffectiveCap, + "R3 total contributions should match effective cap with rollover from R1 and R2" ); - // Verify contributor 2's contribution was clamped to the remaining 50. assertEq( - fundingPot.getUserContributionToRound(round2Id, contributor2_), 50 + fundingPot.getUserContributionToRound(round3Id, contributor3_), + r3ExpectedEffectiveCap, + "R3 C3 contribution incorrect" ); - vm.stopPrank(); - - // Verify total contributions to round 2 is exactly the round cap (450 + 50 = 500). - assertEq(fundingPot.getTotalRoundContribution(round2Id), 500); - - // Additional contributor3 should not be able to contribute anything as the cap is full. - // Attempting to contribute when the cap is already full should revert. - vm.startPrank(contributor3_); - _token.approve(address(fundingPot), 100); - // Expect revert because the round cap (500) is already met. + vm.startPrank(contributor1_); + _token.approve(address(fundingPot), 1); vm.expectRevert( abi.encodeWithSelector( ILM_PC_FundingPot_v1 @@ -3445,685 +3329,439 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { ) ); fundingPot.contributeToRoundFor( - contributor3_, round2Id, 1, accessCriteriaId, new bytes32[](0) + contributor1_, round3Id, 1, 1, new bytes32[](0) ); vm.stopPrank(); - - // Final check that total contributions remain at the round cap. - assertEq(fundingPot.getTotalRoundContribution(round2Id), 500); } - function testContribute_TotalMode_AccumulatesTotalOnly() public { - // 1. Create the first round with AccumulationMode.Total - _defaultRoundParams.accumulationMode = - ILM_PC_FundingPot_v1.AccumulationMode.Total; - - fundingPot.createRound( - _defaultRoundParams.roundStart, - _defaultRoundParams.roundEnd, - 1000, // Round 1 cap of 1000 - _defaultRoundParams.hookContract, - _defaultRoundParams.hookFunction, - _defaultRoundParams.autoClosure, - _defaultRoundParams.accumulationMode - ); - uint32 round1Id = fundingPot.getRoundCount(); - - // Set up access criteria for round 1 (Open) - uint8 accessCriteriaId = 1; - ( - address nftContract, - bytes32 merkleRoot, - address[] memory allowedAddresses - ) = _helper_createAccessCriteria( - uint8(ILM_PC_FundingPot_v1.AccessCriteriaType.OPEN), round1Id - ); - - fundingPot.setAccessCriteria( - round1Id, - uint8(ILM_PC_FundingPot_v1.AccessCriteriaType.OPEN), // accessCriteriaType - 0, // accessCriteriaId (0 for new) - nftContract, - merkleRoot, - allowedAddresses - ); - - // Set a personal cap of 800 for round 1 - fundingPot.setAccessCriteriaPrivileges( - round1Id, accessCriteriaId, 800, false, 0, 0, 0 - ); + function testContributeToRoundFor_allModeAllowsPersonalAccumulation() + public + { + // SCENARIO: globalAccumulationStartRoundId = 1 allows personal cap + // accumulation from R1 to R2, when both are in All mode. (Simplified for stack) - // 2. Create the second round, also with AccumulationMode.Total - RoundParams memory params = _helper_createEditRoundParams( - _defaultRoundParams.roundStart + 3 days, - _defaultRoundParams.roundEnd + 3 days, - 500, // Round 2 base cap of 500 - _defaultRoundParams.hookContract, - _defaultRoundParams.hookFunction, - _defaultRoundParams.autoClosure, - ILM_PC_FundingPot_v1.AccumulationMode.Total - ); + uint initialTimestamp = block.timestamp; + uint8 accessId = 1; - fundingPot.createRound( - params.roundStart, - params.roundEnd, - params.roundCap, - params.hookContract, - params.hookFunction, - params.autoClosure, - params.accumulationMode + // --- Round 1: Setup & C1 Contribution --- + uint r1PersonalCapC1 = 500; + uint r1ContributionC1 = 100; + uint32 round1Id = fundingPot.createRound( + initialTimestamp + 1 days, + initialTimestamp + 2 days, + 1000, + address(0), + bytes(""), + false, + ILM_PC_FundingPot_v1.AccumulationMode.All ); - uint32 round2Id = fundingPot.getRoundCount(); - - // Set up access criteria for round 2 (Open) fundingPot.setAccessCriteria( - round2Id, - uint8(ILM_PC_FundingPot_v1.AccessCriteriaType.OPEN), // accessCriteriaType - 0, // accessCriteriaId (0 for new) - nftContract, - merkleRoot, - allowedAddresses + round1Id, accessId, 0, address(0), bytes32(0), new address[](0) ); - - // Set a personal cap of 300 for round 2 fundingPot.setAccessCriteriaPrivileges( - round2Id, accessCriteriaId, 300, false, 0, 0, 0 + round1Id, accessId, r1PersonalCapC1, false, 0, 0, 0 ); - // Round 1 contribution: contributor1 contributes 600 (less than round cap 1000, less than personal 800) - // Undersubscription: 1000 - 600 = 400 - vm.warp(_defaultRoundParams.roundStart + 1); + vm.warp(initialTimestamp + 1 days + 1 hours); vm.startPrank(contributor1_); - _token.approve(address(fundingPot), 1000); + _token.approve(address(fundingPot), type(uint).max); fundingPot.contributeToRoundFor( - contributor1_, round1Id, 600, accessCriteriaId, new bytes32[](0) + contributor1_, + round1Id, + r1ContributionC1, + accessId, + new bytes32[](0) ); vm.stopPrank(); + uint r1UnusedPersonalForC1 = r1PersonalCapC1 - r1ContributionC1; - // Verify contribution to round 1 - assertEq( - fundingPot.getUserContributionToRound(round1Id, contributor1_), 600 + // --- Round 2: Setup --- + uint r2BasePersonalCapC1 = 200; + uint32 round2Id = fundingPot.createRound( + initialTimestamp + 3 days, + initialTimestamp + 4 days, + 2000, + address(0), + bytes(""), + false, + ILM_PC_FundingPot_v1.AccumulationMode.All ); - assertEq(fundingPot.getTotalRoundContribution(round1Id), 600); - - // Move to round 2 - vm.warp(_defaultRoundParams.roundStart + 3 days + 1); - - // ------------ PART 1: VERIFY TOTAL CAP ACCUMULATION ------------ - // Effective Round 2 Cap = Base Cap (500) + Unused from Round 1 (400) = 900 - vm.startPrank(contributor2_); - _token.approve(address(fundingPot), 1000); // Approve enough - - // Contributor 2 attempts to contribute 700. - // Personal Cap (R2) is 300. Gets clamped to 300. - fundingPot.contributeToRoundFor( - contributor2_, round2Id, 700, accessCriteriaId, new bytes32[](0) + fundingPot.setAccessCriteria( + round2Id, accessId, 0, address(0), bytes32(0), new address[](0) ); - // Verify contributor 2's contribution was clamped by personal cap. - assertEq( - fundingPot.getUserContributionToRound(round2Id, contributor2_), - 300, - "C2 contribution should be clamped by personal cap" + fundingPot.setAccessCriteriaPrivileges( + round2Id, accessId, r2BasePersonalCapC1, false, 0, 0, 0 ); - vm.stopPrank(); - // Verify total contributions after C2 is 300 + // --- Global Start ID Check --- assertEq( - fundingPot.getTotalRoundContribution(round2Id), - 300, - "Total after C2 should be 300" + fundingPot.getGlobalAccumulationStartRoundId(), + 1, + "Default global start ID is 1" ); - // ------------ PART 2: VERIFY PERSONAL CAP NON-ACCUMULATION ------------ - // Contributor 1 had 800 personal cap in R1, contributed 600, unused = 200. - // Contributor 1 has 300 personal cap in R2. - // In Total mode, personal cap does NOT roll over. Max contribution is 300. + // --- C1 Contribution to Round 2 (Testing Personal Cap Rollover) --- + vm.warp(initialTimestamp + 3 days + 1 hours); + uint expectedEffectivePersonalCapC1R2 = + r2BasePersonalCapC1 + r1UnusedPersonalForC1; + uint c1AttemptR2 = expectedEffectivePersonalCapC1R2 + 50; - // Prepare unspent caps struct (even though it shouldn't work for personal in Total mode) - ILM_PC_FundingPot_v1.UnspentPersonalRoundCap[] memory unspentCaps = + ILM_PC_FundingPot_v1.UnspentPersonalRoundCap[] memory unspentCapsC1 = new ILM_PC_FundingPot_v1.UnspentPersonalRoundCap[](1); - unspentCaps[0] = ILM_PC_FundingPot_v1.UnspentPersonalRoundCap({ - roundId: round1Id, - accessCriteriaId: accessCriteriaId, - merkleProof: new bytes32[](0) - }); + unspentCapsC1[0] = ILM_PC_FundingPot_v1.UnspentPersonalRoundCap( + round1Id, accessId, new bytes32[](0) + ); vm.startPrank(contributor1_); - _token.approve(address(fundingPot), 500); - - // Attempt to contribute 400 ( > R2 personal cap 300) - // Total contributions = 300. Effective Round Cap = 900. Remaining Round Cap = 600. - // Personal Cap (R2) = 300. Unspent (R1) = 200, ignored in Total mode. - // Min(Remaining Round Cap, Remaining Personal Cap) = Min(600, 300) = 300. - // Should be clamped to 300. fundingPot.contributeToRoundFor( contributor1_, round2Id, - 400, - accessCriteriaId, + c1AttemptR2, + accessId, new bytes32[](0), - unspentCaps // Provide unspent caps, although they should be ignored for personal limit - ); - // Verify contributor 1's contribution was clamped to their R2 personal cap. - assertEq( - fundingPot.getUserContributionToRound(round2Id, contributor1_), - 300, - "C1 contribution should be clamped by personal cap" + unspentCapsC1 ); vm.stopPrank(); - // Verify total round contributions: 300 (C2) + 300 (C1) = 600 - assertEq( - fundingPot.getTotalRoundContribution(round2Id), - 600, - "Total after C1 and C2 should be 600" - ); - // Effective cap 900, current total 600. Remaining = 300. - - // Contributor 3 contributes 300. Personal Cap = 300. Remaining Round Cap = 300. Should succeed. - vm.startPrank(contributor3_); - _token.approve(address(fundingPot), 300); - fundingPot.contributeToRoundFor( - contributor3_, round2Id, 300, accessCriteriaId, new bytes32[](0) - ); - // Verify C3 contributed 300 + // --- Assertions --- assertEq( - fundingPot.getUserContributionToRound(round2Id, contributor3_), - 300, - "C3 contributes remaining 300" + fundingPot.getUserContributionToRound(round2Id, contributor1_), + expectedEffectivePersonalCapC1R2, + "R2 C1 personal contribution incorrect" ); - vm.stopPrank(); - - // Total contributions should now be 900 (300 + 300 + 300), matching the effective cap. assertEq( fundingPot.getTotalRoundContribution(round2Id), - 900, - "Total should match effective cap after C3" + expectedEffectivePersonalCapC1R2, + "R2 Total contributions incorrect" ); + } - // Now the effective cap is full. Try contributing 1 again. - vm.startPrank(contributor3_); // Can use C3 or another contributor - _token.approve(address(fundingPot), 1); + function testContributeToRoundFor_allModeAllowsTotalAccumulation() public { + // SCENARIO: globalAccumulationStartRoundId = 1 (default or set) allows total cap + // accumulation from R1 to R2, when both are in All mode. - // Try contributing 1, expect revert as cap is full - vm.expectRevert( - abi.encodeWithSelector( - ILM_PC_FundingPot_v1 - .Module__LM_PC_FundingPot__RoundCapReached - .selector - ) - ); - fundingPot.contributeToRoundFor( - contributor3_, round2Id, 1, accessCriteriaId, new bytes32[](0) - ); - vm.stopPrank(); + uint initialTimestamp = block.timestamp; + uint8 accessId = 1; // Open access - // Final total check should remain 900 - assertEq( - fundingPot.getTotalRoundContribution(round2Id), - 900, - "Final total should be effective cap" - ); - } + // --- Round 1 Parameters (All Mode) --- + uint r1BaseTotalCap = 1000; + uint r1C1PersonalCap = 800; + uint r1C1Contribution = 600; - // ------------------------------------------------------------------------- - // Test: Global Accumulation Start Round Settings - // ------------------------------------------------------------------------- + // --- Round 2 Parameters (All Mode) --- + uint r2BaseTotalCap = 500; - /* Test getGlobalAccumulationStartRoundId() and setGlobalAccumulationStart() - ├── For getGlobalAccumulationStartRoundId() - │ └── When no explicit value has been set - │ └── Then it should return the default value of 1 - │ - └── For setGlobalAccumulationStart() - ├── Given user does not have FUNDING_POT_ADMIN_ROLE - │ └── When user attempts to set the global accumulation start round - │ └── Then it should revert - │ - ├── Given user has FUNDING_POT_ADMIN_ROLE - │ ├── And the provided start round ID is 0 - │ │ └── When user attempts to set the global accumulation start round - │ │ └── Then it should revert - │ │ - │ ├── And the provided start round ID is greater than the current round count - │ │ └── When user attempts to set the global accumulation start round - │ │ └── Then it should revert - │ │ - │ └── And a valid start round ID is provided - │ └── When user attempts to set the global accumulation start round - │ ├── Then it should update the globalAccumulationStartRoundId state variable - │ ├── Then it should emit a GlobalAccumulationStartSet event - │ └── Then getGlobalAccumulationStartRoundId() should return the new value - */ + // --- Approvals --- + vm.startPrank(contributor1_); + _token.approve(address(fundingPot), type(uint).max); + vm.stopPrank(); - function testGetGlobalAccumulationStartRoundId_returnsDefaultWhenNotSet() - public - { - // Expecting default value to be 1 as per AC - assertEq( - fundingPot.getGlobalAccumulationStartRoundId(), - 1, - "Default start round ID should be 1" + // --- Create Round 1 (All Mode) --- + uint32 round1Id = fundingPot.createRound( + initialTimestamp + 1 days, + initialTimestamp + 2 days, + r1BaseTotalCap, + address(0), + bytes(""), + false, + ILM_PC_FundingPot_v1.AccumulationMode.All ); - } - - function testSetGlobalAccumulationStart_revertsGivenUserIsNotFundingPotAdmin( - address unauthorizedUser, - uint32 startRoundId - ) public { - vm.assume(unauthorizedUser != address(this)); - vm.assume(startRoundId >= 1); - - bytes32 roleId = _authorizer.generateRoleId( - address(fundingPot), fundingPot.FUNDING_POT_ADMIN_ROLE() + fundingPot.setAccessCriteria( + round1Id, accessId, 0, address(0), bytes32(0), new address[](0) + ); + fundingPot.setAccessCriteriaPrivileges( + round1Id, accessId, r1C1PersonalCap, false, 0, 0, 0 ); - vm.startPrank(unauthorizedUser); - vm.expectRevert( - abi.encodeWithSelector( - IModule_v1.Module__CallerNotAuthorized.selector, - roleId, - unauthorizedUser - ) + // --- Contribution by C1 to Round 1 --- + vm.warp(initialTimestamp + 1 days + 1 hours); + vm.startPrank(contributor1_); + fundingPot.contributeToRoundFor( + contributor1_, + round1Id, + r1C1Contribution, + accessId, + new bytes32[](0) ); - fundingPot.setGlobalAccumulationStart(startRoundId); vm.stopPrank(); - } + uint r1UnusedTotal = r1BaseTotalCap - r1C1Contribution; - function testSetGlobalAccumulationStart_revertsGivenStartRoundIsZero() - public - { - // AC: Must revert if startRoundId_ == 0 - vm.expectRevert( - abi.encodeWithSelector( - ILM_PC_FundingPot_v1 - .Module__LM_PC_FundingPot__StartRoundCannotBeZero - .selector - ) + // --- Create Round 2 (All Mode) --- + uint32 round2Id = fundingPot.createRound( + initialTimestamp + 3 days, + initialTimestamp + 4 days, + r2BaseTotalCap, + address(0), + bytes(""), + false, + ILM_PC_FundingPot_v1.AccumulationMode.All + ); + fundingPot.setAccessCriteria( + round2Id, accessId, 0, address(0), bytes32(0), new address[](0) + ); + uint r2ExpectedEffectiveTotalCap = r2BaseTotalCap + r1UnusedTotal; + fundingPot.setAccessCriteriaPrivileges( + round2Id, accessId, r2ExpectedEffectiveTotalCap, false, 0, 0, 0 ); - fundingPot.setGlobalAccumulationStart(0); - } - - function testSetGlobalAccumulationStart_revertsGivenStartRoundGreaterThanCurrentRoundCount( - uint32 startRoundIdOffset - ) public { - testCreateRound(); // Ensure roundCount is at least 1 - uint32 currentRoundCount = fundingPot.getRoundCount(); - - vm.assume(startRoundIdOffset > 0); // Ensure invalidStartRoundId will be greater - vm.assume(startRoundIdOffset <= type(uint32).max - currentRoundCount); - - uint32 invalidStartRoundId = currentRoundCount + startRoundIdOffset; - vm.expectRevert( - abi.encodeWithSelector( - ILM_PC_FundingPot_v1 - .Module__LM_PC_FundingPot__StartRoundGreaterThanRoundCount - .selector, - invalidStartRoundId, - currentRoundCount - ) + // --- Ensure Global Start Round ID is 1 --- + assertEq( + fundingPot.getGlobalAccumulationStartRoundId(), + 1, + "Global start round ID should be 1 by default" ); - fundingPot.setGlobalAccumulationStart(invalidStartRoundId); - } - function testSetGlobalAccumulationStart_worksGivenValidParameters( - uint32 startRoundId - ) public { - // Ensure we have at least startRoundId rounds created if startRoundId > 0 - if (startRoundId > 0) { - vm.assume(startRoundId <= 10); // Bound the fuzzing - for ( - uint32 i = fundingPot.getRoundCount() + 1; - i <= startRoundId; - i++ - ) { - fundingPot.createRound( - _defaultRoundParams.roundStart + (i * 3 days), - _defaultRoundParams.roundEnd + (i * 3 days), - _defaultRoundParams.roundCap, - _defaultRoundParams.hookContract, - _defaultRoundParams.hookFunction, - _defaultRoundParams.autoClosure, - _defaultRoundParams.accumulationMode - ); - } - vm.assume(startRoundId <= fundingPot.getRoundCount()); // Final check - } else { - vm.assume(startRoundId == 0); - // For startRoundId = 0, test should actually fail based on the revert check above - // However, fuzzing might pass 0. Let's test non-zero valid cases. - vm.assume(false); // This will cause fuzz to skip startRoundId = 0 here - } + // --- Attempt Contribution in Round 2 by C1 to fill effective total cap --- + vm.warp(initialTimestamp + 3 days + 1 hours); - // Expect event emission - vm.expectEmit(true, true, true, true); - emit ILM_PC_FundingPot_v1.GlobalAccumulationStartSet(startRoundId); + uint c1AttemptR2 = r2ExpectedEffectiveTotalCap; - // Set the value - fundingPot.setGlobalAccumulationStart(startRoundId); + vm.startPrank(contributor1_); + fundingPot.contributeToRoundFor( + contributor1_, round2Id, c1AttemptR2, accessId, new bytes32[](0) + ); + vm.stopPrank(); - // Verify the value using the getter + // --- Assertions --- assertEq( - fundingPot.getGlobalAccumulationStartRoundId(), - startRoundId, - "Getter should return the set value" + fundingPot.getUserContributionToRound(round2Id, contributor1_), + c1AttemptR2, + "R2 C1 contribution should match attempt (filling effective total cap)" + ); + assertEq( + fundingPot.getTotalRoundContribution(round2Id), + r2ExpectedEffectiveTotalCap, + "R2 Total contributions should match effective total cap (All mode, global_start=1)" ); } - // ------------------------------------------------------------------------- - // Test: Global Accumulation Start Round - Logic Integration - // ------------------------------------------------------------------------- - - /* Test Accumulation Logic with Global Start Round - │ - ├── Scenario: Global start round restricts accumulation - │ ├── Given globalAccumulationStartRoundId is set to 2 (e.g., R2) - │ │ ├── And target round (e.g., R3) uses AccumulationMode.Personal - │ │ │ └── When contributing to R3 with unspent capacity from R1 and R2 - │ │ │ └── Then only unspent personal capacity from R2 should be considered - │ │ ├── And target round (e.g., R3) uses AccumulationMode.Total - │ │ │ └── When contributing to R3 - │ │ │ └── Then only unspent total capacity from R2 should expand R3's effective cap - │ │ └── And target round (e.g., R3) uses AccumulationMode.All - │ │ ├── When contributing to R3 with unspent personal capacity from R1 and R2 - │ │ │ └── Then only unspent personal capacity from R2 should be considered - │ │ └── When calculating R3's effective total cap - │ │ └── Then only unspent total capacity from R2 should expand R3's effective cap - │ - ├── Scenario: Default global start round (1) allows accumulation from all previous applicable rounds - │ ├── Given globalAccumulationStartRoundId is 1 (default) - │ │ ├── And target round (e.g., R2 or R3) uses AccumulationMode.Personal - │ │ │ └── When contributing with unspent capacity from all previous valid rounds (e.g., R1 for R2; R1 & R2 for R3) - │ │ │ └── Then unspent personal capacity from all applicable previous rounds should be considered - │ │ ├── And target round (e.g., R2 or R3) uses AccumulationMode.Total - │ │ │ └── When calculating effective total cap - │ │ │ └── Then unspent total capacity from all applicable previous rounds should expand the effective cap - │ │ └── And target round (e.g., R2 or R3) uses AccumulationMode.All - │ │ ├── When contributing with unspent personal capacity from all previous valid rounds - │ │ │ └── Then unspent personal capacity from all applicable previous rounds should be considered - │ │ └── When calculating effective total cap - │ │ └── Then unspent total capacity from all applicable previous rounds should expand the effective cap - │ - ├── Scenario: Interaction with AccumulationMode.Disabled - │ └── Given target round's AccumulationMode is Disabled - │ └── When globalAccumulationStartRoundId is set to allow previous rounds - │ └── Then no accumulation (personal or total) should occur for the target round - │ - └── Scenario: Global start round equals target round - └── Given globalAccumulationStartRoundId is set to the target round's ID - └── When target round's AccumulationMode would normally allow accumulation - └── Then no accumulation (personal or total) from any *previous* round should occur - */ - - function testContribute_personalMode_globalStartRestrictsAccumulationFromEarlierRounds( + function testContributeToRoundFor_allModeWithGlobalStartRestrictsPersonalAccumulation( ) public { - // SCENARIO: globalAccumulationStartRoundId = 2 restricts accumulation from Round 1 for Personal mode - // 1. Setup: Round 1, Round 2, Round 3. Partial contributions in R1 & R2. - // 2. Action: setGlobalAccumulationStart(2) - // 3. Verification: For contributions to R3 (Personal mode), only unused personal from R2 rolls over. + // SCENARIO: globalAccumulationStartRoundId = 2 restricts personal cap accumulation + // from R1 for R2, when both are in All mode. uint initialTimestamp = block.timestamp; + uint8 accessId = 1; // Open access - // --- Setup Rounds --- - uint r1PersonalCap = 500; - uint r1Contribution = 100; - // uint r1UnusedPersonal = r1PersonalCap - r1Contribution; // Not used in this restricted scenario directly for R3 calc + // --- Round 1 Parameters (All Mode) --- + uint r1PersonalCapC1 = 500; + uint r1ContributionC1 = 100; + uint r1BaseTotalCap = 1000; - uint r2PersonalCap = 600; - uint r2Contribution = 200; - uint r2UnusedPersonal = r2PersonalCap - r2Contribution; // This IS used for R3 calculation + // --- Round 2 Parameters (All Mode) --- + uint r2BasePersonalCapC1 = 50; + uint r2BaseTotalCap = 1000; - uint r3BasePersonalCap = 300; + // --- Approvals --- + vm.startPrank(contributor1_); + _token.approve(address(fundingPot), type(uint).max); + vm.stopPrank(); - // Round 1 + // --- Create Round 1 (All Mode) --- uint32 round1Id = fundingPot.createRound( initialTimestamp + 1 days, initialTimestamp + 2 days, - 10_000, // large round cap + r1BaseTotalCap, address(0), bytes(""), false, - ILM_PC_FundingPot_v1.AccumulationMode.Personal + ILM_PC_FundingPot_v1.AccumulationMode.All ); fundingPot.setAccessCriteria( - round1Id, 1, 0, address(0), bytes32(0), new address[](0) - ); // Open access + round1Id, accessId, 0, address(0), bytes32(0), new address[](0) + ); fundingPot.setAccessCriteriaPrivileges( - round1Id, 1, r1PersonalCap, false, 0, 0, 0 + round1Id, accessId, r1PersonalCapC1, false, 0, 0, 0 ); - // Round 2 + // --- Contribution by C1 to Round 1 --- + vm.warp(initialTimestamp + 1 days + 1 hours); + vm.startPrank(contributor1_); + fundingPot.contributeToRoundFor( + contributor1_, + round1Id, + r1ContributionC1, + accessId, + new bytes32[](0) + ); + vm.stopPrank(); + + // --- Create Round 2 (All Mode) --- uint32 round2Id = fundingPot.createRound( initialTimestamp + 3 days, initialTimestamp + 4 days, - 10_000, // large round cap - address(0), - bytes(""), - false, - ILM_PC_FundingPot_v1.AccumulationMode.Personal - ); - fundingPot.setAccessCriteria( - round2Id, 1, 0, address(0), bytes32(0), new address[](0) - ); - fundingPot.setAccessCriteriaPrivileges( - round2Id, 1, r2PersonalCap, false, 0, 0, 0 - ); - - // Round 3 - uint32 round3Id = fundingPot.createRound( - initialTimestamp + 5 days, - initialTimestamp + 6 days, - 10_000, // large round cap + r2BaseTotalCap, address(0), bytes(""), false, - ILM_PC_FundingPot_v1.AccumulationMode.Personal + ILM_PC_FundingPot_v1.AccumulationMode.All ); fundingPot.setAccessCriteria( - round3Id, 1, 0, address(0), bytes32(0), new address[](0) + round2Id, accessId, 0, address(0), bytes32(0), new address[](0) ); fundingPot.setAccessCriteriaPrivileges( - round3Id, 1, r3BasePersonalCap, false, 0, 0, 0 - ); - - // --- Contributions --- - vm.startPrank(contributor1_); - _token.approve(address(fundingPot), type(uint).max); - - vm.warp(initialTimestamp + 1 days + 1 hours); // Enter Round 1 - fundingPot.contributeToRoundFor( - contributor1_, round1Id, r1Contribution, 1, new bytes32[](0) + round2Id, accessId, r2BasePersonalCapC1, false, 0, 0, 0 ); - vm.warp(initialTimestamp + 3 days + 1 hours); // Enter Round 2 - fundingPot.contributeToRoundFor( - contributor1_, round2Id, r2Contribution, 1, new bytes32[](0) + // --- Set Global Start Round ID to Round 2's ID --- + fundingPot.setGlobalAccumulationStart(round2Id); + assertEq( + fundingPot.getGlobalAccumulationStartRoundId(), + round2Id, + "Global start ID not set to R2 ID" ); - vm.stopPrank(); - // --- Set Global Start --- - fundingPot.setGlobalAccumulationStart(2); - assertEq(fundingPot.getGlobalAccumulationStartRoundId(), 2); + // --- Attempt Contribution in Round 2 by C1 --- + vm.warp(initialTimestamp + 3 days + 1 hours); - // --- Attempt Contribution in Round 3 --- - vm.warp(initialTimestamp + 5 days + 1 hours); // Enter Round 3 + uint c1AttemptR2 = r2BasePersonalCapC1 + 100; - ILM_PC_FundingPot_v1.UnspentPersonalRoundCap[] memory unspentCaps = - new ILM_PC_FundingPot_v1.UnspentPersonalRoundCap[](2); - // User claims unspent from R1 (should be ignored due to global start) - unspentCaps[0] = ILM_PC_FundingPot_v1.UnspentPersonalRoundCap( - round1Id, 1, new bytes32[](0) - ); - // User claims unspent from R2 (should be counted) - unspentCaps[1] = ILM_PC_FundingPot_v1.UnspentPersonalRoundCap( - round2Id, 1, new bytes32[](0) + ILM_PC_FundingPot_v1.UnspentPersonalRoundCap[] memory unspentCapsC1 = + new ILM_PC_FundingPot_v1.UnspentPersonalRoundCap[](1); + unspentCapsC1[0] = ILM_PC_FundingPot_v1.UnspentPersonalRoundCap( + round1Id, accessId, new bytes32[](0) ); - uint expectedR3PersonalCap = r3BasePersonalCap + r2Contribution; // Only R2's unused personal cap - vm.startPrank(contributor1_); - // Attempt to contribute up to the expected new personal cap fundingPot.contributeToRoundFor( contributor1_, - round3Id, - expectedR3PersonalCap, - 1, + round2Id, + c1AttemptR2, + accessId, new bytes32[](0), - unspentCaps + unspentCapsC1 ); vm.stopPrank(); - // --- Assertion --- + // --- Assertions --- assertEq( - fundingPot.getUserContributionToRound(round3Id, contributor1_), - expectedR3PersonalCap, - "R3 personal contribution incorrect" + fundingPot.getUserContributionToRound(round2Id, contributor1_), + r2BasePersonalCapC1, + "R2 C1 personal contribution should be clamped by R2 base personal cap (All mode, global_start=R2)" + ); + assertEq( + fundingPot.getTotalRoundContribution(round2Id), + r2BasePersonalCapC1, + "R2 Total contributions should be C1's clamped amount (All mode, global_start=R2)" ); } - function testContribute_totalMode_globalStartRestrictsAccumulationFromEarlierRounds( + function testContributeToRoundFor_allModeWithGlobalStartRestrictsTotalAccumulation( ) public { - // SCENARIO: globalAccumulationStartRoundId = 2 restricts accumulation from Round 1 for Total mode - // 1. Setup: Round 1, Round 2, Round 3. Partial contributions in R1 & R2. - // 2. Action: setGlobalAccumulationStart(2) - // 3. Verification: For contributions to R3 (Total mode), only unused total from R2 expands R3 cap. + // SCENARIO: globalAccumulationStartRoundId = 2 restricts total cap accumulation + // from R1 for R2, when both are in All mode. uint initialTimestamp = block.timestamp; + uint8 accessId = 1; // Open access - // --- Setup Rounds --- - uint r1BaseCap = 1000; - uint r1Contribution = 400; - // uint r1UnusedTotal = r1BaseCap - r1Contribution; + // --- Round 1 Parameters (All Mode) --- + uint r1BaseTotalCap = 1000; + uint r1C1PersonalCap = 800; + uint r1C1Contribution = 400; - uint r2BaseCap = 1200; - uint r2Contribution = 500; - // uint r2UnusedTotal = r2BaseCap - r2Contribution; + // --- Round 2 Parameters (All Mode) --- + uint r2BaseTotalCap = 200; - uint r3BaseCap = 300; + // --- Approvals --- + vm.startPrank(contributor1_); + _token.approve(address(fundingPot), type(uint).max); + vm.stopPrank(); - // Round 1 + // --- Create Round 1 (All Mode) --- uint32 round1Id = fundingPot.createRound( initialTimestamp + 1 days, initialTimestamp + 2 days, - r1BaseCap, + r1BaseTotalCap, address(0), bytes(""), false, - ILM_PC_FundingPot_v1.AccumulationMode.Total + ILM_PC_FundingPot_v1.AccumulationMode.All ); fundingPot.setAccessCriteria( - round1Id, 1, 0, address(0), bytes32(0), new address[](0) - ); // Open access + round1Id, accessId, 0, address(0), bytes32(0), new address[](0) + ); fundingPot.setAccessCriteriaPrivileges( - round1Id, 1, r1BaseCap, false, 0, 0, 0 - ); // Personal cap equals round cap + round1Id, accessId, r1C1PersonalCap, false, 0, 0, 0 + ); - // Round 2 + // --- Contribution by C1 to Round 1 --- + vm.warp(initialTimestamp + 1 days + 1 hours); + vm.startPrank(contributor1_); + fundingPot.contributeToRoundFor( + contributor1_, + round1Id, + r1C1Contribution, + accessId, + new bytes32[](0) + ); + vm.stopPrank(); + + // --- Create Round 2 (All Mode) --- uint32 round2Id = fundingPot.createRound( initialTimestamp + 3 days, initialTimestamp + 4 days, - r2BaseCap, - address(0), - bytes(""), - false, - ILM_PC_FundingPot_v1.AccumulationMode.Total - ); - fundingPot.setAccessCriteria( - round2Id, 1, 0, address(0), bytes32(0), new address[](0) - ); - fundingPot.setAccessCriteriaPrivileges( - round2Id, 1, r2BaseCap, false, 0, 0, 0 - ); - - // Round 3 - uint32 round3Id = fundingPot.createRound( - initialTimestamp + 5 days, - initialTimestamp + 6 days, - r3BaseCap, + r2BaseTotalCap, address(0), bytes(""), false, - ILM_PC_FundingPot_v1.AccumulationMode.Total + ILM_PC_FundingPot_v1.AccumulationMode.All ); fundingPot.setAccessCriteria( - round3Id, 1, 0, address(0), bytes32(0), new address[](0) + round2Id, accessId, 0, address(0), bytes32(0), new address[](0) ); fundingPot.setAccessCriteriaPrivileges( - round3Id, 1, r3BaseCap + r2Contribution, false, 0, 0, 0 - ); // Allow full contribution for testing effective cap - - // --- Contributions --- - vm.startPrank(contributor1_); - _token.approve(address(fundingPot), type(uint).max); - - vm.warp(initialTimestamp + 1 days + 1 hours); // Enter Round 1 - fundingPot.contributeToRoundFor( - contributor1_, round1Id, r1Contribution, 1, new bytes32[](0) + round2Id, accessId, r2BaseTotalCap, false, 0, 0, 0 ); - vm.warp(initialTimestamp + 3 days + 1 hours); // Enter Round 2 - fundingPot.contributeToRoundFor( - contributor1_, round2Id, r2Contribution, 1, new bytes32[](0) + // --- Set Global Start Round ID to Round 2's ID --- + fundingPot.setGlobalAccumulationStart(round2Id); + assertEq( + fundingPot.getGlobalAccumulationStartRoundId(), + round2Id, + "Global start ID not set to R2 ID" ); - vm.stopPrank(); - - // --- Set Global Start --- - fundingPot.setGlobalAccumulationStart(2); - assertEq(fundingPot.getGlobalAccumulationStartRoundId(), 2); - // --- Attempt Contribution in Round 3 --- - vm.warp(initialTimestamp + 5 days + 1 hours); // Enter Round 3 + // --- Attempt Contribution in Round 2 by C1 --- + vm.warp(initialTimestamp + 3 days + 1 hours); - uint expectedR3EffectiveCap = r3BaseCap + r2Contribution; // Only R2's unused total cap + uint c1AttemptR2 = r2BaseTotalCap + 100; vm.startPrank(contributor1_); - // Attempt to contribute up to the expected new effective cap fundingPot.contributeToRoundFor( - contributor1_, round3Id, expectedR3EffectiveCap, 1, new bytes32[](0) + contributor1_, round2Id, c1AttemptR2, accessId, new bytes32[](0) ); vm.stopPrank(); - // --- Assertion --- + // --- Assertions --- assertEq( - fundingPot.getTotalRoundContribution(round3Id), - expectedR3EffectiveCap, - "R3 total contribution incorrect, effective cap not as expected" + fundingPot.getUserContributionToRound(round2Id, contributor1_), + r2BaseTotalCap, + "R2 C1 contribution should be clamped by R2 base total cap (All mode, global_start=R2)" ); assertEq( - fundingPot.getUserContributionToRound(round3Id, contributor1_), - expectedR3EffectiveCap, - "R3 user contribution incorrect" + fundingPot.getTotalRoundContribution(round2Id), + r2BaseTotalCap, + "R2 Total contributions should be R2 base total cap (All mode, global_start=R2)" ); } - function testContribute_personalMode_defaultGlobalStartAllowsAccumulationFromAllPrevious( + function testContributeToRoundFor_revertsGivenUnspentCapsRoundIdsNotStrictlyIncreasing( ) public { - // SCENARIO: Default globalAccumulationStartRoundId = 1 allows accumulation from R1 for R2 (Personal mode) - // 1. Setup: Round 1 (Personal), Round 2 (Personal). - // Partial contribution by C1 in R1. - // 2. Action: Verify getGlobalAccumulationStartRoundId() == 1 (default). - // 3. Verification: For C1's contribution to R2, unused personal capacity from R1 rolls over. - + // Setup: Round 1 (Personal), Round 2 (Personal), Round 3 (Personal for contribution) uint initialTimestamp = block.timestamp; uint8 accessId = 1; // Open access + uint personalCap = 500; + uint roundCap = 10_000; - // --- Round Parameters & Contributions for C1 --- - uint r1PersonalCapC1 = 500; - uint r1ContributionC1 = 100; // C1 leaves 400 personal unused from R1 - - uint r2BasePersonalCapC1 = 300; // C1's base personal cap in R2 - - // --- Approvals --- vm.startPrank(contributor1_); _token.approve(address(fundingPot), type(uint).max); vm.stopPrank(); - // --- Create Round 1 (Personal Mode) --- + // Round 1 uint32 round1Id = fundingPot.createRound( initialTimestamp + 1 days, initialTimestamp + 2 days, - 10_000, // Large round cap, not relevant for personal accumulation focus + roundCap, address(0), bytes(""), false, @@ -4133,14 +3771,14 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { round1Id, accessId, 0, address(0), bytes32(0), new address[](0) ); fundingPot.setAccessCriteriaPrivileges( - round1Id, accessId, r1PersonalCapC1, false, 0, 0, 0 + round1Id, accessId, personalCap, false, 0, 0, 0 ); - // --- Create Round 2 (Personal Mode) --- + // Round 2 uint32 round2Id = fundingPot.createRound( initialTimestamp + 3 days, initialTimestamp + 4 days, - 10_000, // Large round cap + roundCap, address(0), bytes(""), false, @@ -4150,1301 +3788,1550 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { round2Id, accessId, 0, address(0), bytes32(0), new address[](0) ); fundingPot.setAccessCriteriaPrivileges( - round2Id, accessId, r2BasePersonalCapC1, false, 0, 0, 0 + round2Id, accessId, personalCap, false, 0, 0, 0 + ); + + // Round 3 (target for contribution) + uint32 round3Id = fundingPot.createRound( + initialTimestamp + 5 days, + initialTimestamp + 6 days, + roundCap, + address(0), + bytes(""), + false, + ILM_PC_FundingPot_v1.AccumulationMode.Personal + ); + fundingPot.setAccessCriteria( + round3Id, accessId, 0, address(0), bytes32(0), new address[](0) + ); + fundingPot.setAccessCriteriaPrivileges( + round3Id, accessId, personalCap, false, 0, 0, 0 + ); + + vm.warp(initialTimestamp + 5 days + 1 hours); // Enter Round 3 + + // Case 1: Out of order + ILM_PC_FundingPot_v1.UnspentPersonalRoundCap[] memory + unspentCapsOutOfOrder = + new ILM_PC_FundingPot_v1.UnspentPersonalRoundCap[](2); + unspentCapsOutOfOrder[0] = ILM_PC_FundingPot_v1.UnspentPersonalRoundCap( + round2Id, accessId, new bytes32[](0) + ); + unspentCapsOutOfOrder[1] = ILM_PC_FundingPot_v1.UnspentPersonalRoundCap( + round1Id, accessId, new bytes32[](0) ); - // --- Contribution by C1 to Round 1 --- vm.startPrank(contributor1_); - vm.warp(initialTimestamp + 1 days + 1 hours); // Enter Round 1 + vm.expectRevert( + ILM_PC_FundingPot_v1 + .Module__LM_PC_FundingPot__UnspentCapsRoundIdsNotStrictlyIncreasing + .selector + ); fundingPot.contributeToRoundFor( contributor1_, - round1Id, - r1ContributionC1, + round3Id, + 100, accessId, - new bytes32[](0) + new bytes32[](0), + unspentCapsOutOfOrder ); vm.stopPrank(); - // --- Verify Default Global Start Round ID --- - assertEq( - fundingPot.getGlobalAccumulationStartRoundId(), - 1, - "Default global start round ID should be 1" + // Case 2: Duplicate roundId + ILM_PC_FundingPot_v1.UnspentPersonalRoundCap[] memory + unspentCapsDuplicate = + new ILM_PC_FundingPot_v1.UnspentPersonalRoundCap[](2); + unspentCapsDuplicate[0] = ILM_PC_FundingPot_v1.UnspentPersonalRoundCap( + round1Id, accessId, new bytes32[](0) ); - - // --- Attempt Contribution in Round 2 by C1 --- - vm.warp(initialTimestamp + 3 days + 1 hours); // Enter Round 2 - - ILM_PC_FundingPot_v1.UnspentPersonalRoundCap[] memory unspentCapsC1 = - new ILM_PC_FundingPot_v1.UnspentPersonalRoundCap[](1); - unspentCapsC1[0] = ILM_PC_FundingPot_v1.UnspentPersonalRoundCap( - round1Id, - accessId, - new bytes32[](0) // Should be counted + unspentCapsDuplicate[1] = ILM_PC_FundingPot_v1.UnspentPersonalRoundCap( + round1Id, accessId, new bytes32[](0) ); - uint r1UnusedPersonalC1 = r1PersonalCapC1 - r1ContributionC1; // 400 - - // Expected C1 effective personal cap in R2 = R2_Base (300) + R1_Unused (400) = 700 - uint expectedC1EffectivePersonalCapR2 = - r2BasePersonalCapC1 + r1UnusedPersonalC1; - - uint c1AttemptR2 = expectedC1EffectivePersonalCapR2 + 50; // Try to contribute slightly more - uint expectedC1ContributionR2 = expectedC1EffectivePersonalCapR2; // Should be clamped - - // Ensure the attempt is not clamped by the round cap (which is large) - if (expectedC1ContributionR2 > 10_000) { - // 10_000 is round cap for R2 - expectedC1ContributionR2 = 10_000; - } - vm.startPrank(contributor1_); + vm.expectRevert( + ILM_PC_FundingPot_v1 + .Module__LM_PC_FundingPot__UnspentCapsRoundIdsNotStrictlyIncreasing + .selector + ); fundingPot.contributeToRoundFor( contributor1_, - round2Id, - c1AttemptR2, + round3Id, + 100, accessId, new bytes32[](0), - unspentCapsC1 + unspentCapsDuplicate ); vm.stopPrank(); - // --- Assertion --- - assertEq( - fundingPot.getUserContributionToRound(round2Id, contributor1_), - expectedC1ContributionR2, - "R2 C1 personal contribution incorrect (should use R1 unused)" - ); - } - - function testContribute_totalMode_defaultGlobalStartAllowsAccumulationFromAllPrevious( - ) public { - // SCENARIO: Default globalAccumulationStartRoundId = 1 allows total cap accumulation from R1 to R2 (Total mode) - // Simplified to reduce stack depth. + // Case 3: Correct order but first element's roundId is 0 (if lastSeenRoundId starts at 0) + // This specific case won't be hit if round IDs must be >0, but good to be aware. + // Assuming valid round IDs start from 1, this case might not be directly testable if 0 isn't a valid roundId. + // The current check `currentProcessingRoundId <= lastSeenRoundId` covers this if roundId can be 0. + // If round IDs are always >= 1, then an initial lastSeenRoundId=0 is fine. - uint initialTimestamp = block.timestamp; - uint8 accessId = 1; // Open access - - // --- Round 1 Parameters & Contribution --- - uint r1BaseCap = 1000; - uint r1ContributionC1 = 600; // Leaves 400 unused total from R1 - uint r1PersonalCap = 1000; - - // --- Round 2 Parameters --- - uint r2BaseCap = 500; - - // --- Approvals --- + // Case 4: Empty array (should not revert with this specific error, but pass) + ILM_PC_FundingPot_v1.UnspentPersonalRoundCap[] memory unspentCapsEmpty = + new ILM_PC_FundingPot_v1.UnspentPersonalRoundCap[](0); vm.startPrank(contributor1_); - _token.approve(address(fundingPot), type(uint).max); + fundingPot.contributeToRoundFor( // This should pass (or revert with a different error if amount is 0 etc.) + contributor1_, + round3Id, + 100, + accessId, + new bytes32[](0), + unspentCapsEmpty + ); vm.stopPrank(); + assertEq( + fundingPot.getUserContributionToRound(round3Id, contributor1_), 100 + ); + } - // --- Create Round 1 (Total Mode) --- - uint32 round1Id = fundingPot.createRound( - initialTimestamp + 1 days, - initialTimestamp + 2 days, - r1BaseCap, - address(0), - bytes(""), - false, - ILM_PC_FundingPot_v1.AccumulationMode.Total + function testContributeToRoundFor_personalModeOnlyAccumulatesPersonalCaps() + public + { + // 1. Create the first round with AccumulationMode.Personal + _defaultRoundParams.accumulationMode = + ILM_PC_FundingPot_v1.AccumulationMode.Personal; + + fundingPot.createRound( + _defaultRoundParams.roundStart, + _defaultRoundParams.roundEnd, + 1000, // Round cap of 1000 + _defaultRoundParams.hookContract, + _defaultRoundParams.hookFunction, + _defaultRoundParams.autoClosure, + _defaultRoundParams.accumulationMode + ); + uint32 round1Id = fundingPot.getRoundCount(); + + // Set up access criteria for round 1 + uint8 accessCriteriaId = 1; // Open access + ( + address nftContract, + bytes32 merkleRoot, + address[] memory allowedAddresses + ) = _helper_createAccessCriteria( + uint8(ILM_PC_FundingPot_v1.AccessCriteriaType.OPEN), round1Id ); + fundingPot.setAccessCriteria( - round1Id, accessId, 0, address(0), bytes32(0), new address[](0) + round1Id, + uint8(ILM_PC_FundingPot_v1.AccessCriteriaType.OPEN), // accessCriteriaType + 0, // accessCriteriaId (0 for new) + nftContract, + merkleRoot, + allowedAddresses ); + + // Set a personal cap of 500 for round 1 fundingPot.setAccessCriteriaPrivileges( - round1Id, accessId, r1PersonalCap, false, 0, 0, 0 + round1Id, accessCriteriaId, 500, false, 0, 0, 0 ); - // --- Contribution by C1 to Round 1 --- - vm.warp(initialTimestamp + 1 days + 1 hours); - vm.startPrank(contributor1_); - fundingPot.contributeToRoundFor( - contributor1_, - round1Id, - r1ContributionC1, - accessId, - new bytes32[](0) + // 2. Create the second round, also with AccumulationMode.Personal + // Use different start and end times to avoid overlap + RoundParams memory params = _helper_createEditRoundParams( + _defaultRoundParams.roundStart + 3 days, + _defaultRoundParams.roundEnd + 3 days, + 500, // Round cap of 500 + _defaultRoundParams.hookContract, + _defaultRoundParams.hookFunction, + _defaultRoundParams.autoClosure, + ILM_PC_FundingPot_v1.AccumulationMode.Personal ); - vm.stopPrank(); - uint r1UnusedTotal = r1BaseCap - r1ContributionC1; // Should be 400 - // --- Create Round 2 (Total Mode) --- - uint32 round2Id = fundingPot.createRound( - initialTimestamp + 3 days, - initialTimestamp + 4 days, - r2BaseCap, - address(0), - bytes(""), - false, - ILM_PC_FundingPot_v1.AccumulationMode.Total + fundingPot.createRound( + params.roundStart, + params.roundEnd, + params.roundCap, + params.hookContract, + params.hookFunction, + params.autoClosure, + params.accumulationMode ); + uint32 round2Id = fundingPot.getRoundCount(); + + // Set up access criteria for round 2 fundingPot.setAccessCriteria( - round2Id, accessId, 0, address(0), bytes32(0), new address[](0) + round2Id, + uint8(ILM_PC_FundingPot_v1.AccessCriteriaType.OPEN), // accessCriteriaType + 0, // accessCriteriaId (0 for new) + nftContract, + merkleRoot, + allowedAddresses ); - // Set personal cap for R2 to be at least the expected effective total cap - uint r2ExpectedEffectiveTotalCap = r2BaseCap + r1UnusedTotal; // 500 + 400 = 900 + + // Set a personal cap of 400 for round 2 fundingPot.setAccessCriteriaPrivileges( - round2Id, accessId, r2ExpectedEffectiveTotalCap, false, 0, 0, 0 + round2Id, accessCriteriaId, 400, false, 0, 0, 0 ); - // --- Verify Default Global Start Round ID --- + // First round contribution: user contributes 200 out of their 500 personal cap + vm.warp(_defaultRoundParams.roundStart + 1); + + vm.startPrank(contributor1_); + _token.approve(address(fundingPot), 1000); + fundingPot.contributeToRoundFor( + contributor1_, round1Id, 200, accessCriteriaId, new bytes32[](0) + ); + vm.stopPrank(); + + // Verify contribution to round 1 assertEq( - fundingPot.getGlobalAccumulationStartRoundId(), - 1, - "Default global start round ID should be 1" + fundingPot.getUserContributionToRound(round1Id, contributor1_), 200 ); - // --- Attempt Contribution in Round 2 by C1 --- - vm.warp(initialTimestamp + 3 days + 1 hours); + // Move to round 2 + vm.warp(_defaultRoundParams.roundStart + 3 days + 1); - uint c1AttemptR2 = r2ExpectedEffectiveTotalCap - 100; // e.g., 900 - 100 = 800. Utilizes expanded cap. - assertTrue( - c1AttemptR2 > r2BaseCap, "C1 R2 attempt should be > R2 base cap" - ); - assertTrue( - c1AttemptR2 <= r2ExpectedEffectiveTotalCap, - "C1 R2 attempt should be <= R2 effective cap" - ); + // ------------ PART 1: VERIFY PERSONAL CAP ACCUMULATION ------------ + // Create unspent capacity structure + ILM_PC_FundingPot_v1.UnspentPersonalRoundCap[] memory unspentCaps = + new ILM_PC_FundingPot_v1.UnspentPersonalRoundCap[](1); + unspentCaps[0] = ILM_PC_FundingPot_v1.UnspentPersonalRoundCap({ + roundId: round1Id, + accessCriteriaId: accessCriteriaId, + merkleProof: new bytes32[](0) + }); + // Try to contribute more than the round 2 personal cap (400) + // In Personal mode, this should succeed up to the personal cap (400) + unspent from round 1 (300) = 700 + // But capped by round cap of 500 vm.startPrank(contributor1_); fundingPot.contributeToRoundFor( - contributor1_, round2Id, c1AttemptR2, accessId, new bytes32[](0) + contributor1_, + round2Id, + 450, // More than the personal cap of round 2 + accessCriteriaId, + new bytes32[](0), + unspentCaps ); vm.stopPrank(); - // --- Assertions --- - assertEq( - fundingPot.getUserContributionToRound(round2Id, contributor1_), - c1AttemptR2, - "R2 C1 contribution incorrect" - ); - assertEq( - fundingPot.getTotalRoundContribution(round2Id), - c1AttemptR2, - "R2 Total contributions after C1 incorrect" - ); + // Verify contributions to round 2 - should be more than the personal cap of round 2 (400) + // This verifies personal caps DO accumulate + uint contributionAmount = + fundingPot.getUserContributionToRound(round2Id, contributor1_); + assertEq(contributionAmount, 450); + assertTrue(contributionAmount > 400, "Personal cap should accumulate"); - // Verify that the total contributions possible is indeed the effective cap - uint remainingToFill = r2ExpectedEffectiveTotalCap - c1AttemptR2; - if (remainingToFill > 0) { - vm.startPrank(contributor1_); - fundingPot.contributeToRoundFor( - contributor1_, - round2Id, - remainingToFill, - accessId, - new bytes32[](0) - ); - vm.stopPrank(); - } + // ------------ PART 2: VERIFY TOTAL CAP NON-ACCUMULATION ------------ + // Attempt to contribute more than the remaining round cap + vm.startPrank(contributor2_); + _token.approve(address(fundingPot), 200); + // Contributor 2 attempts to contribute 100. + // Since contributor1 contributed 450 and round cap is 500, only 50 is remaining. + // The contribution should be clamped to 50. + fundingPot.contributeToRoundFor( + contributor2_, round2Id, 100, accessCriteriaId, new bytes32[](0) + ); + // Verify contributor 2's contribution was clamped to the remaining 50. assertEq( - fundingPot.getTotalRoundContribution(round2Id), - r2ExpectedEffectiveTotalCap, - "R2 final total contributions should match effective total cap" + fundingPot.getUserContributionToRound(round2Id, contributor2_), 50 ); - } - - function testContribute_disabledMode_ignoresGlobalStartAndPreventsAccumulation( - ) public { - // SCENARIO: AccumulationMode.Disabled on a target round (R2) prevents any accumulation - // from a previous round (R1), even if globalAccumulationStartRoundId would allow it. - - uint initialTimestamp = block.timestamp; - uint8 accessId = 1; // Open access + vm.stopPrank(); - // --- Round 1 Parameters --- - uint r1PersonalCapC1 = 500; - uint r1ContributionC1 = 100; - uint r1BaseCap = 1000; + // Verify total contributions to round 2 is exactly the round cap (450 + 50 = 500). + assertEq(fundingPot.getTotalRoundContribution(round2Id), 500); - // --- Round 2 Parameters (Disabled Mode) --- - uint r2BasePersonalCapC1 = 50; - uint r2BaseCap = 200; + // Additional contributor3 should not be able to contribute anything as the cap is full. + // Attempting to contribute when the cap is already full should revert. + vm.startPrank(contributor3_); + _token.approve(address(fundingPot), 100); - // --- Approvals --- - vm.startPrank(contributor1_); - _token.approve(address(fundingPot), type(uint).max); + // Expect revert because the round cap (500) is already met. + vm.expectRevert( + abi.encodeWithSelector( + ILM_PC_FundingPot_v1 + .Module__LM_PC_FundingPot__RoundCapReached + .selector + ) + ); + fundingPot.contributeToRoundFor( + contributor3_, round2Id, 1, accessCriteriaId, new bytes32[](0) + ); vm.stopPrank(); - // --- Create Round 1 (Personal Mode to generate unused personal capacity) --- - uint32 round1Id = fundingPot.createRound( - initialTimestamp + 1 days, - initialTimestamp + 2 days, - r1BaseCap, - address(0), - bytes(""), - false, - ILM_PC_FundingPot_v1.AccumulationMode.Personal + // Final check that total contributions remain at the round cap. + assertEq(fundingPot.getTotalRoundContribution(round2Id), 500); + } + + function testContributeToRoundFor_totalModeOnlyAccumulatesTotalCaps() public { + // 1. Create the first round with AccumulationMode.Total + _defaultRoundParams.accumulationMode = + ILM_PC_FundingPot_v1.AccumulationMode.Total; + + fundingPot.createRound( + _defaultRoundParams.roundStart, + _defaultRoundParams.roundEnd, + 1000, // Round 1 cap of 1000 + _defaultRoundParams.hookContract, + _defaultRoundParams.hookFunction, + _defaultRoundParams.autoClosure, + _defaultRoundParams.accumulationMode + ); + uint32 round1Id = fundingPot.getRoundCount(); + + // Set up access criteria for round 1 (Open) + uint8 accessCriteriaId = 1; + ( + address nftContract, + bytes32 merkleRoot, + address[] memory allowedAddresses + ) = _helper_createAccessCriteria( + uint8(ILM_PC_FundingPot_v1.AccessCriteriaType.OPEN), round1Id ); + fundingPot.setAccessCriteria( - round1Id, accessId, 0, address(0), bytes32(0), new address[](0) + round1Id, + uint8(ILM_PC_FundingPot_v1.AccessCriteriaType.OPEN), // accessCriteriaType + 0, // accessCriteriaId (0 for new) + nftContract, + merkleRoot, + allowedAddresses ); + + // Set a personal cap of 800 for round 1 fundingPot.setAccessCriteriaPrivileges( - round1Id, accessId, r1PersonalCapC1, false, 0, 0, 0 + round1Id, accessCriteriaId, 800, false, 0, 0, 0 ); - // --- Contribution by C1 to Round 1 --- - vm.warp(initialTimestamp + 1 days + 1 hours); - vm.startPrank(contributor1_); - fundingPot.contributeToRoundFor( - contributor1_, - round1Id, - r1ContributionC1, - accessId, - new bytes32[](0) + // 2. Create the second round, also with AccumulationMode.Total + RoundParams memory params = _helper_createEditRoundParams( + _defaultRoundParams.roundStart + 3 days, + _defaultRoundParams.roundEnd + 3 days, + 500, // Round 2 base cap of 500 + _defaultRoundParams.hookContract, + _defaultRoundParams.hookFunction, + _defaultRoundParams.autoClosure, + ILM_PC_FundingPot_v1.AccumulationMode.Total ); - vm.stopPrank(); - // --- Create Round 2 (Disabled Mode) --- - uint32 round2Id = fundingPot.createRound( - initialTimestamp + 3 days, - initialTimestamp + 4 days, - r2BaseCap, - address(0), - bytes(""), - false, - ILM_PC_FundingPot_v1.AccumulationMode.Disabled + fundingPot.createRound( + params.roundStart, + params.roundEnd, + params.roundCap, + params.hookContract, + params.hookFunction, + params.autoClosure, + params.accumulationMode ); + uint32 round2Id = fundingPot.getRoundCount(); + + // Set up access criteria for round 2 (Open) fundingPot.setAccessCriteria( - round2Id, accessId, 0, address(0), bytes32(0), new address[](0) + round2Id, + uint8(ILM_PC_FundingPot_v1.AccessCriteriaType.OPEN), // accessCriteriaType + 0, // accessCriteriaId (0 for new) + nftContract, + merkleRoot, + allowedAddresses ); + + // Set a personal cap of 300 for round 2 fundingPot.setAccessCriteriaPrivileges( - round2Id, accessId, r2BasePersonalCapC1, false, 0, 0, 0 + round2Id, accessCriteriaId, 300, false, 0, 0, 0 ); - // --- Set Global Start Round ID to allow R1 (to show it's ignored by R2's Disabled mode) --- - fundingPot.setGlobalAccumulationStart(1); + // Round 1 contribution: contributor1 contributes 600 (less than round cap 1000, less than personal 800) + // Undersubscription: 1000 - 600 = 400 + vm.warp(_defaultRoundParams.roundStart + 1); + vm.startPrank(contributor1_); + _token.approve(address(fundingPot), 1000); + fundingPot.contributeToRoundFor( + contributor1_, round1Id, 600, accessCriteriaId, new bytes32[](0) + ); + vm.stopPrank(); + + // Verify contribution to round 1 assertEq( - fundingPot.getGlobalAccumulationStartRoundId(), - 1, - "Global start round ID should be 1" + fundingPot.getUserContributionToRound(round1Id, contributor1_), 600 ); + assertEq(fundingPot.getTotalRoundContribution(round1Id), 600); - // --- Attempt Contribution in Round 2 by C1 --- - vm.warp(initialTimestamp + 3 days + 1 hours); + // Move to round 2 + vm.warp(_defaultRoundParams.roundStart + 3 days + 1); - uint c1AttemptR2 = r2BasePersonalCapC1 + 100; + // ------------ PART 1: VERIFY TOTAL CAP ACCUMULATION ------------ + // Effective Round 2 Cap = Base Cap (500) + Unused from Round 1 (400) = 900 + vm.startPrank(contributor2_); + _token.approve(address(fundingPot), 1000); // Approve enough - ILM_PC_FundingPot_v1.UnspentPersonalRoundCap[] memory unspentCapsC1 = - new ILM_PC_FundingPot_v1.UnspentPersonalRoundCap[](1); - unspentCapsC1[0] = ILM_PC_FundingPot_v1.UnspentPersonalRoundCap( - round1Id, accessId, new bytes32[](0) + // Contributor 2 attempts to contribute 700. + // Personal Cap (R2) is 300. Gets clamped to 300. + fundingPot.contributeToRoundFor( + contributor2_, round2Id, 700, accessCriteriaId, new bytes32[](0) + ); + // Verify contributor 2's contribution was clamped by personal cap. + assertEq( + fundingPot.getUserContributionToRound(round2Id, contributor2_), + 300, + "C2 contribution should be clamped by personal cap" + ); + vm.stopPrank(); + + // Verify total contributions after C2 is 300 + assertEq( + fundingPot.getTotalRoundContribution(round2Id), + 300, + "Total after C2 should be 300" ); + // ------------ PART 2: VERIFY PERSONAL CAP NON-ACCUMULATION ------------ + // Contributor 1 had 800 personal cap in R1, contributed 600, unused = 200. + // Contributor 1 has 300 personal cap in R2. + // In Total mode, personal cap does NOT roll over. Max contribution is 300. + + // Prepare unspent caps struct (even though it shouldn't work for personal in Total mode) + ILM_PC_FundingPot_v1.UnspentPersonalRoundCap[] memory unspentCaps = + new ILM_PC_FundingPot_v1.UnspentPersonalRoundCap[](1); + unspentCaps[0] = ILM_PC_FundingPot_v1.UnspentPersonalRoundCap({ + roundId: round1Id, + accessCriteriaId: accessCriteriaId, + merkleProof: new bytes32[](0) + }); + vm.startPrank(contributor1_); + _token.approve(address(fundingPot), 500); + + // Attempt to contribute 400 ( > R2 personal cap 300) + // Total contributions = 300. Effective Round Cap = 900. Remaining Round Cap = 600. + // Personal Cap (R2) = 300. Unspent (R1) = 200, ignored in Total mode. + // Min(Remaining Round Cap, Remaining Personal Cap) = Min(600, 300) = 300. + // Should be clamped to 300. fundingPot.contributeToRoundFor( contributor1_, round2Id, - c1AttemptR2, - accessId, + 400, + accessCriteriaId, new bytes32[](0), - unspentCapsC1 + unspentCaps // Provide unspent caps, although they should be ignored for personal limit ); - vm.stopPrank(); - - // --- Assertions --- + // Verify contributor 1's contribution was clamped to their R2 personal cap. assertEq( fundingPot.getUserContributionToRound(round2Id, contributor1_), - r2BasePersonalCapC1, - "R2 C1 personal contribution should be clamped by R2's base personal cap (Disabled mode)" + 300, + "C1 contribution should be clamped by personal cap" ); + vm.stopPrank(); + + // Verify total round contributions: 300 (C2) + 300 (C1) = 600 assertEq( fundingPot.getTotalRoundContribution(round2Id), - r2BasePersonalCapC1, - "R2 Total contributions should not be expanded by R1 (Disabled mode)" + 600, + "Total after C1 and C2 should be 600" ); - assertTrue( - fundingPot.getTotalRoundContribution(round2Id) <= r2BaseCap, - "R2 Total contributions exceeded R2's original base cap (Disabled mode)" + // Effective cap 900, current total 600. Remaining = 300. + + // Contributor 3 contributes 300. Personal Cap = 300. Remaining Round Cap = 300. Should succeed. + vm.startPrank(contributor3_); + _token.approve(address(fundingPot), 300); + fundingPot.contributeToRoundFor( + contributor3_, round2Id, 300, accessCriteriaId, new bytes32[](0) ); - } + // Verify C3 contributed 300 + assertEq( + fundingPot.getUserContributionToRound(round2Id, contributor3_), + 300, + "C3 contributes remaining 300" + ); + vm.stopPrank(); - function testContribute_anyAccumulativeMode_noAccumulationWhenGlobalStartEqualsTargetRound( - ) public { - // SCENARIO: If globalAccumulationStartRoundId is set to the target round's ID (R2), - // no accumulation from any previous round (R1) occurs for R2, even if R2's mode would allow it. + // Total contributions should now be 900 (300 + 300 + 300), matching the effective cap. + assertEq( + fundingPot.getTotalRoundContribution(round2Id), + 900, + "Total should match effective cap after C3" + ); - uint initialTimestamp = block.timestamp; - uint8 accessId = 1; // Open access + // Now the effective cap is full. Try contributing 1 again. + vm.startPrank(contributor3_); // Can use C3 or another contributor + _token.approve(address(fundingPot), 1); - // --- Round 1 Parameters --- - uint r1PersonalCapC1 = 500; - uint r1ContributionC1 = 100; - uint r1BaseCap = 1000; + // Try contributing 1, expect revert as cap is full + vm.expectRevert( + abi.encodeWithSelector( + ILM_PC_FundingPot_v1 + .Module__LM_PC_FundingPot__RoundCapReached + .selector + ) + ); + fundingPot.contributeToRoundFor( + contributor3_, round2Id, 1, accessCriteriaId, new bytes32[](0) + ); + vm.stopPrank(); - // --- Round 2 Parameters (Mode that would normally allow accumulation, e.g., Personal) --- - uint r2BasePersonalCapC1 = 50; - uint r2BaseCap = 200; + // Final total check should remain 900 + assertEq( + fundingPot.getTotalRoundContribution(round2Id), + 900, + "Final total should be effective cap" + ); + } - // --- Approvals --- - vm.startPrank(contributor1_); - _token.approve(address(fundingPot), type(uint).max); - vm.stopPrank(); + // ------------------------------------------------------------------------- + // Test: closeRound() - // --- Create Round 1 (Personal Mode to generate unused personal capacity) --- - uint32 round1Id = fundingPot.createRound( - initialTimestamp + 1 days, - initialTimestamp + 2 days, - r1BaseCap, - address(0), - bytes(""), - false, - ILM_PC_FundingPot_v1.AccumulationMode.Personal - ); - fundingPot.setAccessCriteria( - round1Id, accessId, 0, address(0), bytes32(0), new address[](0) - ); - fundingPot.setAccessCriteriaPrivileges( - round1Id, accessId, r1PersonalCapC1, false, 0, 0, 0 - ); + /* + ├── Given user does not have FUNDING_POT_ADMIN_ROLE + │ └── When user attempts to close a round + │ └── Then it should revert with Module__CallerNotAuthorized + │ + ├── Given round does not exist + │ └── When user attempts to close the round + │ └── Then it should revert with Module__LM_PC_FundingPot__RoundNotCreated + │ + ├── Given hook execution fails + │ └── When user attempts to close the round + │ └── Then it should revert with Module__LM_PC_FundingPot__HookExecutionFailed + │ + ├── Given closure conditions are not met + │ └── When user attempts to close the round + │ └── Then it should revert with Module__LM_PC_FundingPot__ClosureConditionsNotMet + │ + ├── Given round has started but not ended + ├── Given round is already closed + │ └── When user attempts to close the round again + │ └── Then it should revert with Module__LM_PC_FundingPot__RoundHasEnded + │ + ├── Given round has started but not ended + │ └── And round cap has not been reached + │ └── And user has contributed successfully + │ └── When user attempts to close the round + │ └── Then it should not revert and round should be closed + │ └── And payment orders should be created correctly + │ + ├── Given round has ended (by time) + │ └── And user has contributed during active round + │ └── When user attempts to close the round + │ └── Then it should not revert and round should be closed + │ └── And payment orders should be created correctly + │ + ├── Given round cap has been reached + │ └── And user has contributed up to the cap + │ └── When user attempts to close the round + │ └── Then it should not revert and round should be closed + │ └── And payment orders should be created correctly + -── Given round cap has been reached + │ └── And the round is set up for autoclosure + │ └── And user has contributed up to the cap + │ └── Then it should not revert and round should be closed + │ └── And payment orders should be created correctly + └── Given multiple users contributed before round ended or cap reached + └── When round is closed + └── Then it should not revert and round should be closed + └── And payment orders should be created for all contributors + */ + function testCloseRound_revertsGivenUserIsNotFundingPotAdmin(address user_) + public + { + vm.assume(user_ != address(0) && user_ != address(this)); - // --- Contribution by C1 to Round 1 --- - vm.warp(initialTimestamp + 1 days + 1 hours); - vm.startPrank(contributor1_); - fundingPot.contributeToRoundFor( - contributor1_, - round1Id, - r1ContributionC1, - accessId, - new bytes32[](0) - ); - vm.stopPrank(); + testCreateRound(); + uint32 roundId = fundingPot.getRoundCount(); - // --- Create Round 2 (Personal Mode - would normally allow accumulation from R1) --- - uint32 round2Id = fundingPot.createRound( - initialTimestamp + 3 days, - initialTimestamp + 4 days, - r2BaseCap, - address(0), - bytes(""), - false, - ILM_PC_FundingPot_v1.AccumulationMode.Personal - ); - fundingPot.setAccessCriteria( - round2Id, accessId, 0, address(0), bytes32(0), new address[](0) + vm.startPrank(user_); + bytes32 roleId = _authorizer.generateRoleId( + address(fundingPot), fundingPot.FUNDING_POT_ADMIN_ROLE() ); - fundingPot.setAccessCriteriaPrivileges( - round2Id, accessId, r2BasePersonalCapC1, false, 0, 0, 0 + vm.expectRevert( + abi.encodeWithSelector( + IModule_v1.Module__CallerNotAuthorized.selector, roleId, user_ + ) ); + fundingPot.closeRound(roundId); + vm.stopPrank(); + } - // --- Set Global Start Round ID to be Round 2's ID --- - fundingPot.setGlobalAccumulationStart(round2Id); - assertEq( - fundingPot.getGlobalAccumulationStartRoundId(), - round2Id, - "Global start round ID not set to R2 ID" - ); + function testFuzzCloseRound_revertsGivenRoundDoesNotExist( + uint8 accessCriteriaEnum + ) public { + vm.assume(accessCriteriaEnum >= 0 && accessCriteriaEnum <= 4); - // --- Attempt Contribution in Round 2 by C1 --- - vm.warp(initialTimestamp + 3 days + 1 hours); + uint32 roundId = fundingPot.getRoundCount(); - uint c1AttemptR2 = r2BasePersonalCapC1 + 100; + ( + address nftContract, + bytes32 merkleRoot, + address[] memory allowedAddresses + ) = _helper_createAccessCriteria(accessCriteriaEnum, roundId); - ILM_PC_FundingPot_v1.UnspentPersonalRoundCap[] memory unspentCapsC1 = - new ILM_PC_FundingPot_v1.UnspentPersonalRoundCap[](1); - unspentCapsC1[0] = ILM_PC_FundingPot_v1.UnspentPersonalRoundCap( - round1Id, accessId, new bytes32[](0) + vm.expectRevert( + abi.encodeWithSelector( + ILM_PC_FundingPot_v1 + .Module__LM_PC_FundingPot__RoundNotCreated + .selector + ) ); + fundingPot.closeRound(roundId); + } - vm.startPrank(contributor1_); - fundingPot.contributeToRoundFor( - contributor1_, - round2Id, - c1AttemptR2, - accessId, - new bytes32[](0), - unspentCapsC1 - ); - vm.stopPrank(); + function testCloseRound_revertsGivenHookExecutionFails() public { + uint8 accessCriteriaId = + uint8(ILM_PC_FundingPot_v1.AccessCriteriaType.OPEN); - // --- Assertions --- - assertEq( - fundingPot.getUserContributionToRound(round2Id, contributor1_), - r2BasePersonalCapC1, - "R2 C1 personal contribution should be clamped by R2's base personal cap (global start = R2)" - ); - assertEq( - fundingPot.getTotalRoundContribution(round2Id), - r2BasePersonalCapC1, - "R2 Total contributions should not be expanded by R1 (global start = R2)" - ); - assertTrue( - fundingPot.getTotalRoundContribution(round2Id) <= r2BaseCap, - "R2 Total contributions exceeded R2's original base cap (global start = R2)" + uint32 roundId = fundingPot.createRound( + _defaultRoundParams.roundStart, + _defaultRoundParams.roundEnd, + _defaultRoundParams.roundCap, + address(failingHook), + abi.encodeWithSignature("executeHook()"), + _defaultRoundParams.autoClosure, + _defaultRoundParams.accumulationMode ); - } - // ------------------------------------------------------------------------- - // Test: Global Accumulation Start Round - Logic Integration - Remaining Tests - // (Covers multi-round accumulation where global start allows all previous) - // ------------------------------------------------------------------------- + ( + address nftContract, + bytes32 merkleRoot, + address[] memory allowedAddresses + ) = _helper_createAccessCriteria(accessCriteriaId, roundId); - function testContribute_personalMode_defaultGlobalStartAllowsAccumulationFromMultiplePreviousRounds( - ) public { - // SCENARIO: globalAccumulationStartRoundId = 1 allows personal cap accumulation from R1 AND R2 - // for contributions to R3, when all rounds are in Personal mode. - // 1. Setup: R1, R2, R3 in Personal mode. C1 makes partial contributions in R1 & R2. - // 2. Action: Verify globalAccumulationStartRoundId = 1. C1 contributes to R3. - // 3. Verification: C1's effective personal cap in R3 includes unused from R1 and R2. + fundingPot.setAccessCriteria( + roundId, + accessCriteriaId, + 0, + nftContract, + merkleRoot, + allowedAddresses + ); - uint initialTimestamp = block.timestamp; + fundingPot.setAccessCriteriaPrivileges(roundId, 0, 1000, false, 0, 0, 0); - // --- Round Parameters, Personal Caps, and Contributions for contributor1_ --- - uint r1PersonalCapC1 = 500; - uint r1ContributionC1 = 200; + vm.warp(_defaultRoundParams.roundEnd + 1); - uint r2PersonalCapC1 = 600; - uint r2ContributionC1 = 250; + vm.expectRevert( + ILM_PC_FundingPot_v1 + .Module__LM_PC_FundingPot__HookExecutionFailed + .selector + ); + fundingPot.closeRound(roundId); + } - uint r3BasePersonalCapC1 = 300; + function testCloseRound_revertsGivenClosureConditionsNotMet() public { + uint8 accessCriteriaId = + uint8(ILM_PC_FundingPot_v1.AccessCriteriaType.OPEN); - uint largeRoundCap = 1_000_000; + _helper_setupRoundWithAccessCriteria(accessCriteriaId); + uint32 roundId = fundingPot.getRoundCount(); - // --- Approvals --- - vm.startPrank(contributor1_); - _token.approve(address(fundingPot), type(uint).max); - vm.stopPrank(); + fundingPot.setAccessCriteriaPrivileges(roundId, 0, 1000, false, 0, 0, 0); - // --- Create Round 1 (Personal Mode) --- - uint32 round1Id = fundingPot.createRound( - initialTimestamp + 1 days, - initialTimestamp + 2 days, - largeRoundCap, - address(0), - bytes(""), - false, - ILM_PC_FundingPot_v1.AccumulationMode.Personal - ); - fundingPot.setAccessCriteria( - round1Id, 1, 0, address(0), bytes32(0), new address[](0) + vm.expectRevert( + ILM_PC_FundingPot_v1 + .Module__LM_PC_FundingPot__ClosureConditionsNotMet + .selector ); + fundingPot.closeRound(roundId); + } + + function testCloseRound_revertsGivenRoundHasAlreadyBeenClosed() public { + uint8 accessCriteriaId = + uint8(ILM_PC_FundingPot_v1.AccessCriteriaType.OPEN); + + _helper_setupRoundWithAccessCriteria(accessCriteriaId); + uint32 roundId = fundingPot.getRoundCount(); fundingPot.setAccessCriteriaPrivileges( - round1Id, 1, r1PersonalCapC1, false, 0, 0, 0 + roundId, accessCriteriaId, 1000, false, 0, 0, 0 ); - // --- Contribution by C1 to Round 1 --- - vm.warp(initialTimestamp + 1 days + 1 hours); + (uint roundStart,,,,,,) = fundingPot.getRoundGenericParameters(roundId); + + vm.warp(roundStart + 1); + vm.startPrank(contributor1_); + _token.approve(address(fundingPot), 1000); fundingPot.contributeToRoundFor( - contributor1_, round1Id, r1ContributionC1, 1, new bytes32[](0) + contributor1_, roundId, 1000, accessCriteriaId, new bytes32[](0) ); vm.stopPrank(); - assertEq( - fundingPot.getUserContributionToRound(round1Id, contributor1_), - r1ContributionC1 - ); - // --- Create Round 2 (Personal Mode) --- - uint32 round2Id = fundingPot.createRound( - initialTimestamp + 3 days, - initialTimestamp + 4 days, - largeRoundCap, - address(0), - bytes(""), - false, - ILM_PC_FundingPot_v1.AccumulationMode.Personal + fundingPot.closeRound(roundId); + vm.expectRevert( + ILM_PC_FundingPot_v1 + .Module__LM_PC_FundingPot__RoundHasEnded + .selector ); + fundingPot.closeRound(roundId); + } + + function testCloseRound_worksGivenRoundHasStartedButNotEnded() public { + testCreateRound(); + uint32 roundId = fundingPot.getRoundCount(); + + uint8 accessCriteriaId = 1; + uint8 accessType = uint8(ILM_PC_FundingPot_v1.AccessCriteriaType.OPEN); + ( + address nftContract, + bytes32 merkleRoot, + address[] memory allowedAddresses + ) = _helper_createAccessCriteria(accessType, roundId); + fundingPot.setAccessCriteria( - round2Id, 1, 0, address(0), bytes32(0), new address[](0) + roundId, accessType, 0, nftContract, merkleRoot, allowedAddresses ); fundingPot.setAccessCriteriaPrivileges( - round2Id, 1, r2PersonalCapC1, false, 0, 0, 0 + roundId, accessCriteriaId, 1000, false, 0, 0, 0 ); - // --- Contribution by C1 to Round 2 --- - vm.warp(initialTimestamp + 3 days + 1 hours); + // Warp to round start + (uint roundStart,,,,,,) = fundingPot.getRoundGenericParameters(roundId); + vm.warp(roundStart + 1); + + // Make a contribution vm.startPrank(contributor1_); + _token.approve(address(fundingPot), 1000); fundingPot.contributeToRoundFor( - contributor1_, round2Id, r2ContributionC1, 1, new bytes32[](0) + contributor1_, roundId, 1000, accessCriteriaId, new bytes32[](0) ); vm.stopPrank(); - assertEq( - fundingPot.getUserContributionToRound(round2Id, contributor1_), - r2ContributionC1 - ); - - // --- Create Round 3 (Personal Mode) --- - uint32 round3Id = fundingPot.createRound( - initialTimestamp + 5 days, - initialTimestamp + 6 days, - largeRoundCap, - address(0), - bytes(""), - false, - ILM_PC_FundingPot_v1.AccumulationMode.Personal - ); - fundingPot.setAccessCriteria( - round3Id, 1, 0, address(0), bytes32(0), new address[](0) - ); - fundingPot.setAccessCriteriaPrivileges( - round3Id, 1, r3BasePersonalCapC1, false, 0, 0, 0 - ); - - // --- Verify Global Start Round ID --- - assertEq( - fundingPot.getGlobalAccumulationStartRoundId(), - 1, - "Default global start round ID should be 1" - ); - // --- Attempt Contribution in Round 3 by C1 --- - vm.warp(initialTimestamp + 5 days + 1 hours); + // Close the round + fundingPot.closeRound(roundId); - ILM_PC_FundingPot_v1.UnspentPersonalRoundCap[] memory unspentCapsC1 = - new ILM_PC_FundingPot_v1.UnspentPersonalRoundCap[](2); - unspentCapsC1[0] = ILM_PC_FundingPot_v1.UnspentPersonalRoundCap( - round1Id, 1, new bytes32[](0) - ); - unspentCapsC1[1] = ILM_PC_FundingPot_v1.UnspentPersonalRoundCap( - round2Id, 1, new bytes32[](0) - ); + // Verify round is closed + assertEq(fundingPot.isRoundClosed(roundId), true); + } - uint expectedR3PersonalCapC1 = r3BasePersonalCapC1 - + (r1PersonalCapC1 - r1ContributionC1) - + (r2PersonalCapC1 - r2ContributionC1); + function testCloseRound_worksGivenRoundHasEnded() public { + testCreateRound(); + uint32 roundId = fundingPot.getRoundCount(); - uint c1AttemptR3 = expectedR3PersonalCapC1; + uint8 accessCriteriaId = 1; + uint8 accessType = uint8(ILM_PC_FundingPot_v1.AccessCriteriaType.OPEN); - vm.startPrank(contributor1_); - fundingPot.contributeToRoundFor( - contributor1_, - round3Id, - c1AttemptR3, - 1, - new bytes32[](0), - unspentCapsC1 - ); - vm.stopPrank(); + ( + address nftContract, + bytes32 merkleRoot, + address[] memory allowedAddresses + ) = _helper_createAccessCriteria(accessType, roundId); - // --- Assertions --- - assertEq( - fundingPot.getUserContributionToRound(round3Id, contributor1_), - expectedR3PersonalCapC1, - "R3 C1 personal contribution incorrect (should use R1 & R2 unused)" + fundingPot.setAccessCriteria( + roundId, accessType, 0, nftContract, merkleRoot, allowedAddresses ); - assertEq( - fundingPot.getTotalRoundContribution(round3Id), - expectedR3PersonalCapC1, - "R3 total contributions incorrect after C1" + fundingPot.setAccessCriteriaPrivileges( + roundId, accessCriteriaId, 1000, false, 0, 0, 0 ); + // Make a contribution + (uint roundStart, uint roundEnd,,,,,) = + fundingPot.getRoundGenericParameters(roundId); + vm.warp(roundStart + 1); + vm.startPrank(contributor1_); - _token.approve(address(fundingPot), 1); - vm.expectRevert( - abi.encodeWithSelector( - ILM_PC_FundingPot_v1 - .Module__LM_PC_FundingPot__PersonalCapReached - .selector - ) - ); + _token.approve(address(fundingPot), 500); fundingPot.contributeToRoundFor( - contributor1_, round3Id, 1, 1, new bytes32[](0), unspentCapsC1 + contributor1_, roundId, 500, accessCriteriaId, new bytes32[](0) ); vm.stopPrank(); - } - - function testContribute_totalMode_defaultGlobalStartAllowsAccumulationFromMultiplePreviousRounds( - ) public { - // SCENARIO: globalAccumulationStartRoundId = 1 allows accumulation from Round 1 AND Round 2 for Total mode - // 1. Setup: Round 1, Round 2, Round 3. Partial total contributions in R1 & R2. All in Total mode. - // 2. Action: setGlobalAccumulationStart(1) (or verify default). - // 3. Verification: For contributions to R3 (Total mode), unused total from R1 AND R2 rolls over, expanding R3's effective cap. - uint initialTimestamp = block.timestamp; + // Warp to after round end + vm.warp(roundEnd + 1); - // --- Round Parameters & Contributions --- - uint r1BaseCap = 1000; - uint r1ContributionC1 = 400; + // Close the round + fundingPot.closeRound(roundId); - uint r2BaseCap = 1200; - uint r2ContributionC2 = 700; + // Verify round is closed + assertEq(fundingPot.isRoundClosed(roundId), true); + } - uint r3BaseCap = 300; + function testCloseRound_worksGivenRoundCapHasBeenReached() public { + testCreateRound(); - // --- Approvals --- - vm.startPrank(contributor1_); - _token.approve(address(fundingPot), type(uint).max); - vm.stopPrank(); + uint32 roundId = fundingPot.getRoundCount(); + uint8 accessCriteriaId = 1; + uint8 accessType = uint8(ILM_PC_FundingPot_v1.AccessCriteriaType.NFT); - vm.startPrank(contributor2_); - _token.approve(address(fundingPot), type(uint).max); - vm.stopPrank(); + uint amount = 1000; - vm.startPrank(contributor3_); - _token.approve(address(fundingPot), type(uint).max); - vm.stopPrank(); + ( + address nftContract, + bytes32 merkleRoot, + address[] memory allowedAddresses + ) = _helper_createAccessCriteria(accessType, roundId); - // --- Create Round 1 (Total Mode) --- - uint32 round1Id = fundingPot.createRound( - initialTimestamp + 1 days, - initialTimestamp + 2 days, - r1BaseCap, - address(0), - bytes(""), - false, - ILM_PC_FundingPot_v1.AccumulationMode.Total - ); fundingPot.setAccessCriteria( - round1Id, 1, 0, address(0), bytes32(0), new address[](0) + roundId, accessType, 0, nftContract, merkleRoot, allowedAddresses ); fundingPot.setAccessCriteriaPrivileges( - round1Id, 1, r1BaseCap, false, 0, 0, 0 + roundId, accessCriteriaId, 1000, false, 0, 0, 0 ); - // --- Contribution by C1 to Round 1 --- - vm.warp(initialTimestamp + 1 days + 1 hours); - vm.startPrank(contributor1_); + mockNFTContract.mint(contributor1_); + + (uint roundStart,,,,,,) = fundingPot.getRoundGenericParameters(roundId); + vm.warp(roundStart + 1); + + // Approve + vm.prank(contributor1_); + _token.approve(address(fundingPot), 1000); + + vm.prank(contributor1_); fundingPot.contributeToRoundFor( - contributor1_, round1Id, r1ContributionC1, 1, new bytes32[](0) - ); - vm.stopPrank(); - assertEq( - fundingPot.getTotalRoundContribution(round1Id), r1ContributionC1 + contributor1_, roundId, amount, accessCriteriaId, new bytes32[](0) ); - // --- Create Round 2 (Total Mode) --- - uint32 round2Id = fundingPot.createRound( - initialTimestamp + 3 days, - initialTimestamp + 4 days, - r2BaseCap, - address(0), - bytes(""), - false, - ILM_PC_FundingPot_v1.AccumulationMode.Total - ); + assertEq(fundingPot.isRoundClosed(roundId), false); + fundingPot.closeRound(roundId); + assertEq(fundingPot.isRoundClosed(roundId), true); + } + + function testCloseRound_worksGivenRoundisAutoClosure() public { + testEditRound(); + + uint32 roundId = fundingPot.getRoundCount(); + uint8 accessCriteriaId = 1; + uint8 accessType = uint8(ILM_PC_FundingPot_v1.AccessCriteriaType.OPEN); + + uint amount = 2000; + + ( + address nftContract, + bytes32 merkleRoot, + address[] memory allowedAddresses + ) = _helper_createAccessCriteria(accessType, roundId); + fundingPot.setAccessCriteria( - round2Id, 1, 0, address(0), bytes32(0), new address[](0) + roundId, accessType, 0, nftContract, merkleRoot, allowedAddresses ); + fundingPot.setAccessCriteriaPrivileges( - round2Id, 1, r2BaseCap, false, 0, 0, 0 + roundId, accessCriteriaId, 2000, false, 0, 0, 0 ); + mockNFTContract.mint(contributor1_); - // --- Contribution by C2 to Round 2 --- - vm.warp(initialTimestamp + 3 days + 1 hours); - vm.startPrank(contributor2_); + (uint roundStart,,,,,,) = fundingPot.getRoundGenericParameters(roundId); + vm.warp(roundStart + 1); + + // Approve + vm.prank(contributor1_); + _token.approve(address(fundingPot), 2000); + + vm.prank(contributor1_); fundingPot.contributeToRoundFor( - contributor2_, round2Id, r2ContributionC2, 1, new bytes32[](0) - ); - vm.stopPrank(); - assertEq( - fundingPot.getTotalRoundContribution(round2Id), r2ContributionC2 + contributor1_, roundId, amount, accessCriteriaId, new bytes32[](0) ); - // --- Create Round 3 (Total Mode) --- - uint r3ExpectedEffectiveCap = r3BaseCap + (r1BaseCap - r1ContributionC1) - + (r2BaseCap - r2ContributionC2); - uint32 round3Id = fundingPot.createRound( - initialTimestamp + 5 days, - initialTimestamp + 6 days, - r3BaseCap, - address(0), - bytes(""), - false, - ILM_PC_FundingPot_v1.AccumulationMode.Total - ); - (address nftR3, bytes32 merkleR3, address[] memory allowedR3) = - _helper_createAccessCriteria(1, round3Id); + assertEq(fundingPot.isRoundClosed(roundId), true); + } - // TODO - fundingPot.setAccessCriteria(round3Id, 1, 0, nftR3, merkleR3, allowedR3); + function testCloseRound_worksWithMultipleContributors() public { + testCreateRound(); + uint32 roundId = fundingPot.getRoundCount(); + + // Set up access criteria + uint8 accessCriteriaId = 1; + uint8 accessType = uint8(ILM_PC_FundingPot_v1.AccessCriteriaType.OPEN); + + ( + address nftContract, + bytes32 merkleRoot, + address[] memory allowedAddresses + ) = _helper_createAccessCriteria(accessType, roundId); + + fundingPot.setAccessCriteria( + roundId, accessType, 0, nftContract, merkleRoot, allowedAddresses + ); fundingPot.setAccessCriteriaPrivileges( - round3Id, 1, r3ExpectedEffectiveCap, false, 0, 0, 0 + roundId, accessCriteriaId, 1000, false, 0, 0, 0 ); - assertEq( - fundingPot.getGlobalAccumulationStartRoundId(), - 1, - "Default global start round ID should be 1" + // Warp to round start + (uint roundStart,,,,,,) = fundingPot.getRoundGenericParameters(roundId); + vm.warp(roundStart + 1); + + // Multiple contributors + vm.startPrank(contributor1_); + _token.approve(address(fundingPot), 500); + fundingPot.contributeToRoundFor( + contributor1_, roundId, 500, accessCriteriaId, new bytes32[](0) ); + vm.stopPrank(); - // --- Attempt Contribution in Round 3 by C3 --- - vm.warp(initialTimestamp + 5 days + 1 hours); + vm.startPrank(contributor2_); + _token.approve(address(fundingPot), 200); + fundingPot.contributeToRoundFor( + contributor2_, roundId, 200, accessCriteriaId, new bytes32[](0) + ); + vm.stopPrank(); vm.startPrank(contributor3_); + _token.approve(address(fundingPot), 300); fundingPot.contributeToRoundFor( - contributor3_, round3Id, r3ExpectedEffectiveCap, 1, new bytes32[](0) + contributor3_, roundId, 300, accessCriteriaId, new bytes32[](0) ); vm.stopPrank(); - // --- Assertions --- - assertEq( - fundingPot.getTotalRoundContribution(round3Id), - r3ExpectedEffectiveCap, - "R3 total contributions should match effective cap with rollover from R1 and R2" - ); - assertEq( - fundingPot.getUserContributionToRound(round3Id, contributor3_), - r3ExpectedEffectiveCap, - "R3 C3 contribution incorrect" - ); + // Close the round + fundingPot.closeRound(roundId); + + // Verify round is closed + assertEq(fundingPot.isRoundClosed(roundId), true); + } + + //------------------------------------------------------------------------- + + /* Test createPaymentOrdersForContributorsBatch() + ├── Given round does not exist + │ └── When user attempts to create payment orders in batch + │ └── Then it should revert with Module__LM_PC_FundingPot__RoundNotCreated + │ + ├── Given round is not closed + │ └── When user attempts to create payment orders in batch + │ └── Then it should revert with Module__LM_PC_FundingPot__RoundNotClosed + │ + ├── Given start index is greater than the number of contributors + │ └── When user attempts to create payment orders in batch + │ └── Then it should revert with Module__LM_PC_FundingPot__InvalidBatchParameters + │ + ├── Given batch size is zero + │ └── When user attempts to create payment orders in batch + │ └── Then it should revert with Module__LM_PC_FundingPot__InvalidBatchParameters + │ + ├── Given user does not have FUNDING_POT_ADMIN_ROLE + │ └── Given the round is configured with autoClosure + │ └── When user attempts to create payment orders in batch + │ └── Then it should revert with Module__CallerNotAuthorized + │ + ├── Given a closed round with autoClosure + │ └── When user attempts to create payment orders in batch + │ └── Then it should not revert and payment orders should be created + │ └── And the payment orders should have correct token amounts + │ + ├── Given a closed round with manualClosure + │ └── When funding pot admin attempts to create payment orders in batch + │ └── Then it should not revert and payment orders should be created + │ └── And the payment orders should have correct token amounts + */ + + function testCreatePaymentOrdersForContributorsBatch_revertsGivenRoundDoesNotExist( + ) public { + uint32 nonExistentRoundId = 999; - vm.startPrank(contributor1_); - _token.approve(address(fundingPot), 1); vm.expectRevert( abi.encodeWithSelector( ILM_PC_FundingPot_v1 - .Module__LM_PC_FundingPot__RoundCapReached + .Module__LM_PC_FundingPot__RoundNotCreated .selector ) ); - fundingPot.contributeToRoundFor( - contributor1_, round3Id, 1, 1, new bytes32[](0) + fundingPot.createPaymentOrdersForContributorsBatch( + nonExistentRoundId, 1 ); - vm.stopPrank(); } - function testContribute_allMode_defaultGlobalStartAllowsPersonalCapAccumulationFromAllPrevious( + function testCreatePaymentOrdersForContributorsBatch_revertsGivenRoundIsNotClosed( ) public { - // SCENARIO: globalAccumulationStartRoundId = 1 allows personal cap - // accumulation from R1 to R2, when both are in All mode. (Simplified for stack) - - uint initialTimestamp = block.timestamp; - uint8 accessId = 1; - - // --- Round 1: Setup & C1 Contribution --- - uint r1PersonalCapC1 = 500; - uint r1ContributionC1 = 100; - uint32 round1Id = fundingPot.createRound( - initialTimestamp + 1 days, - initialTimestamp + 2 days, - 1000, - address(0), - bytes(""), - false, - ILM_PC_FundingPot_v1.AccumulationMode.All - ); - fundingPot.setAccessCriteria( - round1Id, accessId, 0, address(0), bytes32(0), new address[](0) - ); - fundingPot.setAccessCriteriaPrivileges( - round1Id, accessId, r1PersonalCapC1, false, 0, 0, 0 - ); + testContributeToRoundFor_worksGivenGenericConfigAndAccessCriteria(); + uint32 roundId = fundingPot.getRoundCount(); - vm.warp(initialTimestamp + 1 days + 1 hours); - vm.startPrank(contributor1_); - _token.approve(address(fundingPot), type(uint).max); - fundingPot.contributeToRoundFor( - contributor1_, - round1Id, - r1ContributionC1, - accessId, - new bytes32[](0) + vm.expectRevert( + abi.encodeWithSelector( + ILM_PC_FundingPot_v1 + .Module__LM_PC_FundingPot__RoundNotClosed + .selector + ) ); - vm.stopPrank(); - uint r1UnusedPersonalForC1 = r1PersonalCapC1 - r1ContributionC1; + fundingPot.createPaymentOrdersForContributorsBatch(roundId, 1); + } - // --- Round 2: Setup --- - uint r2BasePersonalCapC1 = 200; - uint32 round2Id = fundingPot.createRound( - initialTimestamp + 3 days, - initialTimestamp + 4 days, - 2000, - address(0), - bytes(""), - false, - ILM_PC_FundingPot_v1.AccumulationMode.All - ); - fundingPot.setAccessCriteria( - round2Id, accessId, 0, address(0), bytes32(0), new address[](0) - ); - fundingPot.setAccessCriteriaPrivileges( - round2Id, accessId, r2BasePersonalCapC1, false, 0, 0, 0 - ); + function testCreatePaymentOrdersForContributorsBatch_revertsGivenBatchSizeIsGreaterThanContributorCount( + ) public { + testCloseRound_worksWithMultipleContributors(); + uint32 roundId = fundingPot.getRoundCount(); - // --- Global Start ID Check --- - assertEq( - fundingPot.getGlobalAccumulationStartRoundId(), - 1, - "Default global start ID is 1" + vm.expectRevert( + abi.encodeWithSelector( + ILM_PC_FundingPot_v1 + .Module__LM_PC_FundingPot__InvalidBatchParameters + .selector + ) ); + fundingPot.createPaymentOrdersForContributorsBatch(roundId, 999); + } - // --- C1 Contribution to Round 2 (Testing Personal Cap Rollover) --- - vm.warp(initialTimestamp + 3 days + 1 hours); - uint expectedEffectivePersonalCapC1R2 = - r2BasePersonalCapC1 + r1UnusedPersonalForC1; - uint c1AttemptR2 = expectedEffectivePersonalCapC1R2 + 50; + function testCreatePaymentOrdersForContributorsBatch_revertsGivenBatchSizeIsZero( + ) public { + testCloseRound_worksWithMultipleContributors(); + uint32 roundId = fundingPot.getRoundCount(); - ILM_PC_FundingPot_v1.UnspentPersonalRoundCap[] memory unspentCapsC1 = - new ILM_PC_FundingPot_v1.UnspentPersonalRoundCap[](1); - unspentCapsC1[0] = ILM_PC_FundingPot_v1.UnspentPersonalRoundCap( - round1Id, accessId, new bytes32[](0) + vm.expectRevert( + abi.encodeWithSelector( + ILM_PC_FundingPot_v1 + .Module__LM_PC_FundingPot__InvalidBatchParameters + .selector + ) ); + fundingPot.createPaymentOrdersForContributorsBatch(roundId, 0); + } + + function testCreatePaymentOrdersForContributorsBatch_revertsGivenUserDoesNotHaveFundingPotAdminRole( + ) public { + testCloseRound_worksWithMultipleContributors(); + uint32 roundId = fundingPot.getRoundCount(); vm.startPrank(contributor1_); - fundingPot.contributeToRoundFor( - contributor1_, - round2Id, - c1AttemptR2, - accessId, - new bytes32[](0), - unspentCapsC1 + bytes32 roleId = _authorizer.generateRoleId( + address(fundingPot), fundingPot.FUNDING_POT_ADMIN_ROLE() ); - vm.stopPrank(); - - // --- Assertions --- - assertEq( - fundingPot.getUserContributionToRound(round2Id, contributor1_), - expectedEffectivePersonalCapC1R2, - "R2 C1 personal contribution incorrect" - ); - assertEq( - fundingPot.getTotalRoundContribution(round2Id), - expectedEffectivePersonalCapC1R2, - "R2 Total contributions incorrect" + vm.expectRevert( + abi.encodeWithSelector( + IModule_v1.Module__CallerNotAuthorized.selector, + roleId, + contributor1_ + ) ); + fundingPot.createPaymentOrdersForContributorsBatch(roundId, 1); + vm.stopPrank(); } - function testContribute_allMode_defaultGlobalStartAllowsTotalCapAccumulationFromAllPrevious( + function testCreatePaymentOrdersForContributorsBatch_worksGivenRoundIsAutoClosure( ) public { - // SCENARIO: globalAccumulationStartRoundId = 1 (default or set) allows total cap - // accumulation from R1 to R2, when both are in All mode. - - uint initialTimestamp = block.timestamp; - uint8 accessId = 1; // Open access + testCloseRound_worksGivenRoundisAutoClosure(); + uint32 roundId = fundingPot.getRoundCount(); - // --- Round 1 Parameters (All Mode) --- - uint r1BaseTotalCap = 1000; - uint r1C1PersonalCap = 800; - uint r1C1Contribution = 600; + fundingPot.createPaymentOrdersForContributorsBatch(roundId, 1); + assertEq(fundingPot.paymentOrders().length, 1); + } - // --- Round 2 Parameters (All Mode) --- - uint r2BaseTotalCap = 500; + function testCreatePaymentOrdersForContributorsBatch_worksGivenRoundIsManualClosure( + ) public { + testCloseRound_worksWithMultipleContributors(); + uint32 roundId = fundingPot.getRoundCount(); - // --- Approvals --- - vm.startPrank(contributor1_); - _token.approve(address(fundingPot), type(uint).max); - vm.stopPrank(); + fundingPot.createPaymentOrdersForContributorsBatch(roundId, 3); + assertEq(fundingPot.paymentOrders().length, 3); + } + // ------------------------------------------------------------------------- - // --- Create Round 1 (All Mode) --- - uint32 round1Id = fundingPot.createRound( - initialTimestamp + 1 days, - initialTimestamp + 2 days, - r1BaseTotalCap, - address(0), - bytes(""), - false, - ILM_PC_FundingPot_v1.AccumulationMode.All - ); - fundingPot.setAccessCriteria( - round1Id, accessId, 0, address(0), bytes32(0), new address[](0) - ); - fundingPot.setAccessCriteriaPrivileges( - round1Id, accessId, r1C1PersonalCap, false, 0, 0, 0 - ); + // Internal Functions + function testFuzz_validateAndAdjustCapsWithUnspentCap( + uint32 roundId_, + uint amount_, + uint8 accessCriteriaId_, + bool canOverrideContributionSpan_, + uint unspentPersonalCap_ + ) external { + vm.assume(roundId_ > 0 && roundId_ >= fundingPot.getRoundCount()); + vm.assume(amount_ <= 1000); + vm.assume(accessCriteriaId_ <= 4); + vm.assume(unspentPersonalCap_ >= 0); - // --- Contribution by C1 to Round 1 --- - vm.warp(initialTimestamp + 1 days + 1 hours); - vm.startPrank(contributor1_); - fundingPot.contributeToRoundFor( + try fundingPot.exposed_validateAndAdjustCapsWithUnspentCap( contributor1_, - round1Id, - r1C1Contribution, - accessId, - new bytes32[](0) - ); - vm.stopPrank(); - uint r1UnusedTotal = r1BaseTotalCap - r1C1Contribution; + roundId_, + amount_, + accessCriteriaId_, + canOverrideContributionSpan_, + unspentPersonalCap_ + ) returns (uint adjustedAmount) { + assertLe( + adjustedAmount, amount_, "Adjusted amount should be <= amount_" + ); + assertGe(adjustedAmount, 0, "Adjusted amount should be >= 0"); + } catch (bytes memory reason) { + bytes32 roundCapReachedSelector = keccak256( + abi.encodeWithSignature( + "Module__LM_PC_FundingPot__RoundCapReached()" + ) + ); + bytes32 personalCapReachedSelector = keccak256( + abi.encodeWithSignature( + "Module__LM_PC_FundingPot__PersonalCapReached()" + ) + ); - // --- Create Round 2 (All Mode) --- - uint32 round2Id = fundingPot.createRound( - initialTimestamp + 3 days, - initialTimestamp + 4 days, - r2BaseTotalCap, - address(0), - bytes(""), - false, - ILM_PC_FundingPot_v1.AccumulationMode.All - ); - fundingPot.setAccessCriteria( - round2Id, accessId, 0, address(0), bytes32(0), new address[](0) - ); - uint r2ExpectedEffectiveTotalCap = r2BaseTotalCap + r1UnusedTotal; - fundingPot.setAccessCriteriaPrivileges( - round2Id, accessId, r2ExpectedEffectiveTotalCap, false, 0, 0, 0 - ); + if (keccak256(reason) == roundCapReachedSelector) { + assertTrue( + !canOverrideContributionSpan_, + "Should not revert RoundCapReached when canOverrideContributionSpan is true" + ); + } else if (keccak256(reason) == personalCapReachedSelector) { + assertTrue(true, "Personal cap reached as expected"); + } else { + assertTrue(false, "Unexpected revert reason"); + } + } + } - // --- Ensure Global Start Round ID is 1 --- - assertEq( - fundingPot.getGlobalAccumulationStartRoundId(), - 1, - "Global start round ID should be 1 by default" - ); + function testFuzz_ValidTimes(uint start, uint cliff, uint end) public { + vm.assume(cliff <= type(uint).max - start); - // --- Attempt Contribution in Round 2 by C1 to fill effective total cap --- - vm.warp(initialTimestamp + 3 days + 1 hours); + bool isValid = fundingPot.exposed_validTimes(start, cliff, end); - uint c1AttemptR2 = r2ExpectedEffectiveTotalCap; + assertEq(isValid, start + cliff <= end); - vm.startPrank(contributor1_); - fundingPot.contributeToRoundFor( - contributor1_, round2Id, c1AttemptR2, accessId, new bytes32[](0) + if (start > end) { + assertFalse(isValid); + } + + if (start == end) { + assertEq(isValid, cliff == 0); + } + } + + // ------------------------------------------------------------------------- + // Test: _calculateUnusedCapacityFromPreviousRounds + + function test_calculateUnusedCapacityFromPreviousRounds() public { + // round 1 (no accumulation) + fundingPot.createRound( + _defaultRoundParams.roundStart, + _defaultRoundParams.roundEnd, + _defaultRoundParams.roundCap, + _defaultRoundParams.hookContract, + _defaultRoundParams.hookFunction, + _defaultRoundParams.autoClosure, + ILM_PC_FundingPot_v1.AccumulationMode.Disabled ); - vm.stopPrank(); - // --- Assertions --- - assertEq( - fundingPot.getUserContributionToRound(round2Id, contributor1_), - c1AttemptR2, - "R2 C1 contribution should match attempt (filling effective total cap)" + // round 2 (with accumulation) + fundingPot.createRound( + _defaultRoundParams.roundStart + 300, + _defaultRoundParams.roundEnd + 400, + _defaultRoundParams.roundCap, + _defaultRoundParams.hookContract, + _defaultRoundParams.hookFunction, + _defaultRoundParams.autoClosure, + ILM_PC_FundingPot_v1.AccumulationMode.All // globalAccumulativeCaps on ); - assertEq( - fundingPot.getTotalRoundContribution(round2Id), - r2ExpectedEffectiveTotalCap, - "R2 Total contributions should match effective total cap (All mode, global_start=1)" + + // round 3 + fundingPot.createRound( + _defaultRoundParams.roundStart + 500, + _defaultRoundParams.roundEnd + 600, + _defaultRoundParams.roundCap, + _defaultRoundParams.hookContract, + _defaultRoundParams.hookFunction, + _defaultRoundParams.autoClosure, + ILM_PC_FundingPot_v1.AccumulationMode.All ); + + // Calculate unused capacity + uint actualUnusedCapacity = + fundingPot.exposed_calculateUnusedCapacityFromPreviousRounds(3); + assertEq(actualUnusedCapacity, 1000); } - function testContribute_allMode_globalStartRestrictsPersonalCapAccumulationFromEarlierRounds( + // ------------------------------------------------------------------------- + // Test: _contributeToRoundFor() + + function testFuzz_contributeToRoundFor_revertsGivenInvalidAccessCriteria( + uint8 accessCriteriaEnum ) public { - // SCENARIO: globalAccumulationStartRoundId = 2 restricts personal cap accumulation - // from R1 for R2, when both are in All mode. + vm.assume(accessCriteriaEnum > 4); - uint initialTimestamp = block.timestamp; - uint8 accessId = 1; // Open access + testCreateRound(); + uint32 roundId = fundingPot.getRoundCount(); + (uint roundStart,,,,,,) = fundingPot.getRoundGenericParameters(roundId); + vm.warp(roundStart + 1); - // --- Round 1 Parameters (All Mode) --- - uint r1PersonalCapC1 = 500; - uint r1ContributionC1 = 100; - uint r1BaseTotalCap = 1000; + vm.prank(contributor1_); + _token.approve(address(fundingPot), 1000); - // --- Round 2 Parameters (All Mode) --- - uint r2BasePersonalCapC1 = 50; - uint r2BaseTotalCap = 1000; + vm.expectRevert( + abi.encodeWithSelector( + ILM_PC_FundingPot_v1 + .Module__LM_PC_FundingPot__InvalidAccessCriteriaId + .selector + ) + ); - // --- Approvals --- - vm.startPrank(contributor1_); - _token.approve(address(fundingPot), type(uint).max); - vm.stopPrank(); + fundingPot.exposed_contributeToRoundFor( + contributor1_, + roundId, + 1000, + accessCriteriaEnum, + new bytes32[](0), + 0 + ); + } + // ------------------------------------------------------------------------- + // Test: _checkRoundClosureConditions - // --- Create Round 1 (All Mode) --- - uint32 round1Id = fundingPot.createRound( - initialTimestamp + 1 days, - initialTimestamp + 2 days, - r1BaseTotalCap, - address(0), - bytes(""), - false, - ILM_PC_FundingPot_v1.AccumulationMode.All + function test_checkRoundClosureConditions_whenCapReached() public { + RoundParams memory params = _defaultRoundParams; + params.roundStart = block.timestamp + 1 days; + params.roundEnd = block.timestamp + 2 days; + params.roundCap = 1000; + + uint32 roundId = fundingPot.createRound( + params.roundStart, + params.roundEnd, + params.roundCap, + params.hookContract, + params.hookFunction, + params.autoClosure, + params.accumulationMode ); + + // Set access criteria and privileges + uint8 accessCriteriaId = 1; + uint8 accessType = uint8(ILM_PC_FundingPot_v1.AccessCriteriaType.OPEN); + + ( + address nftContract, + bytes32 merkleRoot, + address[] memory allowedAddresses + ) = _helper_createAccessCriteria(accessType, roundId); fundingPot.setAccessCriteria( - round1Id, accessId, 0, address(0), bytes32(0), new address[](0) + roundId, accessType, 0, nftContract, merkleRoot, allowedAddresses ); fundingPot.setAccessCriteriaPrivileges( - round1Id, accessId, r1PersonalCapC1, false, 0, 0, 0 + roundId, accessCriteriaId, 1000, false, 0, 0, 0 ); - // --- Contribution by C1 to Round 1 --- - vm.warp(initialTimestamp + 1 days + 1 hours); + vm.warp(params.roundStart + 1); vm.startPrank(contributor1_); + _token.approve(address(fundingPot), params.roundCap); fundingPot.contributeToRoundFor( contributor1_, - round1Id, - r1ContributionC1, - accessId, + roundId, + params.roundCap, + accessCriteriaId, new bytes32[](0) ); vm.stopPrank(); - // --- Create Round 2 (All Mode) --- - uint32 round2Id = fundingPot.createRound( - initialTimestamp + 3 days, - initialTimestamp + 4 days, - r2BaseTotalCap, - address(0), - bytes(""), - false, - ILM_PC_FundingPot_v1.AccumulationMode.All - ); - fundingPot.setAccessCriteria( - round2Id, accessId, 0, address(0), bytes32(0), new address[](0) - ); - fundingPot.setAccessCriteriaPrivileges( - round2Id, accessId, r2BasePersonalCapC1, false, 0, 0, 0 - ); + assertTrue(fundingPot.exposed_checkRoundClosureConditions(roundId)); + } - // --- Set Global Start Round ID to Round 2's ID --- - fundingPot.setGlobalAccumulationStart(round2Id); - assertEq( - fundingPot.getGlobalAccumulationStartRoundId(), - round2Id, - "Global start ID not set to R2 ID" - ); + function test_checkRoundClosureConditions_whenEndTimeReached() public { + RoundParams memory params = _defaultRoundParams; + params.roundStart = block.timestamp + 1 days; + params.roundEnd = block.timestamp + 2 days; + params.roundCap = 1000; - // --- Attempt Contribution in Round 2 by C1 --- - vm.warp(initialTimestamp + 3 days + 1 hours); + uint32 roundId = fundingPot.createRound( + params.roundStart, + params.roundEnd, + params.roundCap, + params.hookContract, + params.hookFunction, + params.autoClosure, + params.accumulationMode + ); - uint c1AttemptR2 = r2BasePersonalCapC1 + 100; + // Move time past end time + vm.warp(params.roundEnd + 1); + assertTrue(fundingPot.exposed_checkRoundClosureConditions(roundId)); + } - ILM_PC_FundingPot_v1.UnspentPersonalRoundCap[] memory unspentCapsC1 = - new ILM_PC_FundingPot_v1.UnspentPersonalRoundCap[](1); - unspentCapsC1[0] = ILM_PC_FundingPot_v1.UnspentPersonalRoundCap( - round1Id, accessId, new bytes32[](0) - ); + function test_checkRoundClosureConditions_whenNeitherConditionMet() + public + { + RoundParams memory params = _defaultRoundParams; + params.roundStart = block.timestamp + 1 days; + params.roundEnd = block.timestamp + 2 days; + params.roundCap = 1000; - vm.startPrank(contributor1_); - fundingPot.contributeToRoundFor( - contributor1_, - round2Id, - c1AttemptR2, - accessId, - new bytes32[](0), - unspentCapsC1 + uint32 roundId = fundingPot.createRound( + params.roundStart, + params.roundEnd, + params.roundCap, + params.hookContract, + params.hookFunction, + params.autoClosure, + params.accumulationMode ); - vm.stopPrank(); - // --- Assertions --- - assertEq( - fundingPot.getUserContributionToRound(round2Id, contributor1_), - r2BasePersonalCapC1, - "R2 C1 personal contribution should be clamped by R2 base personal cap (All mode, global_start=R2)" - ); - assertEq( - fundingPot.getTotalRoundContribution(round2Id), - r2BasePersonalCapC1, - "R2 Total contributions should be C1's clamped amount (All mode, global_start=R2)" - ); + assertFalse(fundingPot.exposed_checkRoundClosureConditions(roundId)); } - function testContribute_allMode_globalStartRestrictsTotalCapAccumulationFromEarlierRounds( - ) public { - // SCENARIO: globalAccumulationStartRoundId = 2 restricts total cap accumulation - // from R1 for R2, when both are in All mode. - - uint initialTimestamp = block.timestamp; - uint8 accessId = 1; // Open access - - // --- Round 1 Parameters (All Mode) --- - uint r1BaseTotalCap = 1000; - uint r1C1PersonalCap = 800; - uint r1C1Contribution = 400; + function test_checkRoundClosureConditions_withNoEndTime() public { + RoundParams memory params = _defaultRoundParams; + params.roundStart = block.timestamp + 1 days; + params.roundEnd = 0; // No end time + params.roundCap = 1000; - // --- Round 2 Parameters (All Mode) --- - uint r2BaseTotalCap = 200; + uint32 roundId = fundingPot.createRound( + params.roundStart, + params.roundEnd, + params.roundCap, + params.hookContract, + params.hookFunction, + params.autoClosure, + params.accumulationMode + ); - // --- Approvals --- - vm.startPrank(contributor1_); - _token.approve(address(fundingPot), type(uint).max); - vm.stopPrank(); + // Set access criteria and privileges + uint8 accessCriteriaId = 1; + uint8 accessType = uint8(ILM_PC_FundingPot_v1.AccessCriteriaType.OPEN); - // --- Create Round 1 (All Mode) --- - uint32 round1Id = fundingPot.createRound( - initialTimestamp + 1 days, - initialTimestamp + 2 days, - r1BaseTotalCap, - address(0), - bytes(""), - false, - ILM_PC_FundingPot_v1.AccumulationMode.All - ); + ( + address nftContract, + bytes32 merkleRoot, + address[] memory allowedAddresses + ) = _helper_createAccessCriteria(accessType, roundId); fundingPot.setAccessCriteria( - round1Id, accessId, 0, address(0), bytes32(0), new address[](0) + roundId, accessType, 0, nftContract, merkleRoot, allowedAddresses ); fundingPot.setAccessCriteriaPrivileges( - round1Id, accessId, r1C1PersonalCap, false, 0, 0, 0 + roundId, + accessCriteriaId, + 1000, + false, + 0, // no start + 0, // no cliff + 0 // no end ); - // --- Contribution by C1 to Round 1 --- - vm.warp(initialTimestamp + 1 days + 1 hours); + // Should be false initially + assertFalse(fundingPot.exposed_checkRoundClosureConditions(roundId)); + + // Should be true when cap is reached + vm.warp(params.roundStart + 1); vm.startPrank(contributor1_); + _token.approve(address(fundingPot), params.roundCap); fundingPot.contributeToRoundFor( contributor1_, - round1Id, - r1C1Contribution, - accessId, + roundId, + params.roundCap, + accessCriteriaId, new bytes32[](0) ); vm.stopPrank(); - // --- Create Round 2 (All Mode) --- - uint32 round2Id = fundingPot.createRound( - initialTimestamp + 3 days, - initialTimestamp + 4 days, - r2BaseTotalCap, - address(0), - bytes(""), - false, - ILM_PC_FundingPot_v1.AccumulationMode.All - ); - fundingPot.setAccessCriteria( - round2Id, accessId, 0, address(0), bytes32(0), new address[](0) + assertTrue(fundingPot.exposed_checkRoundClosureConditions(roundId)); + } + + function test_checkRoundClosureConditions_withNoCap() public { + RoundParams memory params = _defaultRoundParams; + params.roundStart = block.timestamp + 1 days; + params.roundEnd = block.timestamp + 2 days; + params.roundCap = 0; // No cap + + uint32 roundId = fundingPot.createRound( + params.roundStart, + params.roundEnd, + params.roundCap, + params.hookContract, + params.hookFunction, + params.autoClosure, + params.accumulationMode ); - fundingPot.setAccessCriteriaPrivileges( - round2Id, accessId, r2BaseTotalCap, false, 0, 0, 0 + + // Should be false before end time + assertFalse( + fundingPot.exposed_checkRoundClosureConditions(uint32(roundId)) ); - // --- Set Global Start Round ID to Round 2's ID --- - fundingPot.setGlobalAccumulationStart(round2Id); - assertEq( - fundingPot.getGlobalAccumulationStartRoundId(), - round2Id, - "Global start ID not set to R2 ID" + // Should be true after end time + vm.warp(params.roundEnd + 1); + assertTrue( + fundingPot.exposed_checkRoundClosureConditions(uint32(roundId)) ); + } - // --- Attempt Contribution in Round 2 by C1 --- - vm.warp(initialTimestamp + 3 days + 1 hours); + function test_closeRound_worksGivenCapReached() public { + testCreateRound(); - uint c1AttemptR2 = r2BaseTotalCap + 100; - - vm.startPrank(contributor1_); - fundingPot.contributeToRoundFor( - contributor1_, round2Id, c1AttemptR2, accessId, new bytes32[](0) - ); - vm.stopPrank(); - - // --- Assertions --- - assertEq( - fundingPot.getUserContributionToRound(round2Id, contributor1_), - r2BaseTotalCap, - "R2 C1 contribution should be clamped by R2 base total cap (All mode, global_start=R2)" - ); - assertEq( - fundingPot.getTotalRoundContribution(round2Id), - r2BaseTotalCap, - "R2 Total contributions should be R2 base total cap (All mode, global_start=R2)" - ); - } - - function testContributeToRoundFor_revertsGivenUnspentCapsRoundIdsNotStrictlyIncreasing( - ) public { - // Setup: Round 1 (Personal), Round 2 (Personal), Round 3 (Personal for contribution) - uint initialTimestamp = block.timestamp; - uint8 accessId = 1; // Open access - uint personalCap = 500; - uint roundCap = 10_000; + uint32 roundId = fundingPot.getRoundCount(); + uint8 accessCriteriaId = 1; + uint8 accessType = uint8(ILM_PC_FundingPot_v1.AccessCriteriaType.OPEN); - vm.startPrank(contributor1_); - _token.approve(address(fundingPot), type(uint).max); - vm.stopPrank(); + uint amount = 1000; - // Round 1 - uint32 round1Id = fundingPot.createRound( - initialTimestamp + 1 days, - initialTimestamp + 2 days, - roundCap, - address(0), - bytes(""), - false, - ILM_PC_FundingPot_v1.AccumulationMode.Personal - ); - fundingPot.setAccessCriteria( - round1Id, accessId, 0, address(0), bytes32(0), new address[](0) - ); - fundingPot.setAccessCriteriaPrivileges( - round1Id, accessId, personalCap, false, 0, 0, 0 - ); + ( + address nftContract, + bytes32 merkleRoot, + address[] memory allowedAddresses + ) = _helper_createAccessCriteria(accessType, roundId); - // Round 2 - uint32 round2Id = fundingPot.createRound( - initialTimestamp + 3 days, - initialTimestamp + 4 days, - roundCap, - address(0), - bytes(""), - false, - ILM_PC_FundingPot_v1.AccumulationMode.Personal - ); fundingPot.setAccessCriteria( - round2Id, accessId, 0, address(0), bytes32(0), new address[](0) + roundId, accessType, 0, nftContract, merkleRoot, allowedAddresses ); fundingPot.setAccessCriteriaPrivileges( - round2Id, accessId, personalCap, false, 0, 0, 0 + roundId, accessCriteriaId, 1000, false, 0, 0, 0 ); - // Round 3 (target for contribution) - uint32 round3Id = fundingPot.createRound( - initialTimestamp + 5 days, - initialTimestamp + 6 days, - roundCap, - address(0), - bytes(""), - false, - ILM_PC_FundingPot_v1.AccumulationMode.Personal - ); - fundingPot.setAccessCriteria( - round3Id, accessId, 0, address(0), bytes32(0), new address[](0) - ); - fundingPot.setAccessCriteriaPrivileges( - round3Id, accessId, personalCap, false, 0, 0, 0 - ); + mockNFTContract.mint(contributor1_); - vm.warp(initialTimestamp + 5 days + 1 hours); // Enter Round 3 + (uint roundStart,,,,,,) = fundingPot.getRoundGenericParameters(roundId); + vm.warp(roundStart + 1); - // Case 1: Out of order - ILM_PC_FundingPot_v1.UnspentPersonalRoundCap[] memory - unspentCapsOutOfOrder = - new ILM_PC_FundingPot_v1.UnspentPersonalRoundCap[](2); - unspentCapsOutOfOrder[0] = ILM_PC_FundingPot_v1.UnspentPersonalRoundCap( - round2Id, accessId, new bytes32[](0) - ); - unspentCapsOutOfOrder[1] = ILM_PC_FundingPot_v1.UnspentPersonalRoundCap( - round1Id, accessId, new bytes32[](0) - ); + // Approve + vm.prank(contributor1_); + _token.approve(address(fundingPot), 1000); - vm.startPrank(contributor1_); - vm.expectRevert( - ILM_PC_FundingPot_v1 - .Module__LM_PC_FundingPot__UnspentCapsRoundIdsNotStrictlyIncreasing - .selector - ); + vm.prank(contributor1_); fundingPot.contributeToRoundFor( - contributor1_, - round3Id, - 100, - accessId, - new bytes32[](0), - unspentCapsOutOfOrder + contributor1_, roundId, amount, accessCriteriaId, new bytes32[](0) ); - vm.stopPrank(); - // Case 2: Duplicate roundId - ILM_PC_FundingPot_v1.UnspentPersonalRoundCap[] memory - unspentCapsDuplicate = - new ILM_PC_FundingPot_v1.UnspentPersonalRoundCap[](2); - unspentCapsDuplicate[0] = ILM_PC_FundingPot_v1.UnspentPersonalRoundCap( - round1Id, accessId, new bytes32[](0) + assertTrue( + fundingPot.exposed_checkRoundClosureConditions(uint32(roundId)) ); - unspentCapsDuplicate[1] = ILM_PC_FundingPot_v1.UnspentPersonalRoundCap( - round1Id, accessId, new bytes32[](0) + + uint startIndex = 0; + uint batchSize = 1; + fundingPot.exposed_closeRound(uint32(roundId)); + fundingPot.exposed_buyBondingCurveToken(uint32(roundId)); + fundingPot.exposed_createPaymentOrdersForContributors( + uint32(roundId), startIndex, batchSize ); - vm.startPrank(contributor1_); + assertTrue(fundingPot.isRoundClosed(roundId)); + } + + // ------------------------------------------------------------------------- + // Test: _buyBondingCurveToken + function test_buyBondingCurveToken_revertsGivenNoContributions() public { + testCreateRound(); + uint32 roundId = fundingPot.getRoundCount(); + (uint roundStart,,,,,,) = fundingPot.getRoundGenericParameters(roundId); + vm.warp(roundStart + 1); + vm.expectRevert( ILM_PC_FundingPot_v1 - .Module__LM_PC_FundingPot__UnspentCapsRoundIdsNotStrictlyIncreasing + .Module__LM_PC_FundingPot__NoContributions .selector ); - fundingPot.contributeToRoundFor( - contributor1_, - round3Id, - 100, - accessId, - new bytes32[](0), - unspentCapsDuplicate - ); - vm.stopPrank(); + fundingPot.exposed_buyBondingCurveToken(roundId); + } + // ------------------------------------------------------------------------- + // Helper Functions - // Case 3: Correct order but first element's roundId is 0 (if lastSeenRoundId starts at 0) - // This specific case won't be hit if round IDs must be >0, but good to be aware. - // Assuming valid round IDs start from 1, this case might not be directly testable if 0 isn't a valid roundId. - // The current check `currentProcessingRoundId <= lastSeenRoundId` covers this if roundId can be 0. - // If round IDs are always >= 1, then an initial lastSeenRoundId=0 is fine. + // @notice Creates edit round parameters with customizable values + function _helper_createEditRoundParams( + uint roundStart_, + uint roundEnd_, + uint roundCap_, + address hookContract_, + bytes memory hookFunction_, + bool autoClosure_, + ILM_PC_FundingPot_v1.AccumulationMode accumulationMode_ + ) internal pure returns (RoundParams memory) { + return RoundParams({ + roundStart: roundStart_, + roundEnd: roundEnd_, + roundCap: roundCap_, + hookContract: hookContract_, + hookFunction: hookFunction_, + autoClosure: autoClosure_, + accumulationMode: accumulationMode_ + }); + } - // Case 4: Empty array (should not revert with this specific error, but pass) - ILM_PC_FundingPot_v1.UnspentPersonalRoundCap[] memory unspentCapsEmpty = - new ILM_PC_FundingPot_v1.UnspentPersonalRoundCap[](0); - vm.startPrank(contributor1_); - fundingPot.contributeToRoundFor( // This should pass (or revert with a different error if amount is 0 etc.) - contributor1_, - round3Id, - 100, - accessId, - new bytes32[](0), - unspentCapsEmpty + function _helper_generateMerkleTreeForTwoLeaves( + address contributorA, + address contributorB, + uint32 roundId + ) + internal + pure + returns ( + bytes32 root, + bytes32 leafA, + bytes32 leafB, + bytes32[] memory proofA, + bytes32[] memory proofB + ) + { + leafA = keccak256(abi.encodePacked(contributorA, roundId)); + leafB = keccak256(abi.encodePacked(contributorB, roundId)); + + proofA = new bytes32[](1); + proofB = new bytes32[](1); + + // Ensure consistent ordering for root calculation + if (leafA < leafB) { + root = keccak256(abi.encodePacked(leafA, leafB)); + proofA[0] = leafB; // Proof for A is B + proofB[0] = leafA; // Proof for B is A + } else { + root = keccak256(abi.encodePacked(leafB, leafA)); + proofA[0] = leafB; // Proof for A is still B + proofB[0] = leafA; // Proof for B is still A + } + } + + function _helper_createAccessCriteria( + uint8 accessCriteriaEnum, + uint32 roundId + ) + internal + view + returns ( + address nftContract_, + bytes32 merkleRoot_, + address[] memory allowedAddresses_ + ) + { + { + if ( + accessCriteriaEnum + == uint8(ILM_PC_FundingPot_v1.AccessCriteriaType.OPEN) + ) { + nftContract_ = address(0x0); + merkleRoot_ = bytes32(uint(0x0)); + allowedAddresses_ = new address[](0); + } else if ( + accessCriteriaEnum + == uint8(ILM_PC_FundingPot_v1.AccessCriteriaType.NFT) + ) { + address nftContract = address(mockNFTContract); + + nftContract_ = nftContract; + merkleRoot_ = bytes32(uint(0x0)); + allowedAddresses_ = new address[](0); + } else if ( + accessCriteriaEnum + == uint8(ILM_PC_FundingPot_v1.AccessCriteriaType.MERKLE) + ) { + (bytes32 merkleRoot,,,,) = + _helper_generateMerkleTreeForTwoLeaves( + contributor1_, contributor2_, roundId + ); + + nftContract_ = address(0x0); + merkleRoot_ = merkleRoot; + allowedAddresses_ = new address[](0); + } else if ( + accessCriteriaEnum + == uint8(ILM_PC_FundingPot_v1.AccessCriteriaType.LIST) + ) { + address[] memory allowedAddresses = new address[](4); + allowedAddresses[0] = address(this); + allowedAddresses[1] = address(0x2); + allowedAddresses[2] = address(0x3); + allowedAddresses[3] = contributor2_; + nftContract_ = address(0x0); + merkleRoot_ = bytes32(uint(0x0)); + allowedAddresses_ = allowedAddresses; + } + } + } + + // Helper function to set up a round with access criteria + function _helper_setupRoundWithAccessCriteria(uint8 accessCriteriaEnum) + internal + { + uint32 roundId = fundingPot.createRound( + _defaultRoundParams.roundStart, + _defaultRoundParams.roundEnd, + _defaultRoundParams.roundCap, + _defaultRoundParams.hookContract, + _defaultRoundParams.hookFunction, + _defaultRoundParams.autoClosure, + _defaultRoundParams.accumulationMode ); - vm.stopPrank(); - assertEq( - fundingPot.getUserContributionToRound(round3Id, contributor1_), 100 + + ( + address nftContract, + bytes32 merkleRoot, + address[] memory allowedAddresses + ) = _helper_createAccessCriteria(accessCriteriaEnum, roundId); + + fundingPot.setAccessCriteria( + roundId, + accessCriteriaEnum, + 0, + nftContract, + merkleRoot, + allowedAddresses ); } } From 198f7338856c80f28b7b3cfc3e28830a7f17e686 Mon Sep 17 00:00:00 2001 From: Fabian Scherer Date: Mon, 19 May 2025 11:39:23 +0200 Subject: [PATCH 120/130] chore: fmt --- test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol b/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol index a308c3de2..489fc16e5 100644 --- a/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol +++ b/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol @@ -3888,7 +3888,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { ); } - function testContributeToRoundFor_personalModeOnlyAccumulatesPersonalCaps() + function testContributeToRoundFor_personalModeOnlyAccumulatesPersonalCaps() public { // 1. Create the first round with AccumulationMode.Personal @@ -4059,7 +4059,9 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { assertEq(fundingPot.getTotalRoundContribution(round2Id), 500); } - function testContributeToRoundFor_totalModeOnlyAccumulatesTotalCaps() public { + function testContributeToRoundFor_totalModeOnlyAccumulatesTotalCaps() + public + { // 1. Create the first round with AccumulationMode.Total _defaultRoundParams.accumulationMode = ILM_PC_FundingPot_v1.AccumulationMode.Total; From fb1ad69d11af49edb5bb0d6599472a634071720b Mon Sep 17 00:00:00 2001 From: Fabian Scherer Date: Mon, 19 May 2025 22:39:23 +0200 Subject: [PATCH 121/130] chore: removes redundant code --- .../logicModule/LM_PC_FundingPot_v1.sol | 45 +++++++------------ 1 file changed, 15 insertions(+), 30 deletions(-) diff --git a/src/modules/logicModule/LM_PC_FundingPot_v1.sol b/src/modules/logicModule/LM_PC_FundingPot_v1.sol index 280ff2d69..1a77ed7ae 100644 --- a/src/modules/logicModule/LM_PC_FundingPot_v1.sol +++ b/src/modules/logicModule/LM_PC_FundingPot_v1.sol @@ -1007,13 +1007,13 @@ contract LM_PC_FundingPot_v1 is Round storage round = rounds[roundId_]; + // --- Round Cap Check --- if (!canOverrideContributionSpan_ && round.roundCap > 0) { uint totalRoundContribution = roundIdToTotalContributions[roundId_]; uint effectiveRoundCap = round.roundCap; // If total accumulative caps are enabled for this round, // adjust the effective round cap to accommodate unused capacity from previous rounds - // NOTE: This part is relevant for Total/All modes, but the check itself is needed here. if ( round.accumulationMode == AccumulationMode.Total || round.accumulationMode == AccumulationMode.All @@ -1024,12 +1024,8 @@ contract LM_PC_FundingPot_v1 is } if (totalRoundContribution >= effectiveRoundCap) { - // If user tries to contribute a non-zero amount when cap is full, revert. - if (amount_ > 0) { - revert Module__LM_PC_FundingPot__RoundCapReached(); - } - // If user tries to contribute zero when cap is full, allow adjustedAmount = 0. - adjustedAmount = 0; + // If round cap is reached, revert (we know amount_ > 0 from parent function) + revert Module__LM_PC_FundingPot__RoundCapReached(); } else { // Cap is not full, calculate remaining and clamp if necessary uint remainingRoundCap = @@ -1041,7 +1037,7 @@ contract LM_PC_FundingPot_v1 is } // --- Personal Cap Check --- - // Only proceed if adjustedAmount wasn't already set to 0 by round cap or initial amount. + // Skip personal cap check if adjustedAmount is already 0 if (adjustedAmount > 0) { uint userPreviousContribution = roundIdToUserToContribution[roundId_][user_]; @@ -1050,8 +1046,7 @@ contract LM_PC_FundingPot_v1 is roundIdToAccessCriteriaIdToPrivileges[roundId_][accessCriteriaId_]; uint userPersonalCap = privileges.personalCap; - // Add unspent personal capacity if personal accumulation is enabled for this round (Personal or All) - // Explicitly exclude Total mode here. + // Add unspent personal capacity if personal accumulation is enabled for this round if ( round.accumulationMode == AccumulationMode.Personal || round.accumulationMode == AccumulationMode.All @@ -1059,28 +1054,18 @@ contract LM_PC_FundingPot_v1 is userPersonalCap += unspentPersonalCap_; } - // Check if the already potentially-clamped amount exceeds personal cap - if (userPreviousContribution + adjustedAmount > userPersonalCap) { - // If user hasn't reached personal cap yet, clamp further to remaining personal cap. - if (userPreviousContribution < userPersonalCap) { - uint remainingPersonalCap = - userPersonalCap - userPreviousContribution; - // Ensure we don't accidentally increase amount, only clamp down. - if (remainingPersonalCap < adjustedAmount) { - adjustedAmount = remainingPersonalCap; - } - } else { - // User is already at or over personal cap. - // If they tried to contribute a non-zero amount initially, revert. - if (amount_ > 0) { - revert Module__LM_PC_FundingPot__PersonalCapReached(); - } - // If initial amount was 0, just ensure adjustedAmount remains 0. - adjustedAmount = 0; - } + // If user already reached their cap, revert + if (userPreviousContribution >= userPersonalCap) { + revert Module__LM_PC_FundingPot__PersonalCapReached(); + } + + // Calculate remaining personal cap and take minimum + uint remainingPersonalCap = + userPersonalCap - userPreviousContribution; + if (remainingPersonalCap < adjustedAmount) { + adjustedAmount = remainingPersonalCap; } } - // --- End Personal Cap Check --- ' return adjustedAmount; } From e29de8586d690d7b6bd193c8cc4e3d559d29da8a Mon Sep 17 00:00:00 2001 From: Fabian Scherer Date: Mon, 19 May 2025 22:44:39 +0200 Subject: [PATCH 122/130] fix: compiler warnings --- src/modules/logicModule/LM_PC_FundingPot_v1.sol | 3 +-- .../modules/fundingManager/FundingManagerV1Mock.sol | 4 ++-- test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol | 9 ++------- 3 files changed, 5 insertions(+), 11 deletions(-) diff --git a/src/modules/logicModule/LM_PC_FundingPot_v1.sol b/src/modules/logicModule/LM_PC_FundingPot_v1.sol index 1a77ed7ae..2e73c7fd0 100644 --- a/src/modules/logicModule/LM_PC_FundingPot_v1.sol +++ b/src/modules/logicModule/LM_PC_FundingPot_v1.sol @@ -171,11 +171,10 @@ contract LM_PC_FundingPot_v1 is /// @dev MUST call `__Module_init()`. /// @param orchestrator_ The orchestrator contract. /// @param metadata_ The metadata of the module. - /// @param configData_ The config data of the module, comprised of: function init( IOrchestrator_v1 orchestrator_, Metadata memory metadata_, - bytes memory configData_ + bytes memory ) external override(Module_v1) initializer { __Module_init(orchestrator_, metadata_); // Set the flags for the PaymentOrders (this module uses 3 flags). diff --git a/test/mocks/modules/fundingManager/FundingManagerV1Mock.sol b/test/mocks/modules/fundingManager/FundingManagerV1Mock.sol index 7c6668b28..2071c4348 100644 --- a/test/mocks/modules/fundingManager/FundingManagerV1Mock.sol +++ b/test/mocks/modules/fundingManager/FundingManagerV1Mock.sol @@ -83,11 +83,11 @@ contract FundingManagerV1Mock is IFundingManager_v1, Module_v1 { return address(_bondingToken); } - function calculatePurchaseReturn(uint amount) public view returns (uint) { + function calculatePurchaseReturn(uint amount) public pure returns (uint) { return amount; } - function buyFor(address to, uint amount, uint minTokens) public { + function buyFor(address to, uint amount, uint) public { _token.transferFrom(_msgSender(), address(this), amount); _bondingToken.mint(to, amount); } diff --git a/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol b/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol index 489fc16e5..bc05b537f 100644 --- a/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol +++ b/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol @@ -1829,7 +1829,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { && accessCriteriaEnumNew >= 0 && accessCriteriaEnumNew <= 4 ); uint8 accessCriteriaId = 1; - uint8 accessType = uint8(ILM_PC_FundingPot_v1.AccessCriteriaType.NFT); + _helper_setupRoundWithAccessCriteria(accessCriteriaId); uint32 roundId = fundingPot.getRoundCount(); @@ -2315,7 +2315,6 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { uint r2PersonalCap = 600; uint r2Contribution = 200; - uint r2UnusedPersonal = r2PersonalCap - r2Contribution; // This IS used for R3 calculation uint r3BasePersonalCap = 300; @@ -4355,11 +4354,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { uint32 roundId = fundingPot.getRoundCount(); - ( - address nftContract, - bytes32 merkleRoot, - address[] memory allowedAddresses - ) = _helper_createAccessCriteria(accessCriteriaEnum, roundId); + _helper_createAccessCriteria(accessCriteriaEnum, roundId); vm.expectRevert( abi.encodeWithSelector( From 240f52e5431a616db62061a64660a93cc7f4a6a8 Mon Sep 17 00:00:00 2001 From: Fabian Scherer Date: Thu, 22 May 2025 10:38:04 +0200 Subject: [PATCH 123/130] chore: remove redundant check on zero contribution --- src/modules/logicModule/LM_PC_FundingPot_v1.sol | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/modules/logicModule/LM_PC_FundingPot_v1.sol b/src/modules/logicModule/LM_PC_FundingPot_v1.sol index 2e73c7fd0..0ccf7fdb4 100644 --- a/src/modules/logicModule/LM_PC_FundingPot_v1.sol +++ b/src/modules/logicModule/LM_PC_FundingPot_v1.sol @@ -1036,8 +1036,7 @@ contract LM_PC_FundingPot_v1 is } // --- Personal Cap Check --- - // Skip personal cap check if adjustedAmount is already 0 - if (adjustedAmount > 0) { + { uint userPreviousContribution = roundIdToUserToContribution[roundId_][user_]; From 2a5371f6ccd93b92c73bdcd4be6a2398dfba73d4 Mon Sep 17 00:00:00 2001 From: Fabian Scherer Date: Thu, 22 May 2025 10:38:16 +0200 Subject: [PATCH 124/130] chore: fmt --- test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol b/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol index bc05b537f..a39cdb8ab 100644 --- a/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol +++ b/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol @@ -1829,7 +1829,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { && accessCriteriaEnumNew >= 0 && accessCriteriaEnumNew <= 4 ); uint8 accessCriteriaId = 1; - + _helper_setupRoundWithAccessCriteria(accessCriteriaId); uint32 roundId = fundingPot.getRoundCount(); From 6d3384b0f4794448c77663bc28285bb25ea16abe Mon Sep 17 00:00:00 2001 From: Fabian Scherer Date: Thu, 22 May 2025 14:22:49 +0200 Subject: [PATCH 125/130] fix: `>=` instead of `==` --- src/modules/logicModule/LM_PC_FundingPot_v1.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/modules/logicModule/LM_PC_FundingPot_v1.sol b/src/modules/logicModule/LM_PC_FundingPot_v1.sol index 0ccf7fdb4..fb3e9c121 100644 --- a/src/modules/logicModule/LM_PC_FundingPot_v1.sol +++ b/src/modules/logicModule/LM_PC_FundingPot_v1.sol @@ -1402,7 +1402,7 @@ contract LM_PC_FundingPot_v1 is Round storage round = rounds[roundId_]; uint totalContribution = roundIdToTotalContributions[roundId_]; bool capReached = - round.roundCap > 0 && totalContribution == round.roundCap; + round.roundCap > 0 && totalContribution >= round.roundCap; bool timeEnded = round.roundEnd > 0 && block.timestamp >= round.roundEnd; return capReached || timeEnded; } From 8dda04fe003d42c4eaf0de31ea3806747ccc6166 Mon Sep 17 00:00:00 2001 From: Fabian Scherer Date: Thu, 22 May 2025 17:31:47 +0200 Subject: [PATCH 126/130] fix: merge conflicts --- test/e2e/logicModule/FundingPotE2E.t.sol | 11 +++++----- .../fundingManager/FundingManagerV1Mock.sol | 3 ++- .../logicModule/LM_PC_FundingPot_v1.t.sol | 10 +++++----- .../logicModule/PP_FundingPot_v1_Exposed.sol | 20 ------------------- 4 files changed, 13 insertions(+), 31 deletions(-) delete mode 100644 test/unit/modules/logicModule/PP_FundingPot_v1_Exposed.sol diff --git a/test/e2e/logicModule/FundingPotE2E.t.sol b/test/e2e/logicModule/FundingPotE2E.t.sol index 5c3cdcbb7..25ca300e0 100644 --- a/test/e2e/logicModule/FundingPotE2E.t.sol +++ b/test/e2e/logicModule/FundingPotE2E.t.sol @@ -15,12 +15,11 @@ import { ILM_PC_FundingPot_v1 } from "@lm/LM_PC_FundingPot_v1.sol"; import {IERC20PaymentClientBase_v2} from - "test/utils/mocks/modules/paymentClient/ERC20PaymentClientBaseV2Mock.sol"; + "test/mocks/modules/paymentClient/ERC20PaymentClientBaseV2Mock.sol"; import { FM_BC_Bancor_Redeeming_VirtualSupply_v1, IFM_BC_Bancor_Redeeming_VirtualSupply_v1 -} from - "test/modules/fundingManager/bondingCurve/FM_BC_Bancor_Redeeming_VirtualSupply_v1.t.sol"; +} from "@fm/bondingCurve/FM_BC_Bancor_Redeeming_VirtualSupply_v1.sol"; import {PP_Streaming_v2} from "src/modules/paymentProcessor/PP_Streaming_v2.sol"; import { LM_PC_Bounties_v2, ILM_PC_Bounties_v2 @@ -29,7 +28,7 @@ import { import {FM_DepositVault_v1} from "@fm/depositVault/FM_DepositVault_v1.sol"; import {ERC165Upgradeable} from "@oz-up/utils/introspection/ERC165Upgradeable.sol"; -import {ERC20Mock} from "test/utils/mocks/ERC20Mock.sol"; +import {ERC20Mock} from "test/mocks/external/token/ERC20Mock.sol"; import {SafeERC20} from "@oz/token/ERC20/utils/SafeERC20.sol"; import {ERC20Issuance_v1} from "@ex/token/ERC20Issuance_v1.sol"; @@ -66,9 +65,11 @@ contract FundingPotE2E is E2ETest { // moduleConfigurations[3:] => Additional Logic Modules issuanceToken = new ERC20Issuance_v1( - "Bonding Curve Token", "BCT", 18, type(uint).max - 1, address(this) + "Bonding Curve Token", "BCT", 18, type(uint).max - 1 ); + issuanceToken.setMinter(address(this), true); + IFM_BC_Bancor_Redeeming_VirtualSupply_v1.BondingCurveProperties memory bc_properties = IFM_BC_Bancor_Redeeming_VirtualSupply_v1 .BondingCurveProperties({ diff --git a/test/mocks/modules/fundingManager/FundingManagerV1Mock.sol b/test/mocks/modules/fundingManager/FundingManagerV1Mock.sol index 2071c4348..36e69d90a 100644 --- a/test/mocks/modules/fundingManager/FundingManagerV1Mock.sol +++ b/test/mocks/modules/fundingManager/FundingManagerV1Mock.sol @@ -42,8 +42,9 @@ contract FundingManagerV1Mock is IFundingManager_v1, Module_v1 { ) public override(Module_v1) initializer { __Module_init(orchestrator_, metadata); _bondingToken = new ERC20Issuance_v1( - "Bonding Token", "BOND", 18, type(uint).max - 1, address(this) + "Bonding Token", "BOND", 18, type(uint).max - 1 ); + _bondingToken.setMinter(address(this), true); } function setToken(IERC20 newToken) public { diff --git a/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol b/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol index a39cdb8ab..9b4f6401b 100644 --- a/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol +++ b/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol @@ -6,8 +6,8 @@ import { ModuleTest, IModule_v1, IOrchestrator_v1 -} from "test/modules/ModuleTest.sol"; -import {OZErrors} from "test/utils/errors/OZErrors.sol"; +} from "test/unit/modules/ModuleTest.sol"; +import {OZErrors} from "test/testUtilities/OZErrors.sol"; // External import {Clones} from "@oz/proxy/Clones.sol"; @@ -17,20 +17,20 @@ import { IERC20PaymentClientBase_v2, ERC20PaymentClientBaseV2Mock, ERC20Mock -} from "test/utils/mocks/modules/paymentClient/ERC20PaymentClientBaseV2Mock.sol"; +} from "test/mocks/modules/paymentClient/ERC20PaymentClientBaseV2Mock.sol"; import { ERC721Mock, MockHookContract, MockFailingHookContract -} from "test/utils/mocks/modules/logicModules/LM_PC_FundingPot_v1Mock.sol"; +} from "test/mocks/modules/logicModule/LM_PC_FundingPot_v1Mock.sol"; import {IBondingCurveBase_v1} from "@fm/bondingCurve/interfaces/IBondingCurveBase_v1.sol"; // System under Test (SuT) import {LM_PC_FundingPot_v1_Exposed} from - "test/modules/logicModule/LM_PC_FundingPot_v1_Exposed.sol"; + "test/unit/modules/logicModule/LM_PC_FundingPot_v1_Exposed.sol"; import {ILM_PC_FundingPot_v1} from "src/modules/logicModule/interfaces/ILM_PC_FundingPot_v1.sol"; diff --git a/test/unit/modules/logicModule/PP_FundingPot_v1_Exposed.sol b/test/unit/modules/logicModule/PP_FundingPot_v1_Exposed.sol deleted file mode 100644 index a96804e24..000000000 --- a/test/unit/modules/logicModule/PP_FundingPot_v1_Exposed.sol +++ /dev/null @@ -1,20 +0,0 @@ -// SPDX-License-Identifier: LGPL-3.0-only -pragma solidity ^0.8.0; - -// Internal -import {LM_PC_FundingPot_v1} from - "src/modules/logicModule/LM_PC_FundingPot_v1.sol"; - -// Access Mock of the PP_FundingPot_v1 contract for Testing. -contract LM_PC_FundingPot_v1_Exposed is LM_PC_FundingPot_v1 { - // Use the `exposed_` prefix for functions to expose internal contract for - // testing. - - function exposed_checkForFundingPotAdminRole(address admin_) - external - view - returns (bool) - { - return _checkForFundingPotAdminRole(admin_); - } -} From 7940d76983bdb658994a5fee45f896e9d517aff6 Mon Sep 17 00:00:00 2001 From: Zuhaib Mohammed Date: Tue, 27 May 2025 20:49:19 +0530 Subject: [PATCH 127/130] fix: audit fixes #1 --- .../logicModule/LM_PC_FundingPot_v1.sol | 27 ++++++++----------- .../logicModule/LM_PC_FundingPot_v1.t.sol | 2 +- 2 files changed, 12 insertions(+), 17 deletions(-) diff --git a/src/modules/logicModule/LM_PC_FundingPot_v1.sol b/src/modules/logicModule/LM_PC_FundingPot_v1.sol index fb3e9c121..8159b2d8f 100644 --- a/src/modules/logicModule/LM_PC_FundingPot_v1.sol +++ b/src/modules/logicModule/LM_PC_FundingPot_v1.sol @@ -109,7 +109,7 @@ contract LM_PC_FundingPot_v1 is uint8 internal constant FLAG_END = 3; /// @notice The maximum valid access criteria ID. - uint8 internal constant MAX_ACCESS_CRITERIA_ID = 4; + uint8 internal constant MAX_ACCESS_CRITERIA_TYPE = 4; // ------------------------------------------------------------------------- // State @@ -305,7 +305,7 @@ contract LM_PC_FundingPot_v1 is view returns (bool isEligible, uint remainingAmountAllowedToContribute) { - if (accessCriteriaId_ > MAX_ACCESS_CRITERIA_ID) { + if (accessCriteriaId_ > MAX_ACCESS_CRITERIA_TYPE) { revert Module__LM_PC_FundingPot__InvalidAccessCriteriaId(); } @@ -471,7 +471,7 @@ contract LM_PC_FundingPot_v1 is ) external onlyModuleRole(FUNDING_POT_ADMIN_ROLE) { Round storage round = rounds[roundId_]; - if (accessCriteriaType_ > MAX_ACCESS_CRITERIA_ID) { + if (accessCriteriaType_ > MAX_ACCESS_CRITERIA_TYPE) { revert Module__LM_PC_FundingPot__InvalidAccessCriteriaId(); } @@ -535,10 +535,8 @@ contract LM_PC_FundingPot_v1 is } else if (accessCriteriaType == AccessCriteriaType.LIST) { // For LIST type, update the allowed addresses for (uint i = 0; i < allowedAddresses_.length; i++) { - unchecked { - round.accessCriterias[criteriaId].allowedAddresses[allowedAddresses_[i]] - = true; - } + round.accessCriterias[criteriaId].allowedAddresses[allowedAddresses_[i]] + = true; } } @@ -570,10 +568,8 @@ contract LM_PC_FundingPot_v1 is _validateEditRoundParameters(round); for (uint i = 0; i < addressesToRemove_.length; i++) { - unchecked { - round.accessCriterias[accessCriteriaId_].allowedAddresses[addressesToRemove_[i]] - = false; - } + round.accessCriterias[accessCriteriaId_].allowedAddresses[addressesToRemove_[i]] + = false; } emit AllowlistedAddressesRemoved( @@ -659,9 +655,10 @@ contract LM_PC_FundingPot_v1 is ); } + lastSeenRoundId = currentProcessingRoundId; // Update lastSeenRoundId before continuing + // Skip if this round is before the global accumulation start round if (currentProcessingRoundId < globalAccumulationStartRoundId) { - lastSeenRoundId = currentProcessingRoundId; // Update lastSeenRoundId before continuing continue; } @@ -672,7 +669,6 @@ contract LM_PC_FundingPot_v1 is && rounds[currentProcessingRoundId].accumulationMode != AccumulationMode.All ) { - lastSeenRoundId = currentProcessingRoundId; // Update lastSeenRoundId before continuing continue; } @@ -698,7 +694,6 @@ contract LM_PC_FundingPot_v1 is } unspentPersonalCap += unspentForThisEntry; } - lastSeenRoundId = currentProcessingRoundId; // Update after processing or skipping } _contributeToRoundFor( @@ -899,7 +894,7 @@ contract LM_PC_FundingPot_v1 is revert Module__LM_PC_FundingPot__RoundHasNotStarted(); } - if (accessCriteriaId_ > MAX_ACCESS_CRITERIA_ID) { + if (accessCriteriaId_ > MAX_ACCESS_CRITERIA_TYPE) { revert Module__LM_PC_FundingPot__InvalidAccessCriteriaId(); } @@ -1239,7 +1234,7 @@ contract LM_PC_FundingPot_v1 is for ( uint8 accessCriteriaId = 1; - accessCriteriaId <= MAX_ACCESS_CRITERIA_ID; + accessCriteriaId <= MAX_ACCESS_CRITERIA_TYPE; accessCriteriaId++ ) { uint contributionByAccessCriteria = diff --git a/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol b/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol index 9b4f6401b..b01dd2a48 100644 --- a/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol +++ b/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol @@ -791,7 +791,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { │ └── When user attempts to set access criteria │ └── Then it should revert │ - ├── Given AccessCriteriaId is greater than MAX_ACCESS_CRITERIA_ID + ├── Given AccessCriteriaId is greater than MAX_ACCESS_CRITERIA_TYPE │ └── When user attempts to set access criteria │ └── Then it should revert │ From 87cebb9fa9c188b5ff2d18996cc3aee9fa84414d Mon Sep 17 00:00:00 2001 From: Zuhaib Mohammed Date: Fri, 30 May 2025 10:59:15 +0530 Subject: [PATCH 128/130] fix: audit fixes 2 --- .../logicModule/LM_PC_FundingPot_v1.sol | 6 +++- .../logicModule/LM_PC_FundingPot_v1.t.sol | 32 ++++++++----------- 2 files changed, 18 insertions(+), 20 deletions(-) diff --git a/src/modules/logicModule/LM_PC_FundingPot_v1.sol b/src/modules/logicModule/LM_PC_FundingPot_v1.sol index 8159b2d8f..1d1bd3369 100644 --- a/src/modules/logicModule/LM_PC_FundingPot_v1.sol +++ b/src/modules/logicModule/LM_PC_FundingPot_v1.sol @@ -755,9 +755,13 @@ contract LM_PC_FundingPot_v1 is uint contributorCount = contributors.length; // Check batch size is not zero - if (batchSize_ == 0 || batchSize_ > contributorCount) { + if (batchSize_ == 0) { revert Module__LM_PC_FundingPot__InvalidBatchParameters(); } + // If batch size is greater than contributor count, set batch size to contributor count + if (batchSize_ > contributorCount) { + batchSize_ = contributorCount; + } // If autoClosure is false, only admin can process contributors if (!round.autoClosure) { diff --git a/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol b/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol index b01dd2a48..782714af7 100644 --- a/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol +++ b/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol @@ -4679,10 +4679,6 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { │ └── When user attempts to create payment orders in batch │ └── Then it should revert with Module__LM_PC_FundingPot__RoundNotClosed │ - ├── Given start index is greater than the number of contributors - │ └── When user attempts to create payment orders in batch - │ └── Then it should revert with Module__LM_PC_FundingPot__InvalidBatchParameters - │ ├── Given batch size is zero │ └── When user attempts to create payment orders in batch │ └── Then it should revert with Module__LM_PC_FundingPot__InvalidBatchParameters @@ -4692,6 +4688,10 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { │ └── When user attempts to create payment orders in batch │ └── Then it should revert with Module__CallerNotAuthorized │ + ├── Given start index is greater than the number of contributors + │ └── When user attempts to create payment orders in batch + │ └── Then it should not revert and create payment orders + │ ├── Given a closed round with autoClosure │ └── When user attempts to create payment orders in batch │ └── Then it should not revert and payment orders should be created @@ -4734,21 +4734,6 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { fundingPot.createPaymentOrdersForContributorsBatch(roundId, 1); } - function testCreatePaymentOrdersForContributorsBatch_revertsGivenBatchSizeIsGreaterThanContributorCount( - ) public { - testCloseRound_worksWithMultipleContributors(); - uint32 roundId = fundingPot.getRoundCount(); - - vm.expectRevert( - abi.encodeWithSelector( - ILM_PC_FundingPot_v1 - .Module__LM_PC_FundingPot__InvalidBatchParameters - .selector - ) - ); - fundingPot.createPaymentOrdersForContributorsBatch(roundId, 999); - } - function testCreatePaymentOrdersForContributorsBatch_revertsGivenBatchSizeIsZero( ) public { testCloseRound_worksWithMultipleContributors(); @@ -4784,6 +4769,15 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { vm.stopPrank(); } + function testCreatePaymentOrdersForContributorsBatch_worksGivenBatchSizeIsGreaterThanContributorCount( + ) public { + testCloseRound_worksWithMultipleContributors(); + uint32 roundId = fundingPot.getRoundCount(); + + fundingPot.createPaymentOrdersForContributorsBatch(roundId, 999); + assertEq(fundingPot.paymentOrders().length, 3); + } + function testCreatePaymentOrdersForContributorsBatch_worksGivenRoundIsAutoClosure( ) public { testCloseRound_worksGivenRoundisAutoClosure(); From 32df6043c48af972d552882f431fd8d1f0efac6e Mon Sep 17 00:00:00 2001 From: leeftk <40748420+leeftk@users.noreply.github.com> Date: Tue, 16 Sep 2025 02:25:43 -0500 Subject: [PATCH 129/130] Feat/contract resizing (#769) * fix: new audit fixes 1 * fix: new audit fixes 2 * fix: new audit fixes 3 * fix: audit fixes 4 * fix: new audit fixes 4 -remove unneccessary code * fix: new audit fixes 5 * fix: new audit fixes 5: add getter function * fix: contract size fix 1 * fix: contract size fix 2: remove getter functions and make public * fix: contract size fix 3 * fix: contract size fix 3: if condition * contract resizing * chore:remove uncommented tests * chore:remove hook validation --------- Co-authored-by: Zuhaib Mohammed Co-authored-by: Jeffrey Owoloko <72028836+JeffreyJoel@users.noreply.github.com> Co-authored-by: JeffreyJoel --- .../logicModule/LM_PC_FundingPot_v1.sol | 362 +-- .../interfaces/ILM_PC_FundingPot_v1.sol | 181 +- test/e2e/logicModule/FundingPotE2E.t.sol | 24 +- .../logicModule/LM_PC_FundingPot_v1.t.sol | 2478 +++++++++++------ 4 files changed, 1724 insertions(+), 1321 deletions(-) diff --git a/src/modules/logicModule/LM_PC_FundingPot_v1.sol b/src/modules/logicModule/LM_PC_FundingPot_v1.sol index 1d1bd3369..b987a6e28 100644 --- a/src/modules/logicModule/LM_PC_FundingPot_v1.sol +++ b/src/modules/logicModule/LM_PC_FundingPot_v1.sol @@ -115,7 +115,7 @@ contract LM_PC_FundingPot_v1 is // State /// @notice The current round count. - uint32 private roundCount; + uint32 public roundCount; /// @notice Stores all funding rounds by their unique ID. mapping(uint32 => Round) private rounds; @@ -127,14 +127,14 @@ contract LM_PC_FundingPot_v1 is ) private roundIdToAccessCriteriaIdToPrivileges; /// @notice Maps round IDs to user addresses to contribution amounts. - mapping(uint32 => mapping(address => uint)) private + mapping(uint32 => mapping(address => uint)) public roundIdToUserToContribution; /// @notice Maps round IDs to total contributions. - mapping(uint32 => uint) private roundIdToTotalContributions; + mapping(uint32 => uint) public roundIdToTotalContributions; /// @notice Maps round IDs to closed status. - mapping(uint32 => bool) private roundIdToClosedStatus; + mapping(uint32 => bool) public roundIdToClosedStatus; /// @notice Maps round IDs to bonding curve tokens bought. mapping(uint32 => uint) private roundTokensBought; @@ -155,10 +155,14 @@ contract LM_PC_FundingPot_v1 is /// @notice The minimum round ID (inclusive, >= 1) to consider for accumulation calculations. /// @dev Defaults to 1. If a target round's mode allows accumulation, /// only previous rounds with roundId >= globalAccumulationStartRoundId will be included. - uint32 internal globalAccumulationStartRoundId; + uint32 public globalAccumulationStartRoundId; + + /// @notice Maps user addresses to a mapping of round IDs to a mapping of access criteria IDs to whether their unspent cap has been used + mapping(address => mapping(uint32 => mapping(uint8 => bool))) public + usedUnspentCaps; /// @notice Storage gap for future upgrades. - uint[50] private __gap; + uint[47] private __gap; // ------------------------------------------------------------------------- // Modifiers @@ -284,98 +288,7 @@ contract LM_PC_FundingPot_v1 is ); } - /// @inheritdoc ILM_PC_FundingPot_v1 - function getRoundCount() external view returns (uint32) { - return roundCount; - } - - /// @inheritdoc ILM_PC_FundingPot_v1 - function isRoundClosed(uint32 roundId_) external view returns (bool) { - return roundIdToClosedStatus[roundId_]; - } - - /// @inheritdoc ILM_PC_FundingPot_v1 - function getUserEligibility( - uint32 roundId_, - uint8 accessCriteriaId_, - bytes32[] memory merkleProof_, - address user_ - ) - external - view - returns (bool isEligible, uint remainingAmountAllowedToContribute) - { - if (accessCriteriaId_ > MAX_ACCESS_CRITERIA_TYPE) { - revert Module__LM_PC_FundingPot__InvalidAccessCriteriaId(); - } - - Round storage round = rounds[roundId_]; - - if (round.roundEnd == 0 && round.roundCap == 0) { - revert Module__LM_PC_FundingPot__RoundNotCreated(); - } - - AccessCriteria storage accessCriteria = - round.accessCriterias[accessCriteriaId_]; - - if (accessCriteria.accessCriteriaType == AccessCriteriaType.UNSET) { - return (false, 0); - } - - isEligible = _checkAccessCriteriaEligibility( - roundId_, accessCriteriaId_, merkleProof_, user_ - ); - - if (isEligible) { - AccessCriteriaPrivileges storage privileges = - roundIdToAccessCriteriaIdToPrivileges[roundId_][accessCriteriaId_]; - uint userPersonalCap = privileges.personalCap; - uint userContribution = roundIdToUserToContribution[roundId_][user_]; - - uint personalCapRemaining = userPersonalCap > userContribution - ? userPersonalCap - userContribution - : 0; - - uint totalContributions = roundIdToTotalContributions[roundId_]; - uint roundCapRemaining = round.roundCap > totalContributions - ? round.roundCap - totalContributions - : 0; - - remainingAmountAllowedToContribute = personalCapRemaining - < roundCapRemaining ? personalCapRemaining : roundCapRemaining; - - return (true, remainingAmountAllowedToContribute); - } else { - return (false, 0); - } - } - - /// @inheritdoc ILM_PC_FundingPot_v1 - function getTotalRoundContribution(uint32 roundId_) - external - view - returns (uint) - { - return roundIdToTotalContributions[roundId_]; - } - - /// @inheritdoc ILM_PC_FundingPot_v1 - function getUserContributionToRound(uint32 roundId_, address user_) - external - view - returns (uint) - { - return roundIdToUserToContribution[roundId_][user_]; - } - /// @inheritdoc ILM_PC_FundingPot_v1 - function getGlobalAccumulationStartRoundId() - external - view - returns (uint32) - { - return globalAccumulationStartRoundId; - } // ------------------------------------------------------------------------- // Public - Mutating @@ -467,12 +380,13 @@ contract LM_PC_FundingPot_v1 is uint8 accessCriteriaId_, // Optional: 0 for new, non-zero for edit address nftContract_, bytes32 merkleRoot_, - address[] calldata allowedAddresses_ + address[] calldata allowedAddresses_, + address[] calldata removedAddresses_ ) external onlyModuleRole(FUNDING_POT_ADMIN_ROLE) { Round storage round = rounds[roundId_]; if (accessCriteriaType_ > MAX_ACCESS_CRITERIA_TYPE) { - revert Module__LM_PC_FundingPot__InvalidAccessCriteriaId(); + revert Module__LM_PC_FundingPot__InvalidAccessCriteriaType(); } _validateEditRoundParameters(round); @@ -494,7 +408,7 @@ contract LM_PC_FundingPot_v1 is round.accessCriterias[criteriaId].accessCriteriaType == AccessCriteriaType.UNSET ) { - revert Module__LM_PC_FundingPot__InvalidAccessCriteriaId(); + revert Module__LM_PC_FundingPot__InvalidAccessCriteriaType(); } } @@ -522,7 +436,6 @@ contract LM_PC_FundingPot_v1 is round.accessCriterias[criteriaId].nftContract = address(0); round.accessCriterias[criteriaId].merkleRoot = bytes32(0); // @note: When changing allowlists, call removeAllowlistedAddresses first to clear previous entries - // Set the access criteria type round.accessCriterias[criteriaId].accessCriteriaType = accessCriteriaType; @@ -533,6 +446,13 @@ contract LM_PC_FundingPot_v1 is } else if (accessCriteriaType == AccessCriteriaType.MERKLE) { round.accessCriterias[criteriaId].merkleRoot = merkleRoot_; } else if (accessCriteriaType == AccessCriteriaType.LIST) { + // Remove the addresses from the allowed list if any + if (removedAddresses_.length > 0) { + for (uint i = 0; i < removedAddresses_.length; i++) { + round.accessCriterias[criteriaId].allowedAddresses[removedAddresses_[i]] + = false; + } + } // For LIST type, update the allowed addresses for (uint i = 0; i < allowedAddresses_.length; i++) { round.accessCriterias[criteriaId].allowedAddresses[allowedAddresses_[i]] @@ -540,12 +460,7 @@ contract LM_PC_FundingPot_v1 is } } - // Emit the appropriate event based on whether this is a new setting or an edit - if (isEdit) { - emit AccessCriteriaEdited(roundId_, criteriaId); - } else { - emit AccessCriteriaSet(roundId_, criteriaId); - } + emit AccessUpdated(isEdit, roundId_, criteriaId); } // Update removeAllowlistedAddresses to match the new approach @@ -562,7 +477,7 @@ contract LM_PC_FundingPot_v1 is round.accessCriterias[accessCriteriaId_].accessCriteriaType == AccessCriteriaType.UNSET ) { - revert Module__LM_PC_FundingPot__InvalidAccessCriteriaId(); + revert Module__LM_PC_FundingPot__InvalidAccessCriteriaType(); } _validateEditRoundParameters(round); @@ -573,7 +488,7 @@ contract LM_PC_FundingPot_v1 is } emit AllowlistedAddressesRemoved( - roundId_, accessCriteriaId_, addressesToRemove_ + ); } @@ -592,7 +507,7 @@ contract LM_PC_FundingPot_v1 is _validateEditRoundParameters(round); if (!_validTimes(start_, cliff_, end_)) { - revert Module__LM_PC_FundingPot__InvalidTimes(); + revert Module__LM_PC_FundingPot__InvalidInput(); } AccessCriteriaPrivileges storage accessCriteriaPrivileges = @@ -616,19 +531,6 @@ contract LM_PC_FundingPot_v1 is ); } - /// @inheritdoc ILM_PC_FundingPot_v1 - function contributeToRoundFor( - address user_, - uint32 roundId_, - uint amount_, - uint8 accessCriteriaId_, - bytes32[] calldata merkleProof_ - ) external { - // Call the internal function with no additional unspent personal cap - _contributeToRoundFor( - user_, roundId_, amount_, accessCriteriaId_, merkleProof_, 0 - ); - } /// @inheritdoc ILM_PC_FundingPot_v1 function contributeToRoundFor( @@ -639,62 +541,15 @@ contract LM_PC_FundingPot_v1 is bytes32[] memory merkleProof_, UnspentPersonalRoundCap[] calldata unspentPersonalRoundCaps_ ) external { - uint unspentPersonalCap = 0; // Initialize to 0 - uint32 lastSeenRoundId = 0; // Tracks the last seen roundId to ensure strictly increasing order - - // Process each previous round cap that the user wants to carry over - for (uint i = 0; i < unspentPersonalRoundCaps_.length; i++) { - UnspentPersonalRoundCap memory roundCapInfo = - unspentPersonalRoundCaps_[i]; - uint32 currentProcessingRoundId = roundCapInfo.roundId; - - // Enforcement: Round IDs in the array must be strictly increasing. - if (currentProcessingRoundId <= lastSeenRoundId) { - revert - Module__LM_PC_FundingPot__UnspentCapsRoundIdsNotStrictlyIncreasing( - ); - } - - lastSeenRoundId = currentProcessingRoundId; // Update lastSeenRoundId before continuing - - // Skip if this round is before the global accumulation start round - if (currentProcessingRoundId < globalAccumulationStartRoundId) { - continue; - } - - // For PERSONAL cap rollover, the PREVIOUS round must have allowed it (Personal or All). - if ( - rounds[currentProcessingRoundId].accumulationMode - != AccumulationMode.Personal - && rounds[currentProcessingRoundId].accumulationMode - != AccumulationMode.All - ) { - continue; - } - - if ( - _checkAccessCriteriaEligibility( - currentProcessingRoundId, - roundCapInfo.accessCriteriaId, - roundCapInfo.merkleProof, - user_ - ) - ) { - AccessCriteriaPrivileges storage privileges = - roundIdToAccessCriteriaIdToPrivileges[currentProcessingRoundId][roundCapInfo - .accessCriteriaId]; - - uint userContribution = - roundIdToUserToContribution[currentProcessingRoundId][user_]; - uint personalCap = privileges.personalCap; - uint unspentForThisEntry = 0; - - if (userContribution < personalCap) { - unspentForThisEntry = personalCap - userContribution; - } - unspentPersonalCap += unspentForThisEntry; - } + // If using unspent caps, only the owner can use them + if (unspentPersonalRoundCaps_.length > 0 && _msgSender() != user_) { + revert Module__LM_PC_FundingPot__OnlyOwnerCanUseUnspentCaps(); } + + uint unspentPersonalCap = _calculateUnspentPersonalCap( + user_, roundId_, unspentPersonalRoundCaps_ + ); + _contributeToRoundFor( user_, @@ -754,14 +609,7 @@ contract LM_PC_FundingPot_v1 is EnumerableSet.values(contributorsByRound[roundId_]); uint contributorCount = contributors.length; - // Check batch size is not zero - if (batchSize_ == 0) { - revert Module__LM_PC_FundingPot__InvalidBatchParameters(); - } - // If batch size is greater than contributor count, set batch size to contributor count - if (batchSize_ > contributorCount) { - batchSize_ = contributorCount; - } + // If autoClosure is false, only admin can process contributors if (!round.autoClosure) { @@ -794,9 +642,7 @@ contract LM_PC_FundingPot_v1 is revert Module__LM_PC_FundingPot__StartRoundCannotBeZero(); } if (startRoundId_ > roundCount) { - revert Module__LM_PC_FundingPot__StartRoundGreaterThanRoundCount( - startRoundId_, roundCount - ); + revert Module__LM_PC_FundingPot__StartRoundGreaterThanRoundCount(); } globalAccumulationStartRoundId = startRoundId_; @@ -807,6 +653,70 @@ contract LM_PC_FundingPot_v1 is // ------------------------------------------------------------------------- // Internal + /// @notice Calculates the unspent personal capacity from previous rounds. + /// @param user_ The user address to calculate unspent capacity for. + /// @param roundId_ The current round ID. + /// @param unspentPersonalRoundCaps_ Array of previous rounds and access criteria to calculate unused capacity from. + /// @return unspentPersonalCap The amount of unspent personal capacity that can be used. + function _calculateUnspentPersonalCap( + address user_, + uint32 roundId_, + UnspentPersonalRoundCap[] calldata unspentPersonalRoundCaps_ + ) internal returns (uint unspentPersonalCap) { + + + uint totalAggregatedPersonalCap = 0; + uint totalSpentInPastRounds = 0; + + for (uint i = 0; i < unspentPersonalRoundCaps_.length; i++) { + UnspentPersonalRoundCap memory roundCapInfo = unspentPersonalRoundCaps_[i]; + uint32 currentProcessingRoundId = roundCapInfo.roundId; + + // Skip if this round is before the global accumulation start round + if (currentProcessingRoundId < globalAccumulationStartRoundId) { + continue; + } + + // Skip if round is current or future round + if (currentProcessingRoundId >= roundId_) { + revert Module__LM_PC_FundingPot__UnspentCapsMustBeFromPreviousRounds(); + } + + // Skip if cap was already used + if (usedUnspentCaps[user_][currentProcessingRoundId][roundCapInfo.accessCriteriaId]) { + continue; + } + + // For PERSONAL cap rollover, the PREVIOUS round must have allowed it (Personal or All) + if (rounds[currentProcessingRoundId].accumulationMode != AccumulationMode.Personal + && rounds[currentProcessingRoundId].accumulationMode != AccumulationMode.All) { + continue; + } + + // Only count spent amounts from rounds that meet the accumulation criteria + totalSpentInPastRounds += roundIdToUserToContribution[currentProcessingRoundId][user_]; + + // Check eligibility for the past round + if (_checkAccessCriteriaEligibility( + currentProcessingRoundId, + roundCapInfo.accessCriteriaId, + roundCapInfo.merkleProof, + user_ + )) { + AccessCriteriaPrivileges storage privileges = roundIdToAccessCriteriaIdToPrivileges[currentProcessingRoundId][roundCapInfo.accessCriteriaId]; + totalAggregatedPersonalCap += privileges.personalCap; + } + // Mark the specific caps that were used in this contribution + usedUnspentCaps[user_][currentProcessingRoundId][roundCapInfo.accessCriteriaId] = true; + } + + if (totalAggregatedPersonalCap > totalSpentInPastRounds) { + unspentPersonalCap = totalAggregatedPersonalCap - totalSpentInPastRounds; + } + + return unspentPersonalCap; + } + /// @notice Validates the round parameters. /// @param round_ The round to validate. /// @dev Reverts if the round parameters are invalid. @@ -814,32 +724,19 @@ contract LM_PC_FundingPot_v1 is // Validate round start time is in the future // @note: The below condition wont allow _roundStart == block.timestamp if (round_.roundStart <= block.timestamp) { - revert Module__LM_PC_FundingPot__RoundStartMustBeInFuture(); + revert Module__LM_PC_FundingPot__InvalidInput(); } // Validate that either end time or cap is set if (round_.roundEnd == 0 && round_.roundCap == 0) { - revert Module__LM_PC_FundingPot__RoundMustHaveEndTimeOrCap(); + revert Module__LM_PC_FundingPot__InvalidInput(); } // If end time is set, validate it's after start time if (round_.roundEnd > 0 && round_.roundEnd < round_.roundStart) { - revert Module__LM_PC_FundingPot__RoundEndMustBeAfterStart(); + revert Module__LM_PC_FundingPot__InvalidInput(); } - // Validate hook contract and function consistency - if ( - round_.hookContract != address(0) && round_.hookFunction.length == 0 - ) { - revert - Module__LM_PC_FundingPot__HookFunctionRequiredWithHookContract(); - } - - if (round_.hookContract == address(0) && round_.hookFunction.length > 0) - { - revert - Module__LM_PC_FundingPot__HookContractRequiredWithHookFunction(); - } } /// @notice Validates the round parameters before editing. @@ -851,7 +748,7 @@ contract LM_PC_FundingPot_v1 is } if (block.timestamp > round_.roundStart) { - revert Module__LM_PC_FundingPot__RoundAlreadyStarted(); + revert Module__LM_PC_FundingPot__InvalidInput(); } } @@ -884,22 +781,22 @@ contract LM_PC_FundingPot_v1 is uint unspentPersonalCap_ ) internal { if (amount_ == 0) { - revert Module__LM_PC_FundingPot__InvalidDepositAmount(); + revert Module__LM_PC_FundingPot__InvalidInput(); } Round storage round = rounds[roundId_]; - uint currentTime = block.timestamp; + if (round.roundEnd == 0 && round.roundCap == 0) { revert Module__LM_PC_FundingPot__RoundNotCreated(); } - if (currentTime < round.roundStart) { + if (block.timestamp < round.roundStart) { revert Module__LM_PC_FundingPot__RoundHasNotStarted(); } if (accessCriteriaId_ > MAX_ACCESS_CRITERIA_TYPE) { - revert Module__LM_PC_FundingPot__InvalidAccessCriteriaId(); + revert Module__LM_PC_FundingPot__InvalidAccessCriteriaType(); } _validateAccessCriteria( @@ -911,7 +808,7 @@ contract LM_PC_FundingPot_v1 is bool canOverrideContributionSpan = privileges.overrideContributionSpan; if ( - round.roundEnd > 0 && currentTime > round.roundEnd + round.roundEnd > 0 && block.timestamp > round.roundEnd && !canOverrideContributionSpan ) { revert Module__LM_PC_FundingPot__RoundHasEnded(); @@ -971,18 +868,16 @@ contract LM_PC_FundingPot_v1 is roundId_, accessCriteriaId_, merkleProof_, user_ ); - if (!isEligible) { - if (accessCriteria.accessCriteriaType == AccessCriteriaType.NFT) { - revert Module__LM_PC_FundingPot__AccessCriteriaNftFailed(); - } - if (accessCriteria.accessCriteriaType == AccessCriteriaType.MERKLE) - { - revert Module__LM_PC_FundingPot__AccessCriteriaMerkleFailed(); - } - - if (accessCriteria.accessCriteriaType == AccessCriteriaType.LIST) { - revert Module__LM_PC_FundingPot__AccessCriteriaListFailed(); - } + if ( + !isEligible + && ( + accessCriteria.accessCriteriaType == AccessCriteriaType.NFT + || accessCriteria.accessCriteriaType + == AccessCriteriaType.MERKLE + || accessCriteria.accessCriteriaType == AccessCriteriaType.LIST + ) + ) { + revert Module__LM_PC_FundingPot__AccessCriteriaFailed(); } } @@ -1147,9 +1042,6 @@ contract LM_PC_FundingPot_v1 is view returns (bool) { - if (nftContract_ == address(0) || user_ == address(0)) { - return false; - } try IERC721(nftContract_).balanceOf(user_) returns (uint balance) { return balance > 0; @@ -1183,12 +1075,10 @@ contract LM_PC_FundingPot_v1 is Round storage round = rounds[roundId_]; roundIdToClosedStatus[roundId_] = true; - + // @note: we don't check if the hook contract is valid here, because we don't want to revert the round closure + // if the hook contract is invalid. if (round.hookContract != address(0)) { (bool success,) = round.hookContract.call(round.hookFunction); - if (!success) { - revert Module__LM_PC_FundingPot__HookExecutionFailed(); - } } emit RoundClosed(roundId_, roundIdToTotalContributions[roundId_]); @@ -1214,7 +1104,7 @@ contract LM_PC_FundingPot_v1 is uint contributorCount = contributors.length; if (startIndex_ >= contributorCount) { - revert Module__LM_PC_FundingPot__InvalidStartIndex(); + revert Module__LM_PC_FundingPot__InvalidInput(); } // Calculate the end index (don't exceed array bounds) @@ -1353,16 +1243,6 @@ contract LM_PC_FundingPot_v1 is }); _addPaymentOrder(paymentOrder); - - emit PaymentOrderCreated( - roundId_, - recipient_, - accessCriteriaId_, - tokensAmount_, - start, - cliff, - end - ); } function _buyBondingCurveToken(uint32 roundId_) internal { diff --git a/src/modules/logicModule/interfaces/ILM_PC_FundingPot_v1.sol b/src/modules/logicModule/interfaces/ILM_PC_FundingPot_v1.sol index 915a61dfd..dae5bd750 100644 --- a/src/modules/logicModule/interfaces/ILM_PC_FundingPot_v1.sol +++ b/src/modules/logicModule/interfaces/ILM_PC_FundingPot_v1.sol @@ -150,16 +150,12 @@ interface ILM_PC_FundingPot_v1 is IERC20PaymentClientBase_v2 { AccumulationMode accumulationMode_ ); - /// @notice Emitted when access criteria is set for a round. - /// @param roundId_ The unique identifier of the round. - /// @param accessCriteriaId_ The identifier of the access criteria. - event AccessCriteriaSet(uint32 indexed roundId_, uint8 accessCriteriaId_); - /// @notice Emitted when access criteria is edited for a round. + /// @param isEdit_ represents new or edited setting /// @param roundId_ The unique identifier of the round. /// @param accessCriteriaId_ The identifier of the access criteria. - event AccessCriteriaEdited( - uint32 indexed roundId_, uint8 accessCriteriaId_ + event AccessUpdated( + bool isEdit_, uint32 indexed roundId_, uint8 accessCriteriaId_ ); /// @notice Emitted when access criteria privileges are set for a round. @@ -192,30 +188,7 @@ interface ILM_PC_FundingPot_v1 is IERC20PaymentClientBase_v2 { event RoundClosed(uint32 roundId_, uint totalContributions_); /// @notice Emitted when addresses are removed from an access criteria's allowed list. - /// @param roundId_ The ID of the round. - /// @param accessCriteriaId_ The ID of the access criteria. - /// @param addressesRemoved_ The addresses that were removed from the allowlist. - event AllowlistedAddressesRemoved( - uint32 roundId_, uint8 accessCriteriaId_, address[] addressesRemoved_ - ); - - /// @notice Emitted when a payment order is created. - /// @param roundId_ The ID of the round. - /// @param contributor_ The address of the contributor. - /// @param accessCriteriaId_ The ID of the access criteria. - /// @param tokensForThisAccessCriteria_ The amount of tokens contributed for this access criteria. - /// @param start_ The start timestamp for for when the linear vesting starts. - /// @param cliff_ The time in seconds from start time at which the unlock starts. - /// @param end_ The end timestamp for when the linear vesting ends. - event PaymentOrderCreated( - uint32 roundId_, - address contributor_, - uint8 accessCriteriaId_, - uint tokensForThisAccessCriteria_, - uint start_, - uint cliff_, - uint end_ - ); + event AllowlistedAddressesRemoved(); /// @notice Emitted when a contributor batch is processed. /// @param roundId_ The ID of the round. @@ -232,26 +205,8 @@ interface ILM_PC_FundingPot_v1 is IERC20PaymentClientBase_v2 { // ------------------------------------------------------------------------- // Errors - /// @notice Amount can not be zero. - error Module__LM_PC_FundingPot__InvalidDepositAmount(); - - /// @notice Round start time must be in the future. - error Module__LM_PC_FundingPot__RoundStartMustBeInFuture(); - - /// @notice Round must have either an end time or a funding cap. - error Module__LM_PC_FundingPot__RoundMustHaveEndTimeOrCap(); - - /// @notice Round end time must be after round start time. - error Module__LM_PC_FundingPot__RoundEndMustBeAfterStart(); - - /// @notice Round has already started and cannot be modified. - error Module__LM_PC_FundingPot__RoundAlreadyStarted(); - - /// @notice Thrown when a hook contract is specified without a hook function. - error Module__LM_PC_FundingPot__HookFunctionRequiredWithHookContract(); - - /// @notice Thrown when a hook function is specified without a hook contract. - error Module__LM_PC_FundingPot__HookContractRequiredWithHookFunction(); + /// @notice Invalid input validation. + error Module__LM_PC_FundingPot__InvalidInput(); /// @notice Round does not exist. error Module__LM_PC_FundingPot__RoundNotCreated(); @@ -259,11 +214,8 @@ interface ILM_PC_FundingPot_v1 is IERC20PaymentClientBase_v2 { /// @notice Incorrect access criteria. error Module__LM_PC_FundingPot__MissingRequiredAccessCriteriaData(); - /// @notice Invalid access criteria ID. - error Module__LM_PC_FundingPot__InvalidAccessCriteriaId(); - - /// @notice Invalid times. - error Module__LM_PC_FundingPot__InvalidTimes(); + /// @notice Invalid access criteria type. + error Module__LM_PC_FundingPot__InvalidAccessCriteriaType(); /// @notice Round has not started yet. error Module__LM_PC_FundingPot__RoundHasNotStarted(); @@ -271,19 +223,10 @@ interface ILM_PC_FundingPot_v1 is IERC20PaymentClientBase_v2 { /// @notice Round has already ended. error Module__LM_PC_FundingPot__RoundHasEnded(); - /// @notice User does not meet the NFT access criteria. - error Module__LM_PC_FundingPot__AccessCriteriaNftFailed(); - - /// @notice User does not meet the merkle proof access criteria. - error Module__LM_PC_FundingPot__AccessCriteriaMerkleFailed(); + /// @notice Access criteria failed. + error Module__LM_PC_FundingPot__AccessCriteriaFailed(); - /// @notice User is not on the allowlist. - error Module__LM_PC_FundingPot__AccessCriteriaListFailed(); - - /// @notice Access not permitted. - error Module__LM_PC_FundingPot__AccessNotPermitted(); - - /// @notice User has reached their personal contribution cap. + /// @notice User has reached their personal contribution cap. error Module__LM_PC_FundingPot__PersonalCapReached(); /// @notice Round contribution cap has been reached. @@ -292,33 +235,27 @@ interface ILM_PC_FundingPot_v1 is IERC20PaymentClientBase_v2 { /// @notice Round Closure conditions are not met. error Module__LM_PC_FundingPot__ClosureConditionsNotMet(); - /// @notice Hook execution failed. - error Module__LM_PC_FundingPot__HookExecutionFailed(); - /// @notice No contributions were made to the round. error Module__LM_PC_FundingPot__NoContributions(); /// @notice Round is not closed. error Module__LM_PC_FundingPot__RoundNotClosed(); - /// @notice Invalid start index. - error Module__LM_PC_FundingPot__InvalidStartIndex(); - - /// @notice Invalid batch parameters. - error Module__LM_PC_FundingPot__InvalidBatchParameters(); - /// @notice Start round ID must be greater than zero. error Module__LM_PC_FundingPot__StartRoundCannotBeZero(); /// @notice Start round ID cannot be greater than the current round count. - /// @param startRoundId_ The provided start round ID. - /// @param currentRoundCount_ The current total number of rounds. - error Module__LM_PC_FundingPot__StartRoundGreaterThanRoundCount( - uint32 startRoundId_, uint32 currentRoundCount_ - ); + error Module__LM_PC_FundingPot__StartRoundGreaterThanRoundCount(); + + /// @notice Unspent caps must be from previous rounds. + error Module__LM_PC_FundingPot__UnspentCapsMustBeFromPreviousRounds(); + + /// @notice Thrown when someone tries to use another user's unspent caps + error Module__LM_PC_FundingPot__OnlyOwnerCanUseUnspentCaps(); + + /// @notice Hook execution failed. + error Module__LM_PC_FundingPot__HookExecutionFailed(); - /// @notice Thrown when round IDs in UnspentPersonalRoundCap array are not strictly increasing. - error Module__LM_PC_FundingPot__UnspentCapsRoundIdsNotStrictlyIncreasing(); // ------------------------------------------------------------------------- // Public - Getters @@ -384,59 +321,6 @@ interface ILM_PC_FundingPot_v1 is IERC20PaymentClientBase_v2 { uint end_ ); - /// @notice Retrieves the total number of funding rounds. - /// @return roundCount_ The total number of funding rounds. - function getRoundCount() external view returns (uint32 roundCount_); - - /// @notice Retrieves the closed status of a round. - /// @param roundId_ The ID of the round. - /// @return The closed status of the round. - function isRoundClosed(uint32 roundId_) external view returns (bool); - - /// @notice Gets eligibility information for a user in a specific round. - /// @param roundId_ The ID of the round to check eligibility for. - /// @param accessCriteriaId_ The ID of the access criteria to check eligibility for. - /// @param merkleProof_ The Merkle proof for validation if needed. - /// @param user_ The address of the user to check. - /// @return isEligible Whether the user is eligible for the round through any criteria. - /// @return remainingAmountAllowedToContribute The remaining contribution the user can make. - function getUserEligibility( - uint32 roundId_, - uint8 accessCriteriaId_, - bytes32[] memory merkleProof_, - address user_ - ) - external - view - returns (bool isEligible, uint remainingAmountAllowedToContribute); - - /// @notice Retrieves the total contribution for a specific round. - /// @param roundId_ The ID of the round to check contributions for. - /// @return The total contributions for the specified round. - function getTotalRoundContribution(uint32 roundId_) - external - view - returns (uint); - - /// @notice Retrieves the contribution amount for a specific user in a round. - /// @param roundId_ The ID of the round to check contributions for. - /// @param user_ The address of the user. - /// @return The user's contribution amount for the specified round. - function getUserContributionToRound(uint32 roundId_, address user_) - external - view - returns (uint); - - /// @notice Retrieves the globally configured start round ID for accumulation calculations. - /// @dev Accumulation (both personal and total) will only consider previous rounds - /// with IDs greater than or equal to this value, provided the target round's - /// AccumulationMode allows it. Defaults to 1. - /// @return The first round ID (inclusive) to consider for accumulation. - function getGlobalAccumulationStartRoundId() - external - view - returns (uint32); - // ------------------------------------------------------------------------- // Public - Mutating @@ -489,13 +373,15 @@ interface ILM_PC_FundingPot_v1 is IERC20PaymentClientBase_v2 { /// @param nftContract_ Address of the NFT contract. /// @param merkleRoot_ Merkle root for the access criteria. /// @param allowedAddresses_ List of explicitly allowed addresses. + /// @param removedAddresses_ List of addresses to remove from the allowed list. function setAccessCriteria( uint32 roundId_, uint8 accessCriteriaType_, uint8 accessCriteriaId_, address nftContract_, bytes32 merkleRoot_, - address[] memory allowedAddresses_ + address[] memory allowedAddresses_, + address[] memory removedAddresses_ ) external; /// @notice Removes addresses from the allowed list for a specific access criteria. @@ -528,22 +414,7 @@ interface ILM_PC_FundingPot_v1 is IERC20PaymentClientBase_v2 { uint end_ ) external; - /// @notice Allows a user to contribute to a specific funding round. - /// @dev Verifies the contribution eligibility based on the provided Merkle proof. - /// @param user_ The address of the user to contribute for. - /// @param roundId_ The unique identifier of the funding round. - /// @param amount_ The amount of tokens being contributed. - /// @param accessCriteriaId_ The identifier for the access criteria to validate eligibility. - /// @param merkleProof_ The Merkle proof used to verify the contributor's eligibility. - function contributeToRoundFor( - address user_, - uint32 roundId_, - uint amount_, - uint8 accessCriteriaId_, - bytes32[] calldata merkleProof_ - ) external; - - /// @notice Allows a user to contribute to a round with unused capacity from previous rounds. + /// @notice Contributes to a round on behalf of a user. /// @param user_ The address of the user to contribute for. /// @param roundId_ The ID of the round to contribute to. /// @param amount_ The amount to contribute. @@ -555,7 +426,7 @@ interface ILM_PC_FundingPot_v1 is IERC20PaymentClientBase_v2 { uint32 roundId_, uint amount_, uint8 accessCriteriaId_, - bytes32[] calldata merkleProof_, + bytes32[] memory merkleProof_, UnspentPersonalRoundCap[] calldata unspentPersonalRoundCaps_ ) external; diff --git a/test/e2e/logicModule/FundingPotE2E.t.sol b/test/e2e/logicModule/FundingPotE2E.t.sol index 25ca300e0..08d2533d7 100644 --- a/test/e2e/logicModule/FundingPotE2E.t.sol +++ b/test/e2e/logicModule/FundingPotE2E.t.sol @@ -199,13 +199,16 @@ contract FundingPotE2E is E2ETest { allowedAddresses[0] = contributor1; allowedAddresses[1] = contributor2; + address[] memory removedAddresses = new address[](0); + fundingPot.setAccessCriteria( round1Id, uint8(ILM_PC_FundingPot_v1.AccessCriteriaType.LIST), 0, address(0), bytes32(0), - allowedAddresses + allowedAddresses, + removedAddresses ); // Add access criteria to round 2 @@ -218,7 +221,8 @@ contract FundingPotE2E is E2ETest { 0, address(0), bytes32(0), - allowedAddresses + allowedAddresses, + removedAddresses ); // 5. Set access criteria privileges for the rounds @@ -257,22 +261,28 @@ contract FundingPotE2E is E2ETest { vm.startPrank(contributor1); contributionToken.approve(address(fundingPot), contributor1Amount); + ILM_PC_FundingPot_v1.UnspentPersonalRoundCap[] memory + unspentPersonalRoundCaps = + new ILM_PC_FundingPot_v1.UnspentPersonalRoundCap[](0); fundingPot.contributeToRoundFor( - contributor1, round1Id, contributor1Amount, 1, new bytes32[](0) + contributor1, round1Id, contributor1Amount, 1, new bytes32[](0), unspentPersonalRoundCaps ); vm.stopPrank(); vm.startPrank(contributor2); contributionToken.approve(address(fundingPot), contributor2Amount); + unspentPersonalRoundCaps = + new ILM_PC_FundingPot_v1.UnspentPersonalRoundCap[](0); fundingPot.contributeToRoundFor( - contributor2, round1Id, contributor2Amount, 1, new bytes32[](0) + contributor2, round1Id, contributor2Amount, 1, new bytes32[](0), unspentPersonalRoundCaps ); vm.stopPrank(); vm.startPrank(contributor3); contributionToken.approve(address(fundingPot), contributor3Amount); + fundingPot.contributeToRoundFor( - contributor3, round2Id, contributor3Amount, 1, new bytes32[](0) + contributor3, round2Id, contributor3Amount, 1, new bytes32[](0), unspentPersonalRoundCaps ); vm.stopPrank(); @@ -281,8 +291,8 @@ contract FundingPotE2E is E2ETest { // 8. Close rounds fundingPot.closeRound(round1Id); - assertEq(fundingPot.isRoundClosed(round1Id), true); - assertEq(fundingPot.isRoundClosed(round2Id), true); // round2 is auto closed + assertEq(fundingPot.roundIdToClosedStatus(round1Id), true); + assertEq(fundingPot.roundIdToClosedStatus(round2Id), true); // round2 is auto closed assertEq(contributionToken.balanceOf(address(fundingPot)), 0); assertGt(issuanceToken.balanceOf(address(fundingPot)), 0); diff --git a/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol b/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol index 782714af7..247fa865d 100644 --- a/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol +++ b/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol @@ -66,6 +66,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { // Default round parameters for testing RoundParams private _defaultRoundParams; RoundParams private _editedRoundParams; + ILM_PC_FundingPot_v1.UnspentPersonalRoundCap[] private _unspentPersonalRoundCaps; // Struct to hold round parameters struct RoundParams { @@ -81,6 +82,8 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { ERC721Mock mockNFTContract = new ERC721Mock("NFT Mock", "NFT"); MockFailingHookContract failingHook = new MockFailingHookContract(); + address[] public removedAddresses; + // ------------------------------------------------------------------------- // Setup @@ -130,6 +133,8 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { true, ILM_PC_FundingPot_v1.AccumulationMode.All ); + + removedAddresses = new address[](0); } // ------------------------------------------------------------------------- @@ -224,7 +229,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { vm.expectRevert( abi.encodeWithSelector( ILM_PC_FundingPot_v1 - .Module__LM_PC_FundingPot__RoundStartMustBeInFuture + .Module__LM_PC_FundingPot__InvalidInput .selector ) ); @@ -249,7 +254,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { vm.expectRevert( abi.encodeWithSelector( ILM_PC_FundingPot_v1 - .Module__LM_PC_FundingPot__RoundMustHaveEndTimeOrCap + .Module__LM_PC_FundingPot__InvalidInput .selector ) ); @@ -274,30 +279,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { vm.expectRevert( abi.encodeWithSelector( ILM_PC_FundingPot_v1 - .Module__LM_PC_FundingPot__RoundEndMustBeAfterStart - .selector - ) - ); - fundingPot.createRound( - params.roundStart, - params.roundEnd, - params.roundCap, - params.hookContract, - params.hookFunction, - params.autoClosure, - params.accumulationMode - ); - } - - function testCreateRound_revertsGivenHookContractIsSetButHookFunctionIsEmpty( - ) public { - RoundParams memory params = _defaultRoundParams; - params.hookContract = address(1); - params.hookFunction = bytes(""); - vm.expectRevert( - abi.encodeWithSelector( - ILM_PC_FundingPot_v1 - .Module__LM_PC_FundingPot__HookFunctionRequiredWithHookContract + .Module__LM_PC_FundingPot__InvalidInput .selector ) ); @@ -312,29 +294,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { ); } - function testCreateRound_revertsGivenHookFunctionIsSetButHookContractIsEmpty( - ) public { - RoundParams memory params = _defaultRoundParams; - params.hookContract = address(0); - params.hookFunction = bytes("test"); - vm.expectRevert( - abi.encodeWithSelector( - ILM_PC_FundingPot_v1 - .Module__LM_PC_FundingPot__HookContractRequiredWithHookFunction - .selector - ) - ); - fundingPot.createRound( - params.roundStart, - params.roundEnd, - params.roundCap, - params.hookContract, - params.hookFunction, - params.autoClosure, - params.accumulationMode - ); - } /* Test Fuzz createRound() ├── Given all the valid parameters are provided @@ -355,7 +315,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { params.accumulationMode ); - uint32 roundId = fundingPot.getRoundCount(); + uint32 roundId = fundingPot.roundCount(); // Retrieve the stored parameters ( @@ -429,7 +389,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { { vm.assume(user_ != address(0) && user_ != address(this)); testCreateRound(); - uint32 roundId = fundingPot.getRoundCount(); + uint32 roundId = fundingPot.roundCount(); RoundParams memory params = RoundParams({ roundStart: block.timestamp + 3 days, @@ -465,7 +425,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { function testEditRound_revertsGivenRoundIsNotCreated() public { testCreateRound(); - uint32 roundId = fundingPot.getRoundCount(); + uint32 roundId = fundingPot.roundCount(); RoundParams memory params = RoundParams({ roundStart: block.timestamp + 3 days, @@ -498,7 +458,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { function testEditRound_revertsGivenRoundIsActive() public { testCreateRound(); - uint32 roundId = fundingPot.getRoundCount(); + uint32 roundId = fundingPot.roundCount(); RoundParams memory params; ( @@ -525,7 +485,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { vm.expectRevert( abi.encodeWithSelector( ILM_PC_FundingPot_v1 - .Module__LM_PC_FundingPot__RoundAlreadyStarted + .Module__LM_PC_FundingPot__InvalidInput .selector ) ); @@ -545,7 +505,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { public { testCreateRound(); - uint32 roundId = fundingPot.getRoundCount(); + uint32 roundId = fundingPot.roundCount(); _editedRoundParams; vm.assume(roundStart_ < block.timestamp); _editedRoundParams.roundStart = roundStart_; @@ -553,7 +513,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { vm.expectRevert( abi.encodeWithSelector( ILM_PC_FundingPot_v1 - .Module__LM_PC_FundingPot__RoundStartMustBeInFuture + .Module__LM_PC_FundingPot__InvalidInput .selector ) ); @@ -572,7 +532,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { function testEditRound_revertsGivenRoundEndTimeAndCapAreBothZero() public { testCreateRound(); - uint32 roundId = fundingPot.getRoundCount(); + uint32 roundId = fundingPot.roundCount(); RoundParams memory params = RoundParams({ roundStart: block.timestamp + 3 days, @@ -587,7 +547,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { vm.expectRevert( abi.encodeWithSelector( ILM_PC_FundingPot_v1 - .Module__LM_PC_FundingPot__RoundMustHaveEndTimeOrCap + .Module__LM_PC_FundingPot__InvalidInput .selector ) ); @@ -613,7 +573,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { && roundEnd_ < roundStart_ ); testCreateRound(); - uint32 roundId = fundingPot.getRoundCount(); + uint32 roundId = fundingPot.roundCount(); // Get the current round start time (uint currentRoundStart,,,,,,) = @@ -636,43 +596,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { vm.expectRevert( abi.encodeWithSelector( ILM_PC_FundingPot_v1 - .Module__LM_PC_FundingPot__RoundEndMustBeAfterStart - .selector - ) - ); - - fundingPot.editRound( - roundId, - params.roundStart, - params.roundEnd, - params.roundCap, - params.hookContract, - params.hookFunction, - params.autoClosure, - params.accumulationMode - ); - } - - function testEditRound_revertsGivenHookContractIsSetButHookFunctionIsEmpty() - public - { - testCreateRound(); - uint32 roundId = fundingPot.getRoundCount(); - - RoundParams memory params = _helper_createEditRoundParams( - block.timestamp + 3 days, - block.timestamp + 4 days, - 2000, - address(1), - bytes(""), - true, - ILM_PC_FundingPot_v1.AccumulationMode.All - ); - - vm.expectRevert( - abi.encodeWithSelector( - ILM_PC_FundingPot_v1 - .Module__LM_PC_FundingPot__HookFunctionRequiredWithHookContract + .Module__LM_PC_FundingPot__InvalidInput .selector ) ); @@ -689,41 +613,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { ); } - function testEditRound_revertsGivenHookFunctionIsSetButHookContractIsEmpty() - public - { - testCreateRound(); - uint32 roundId = fundingPot.getRoundCount(); - - RoundParams memory params = _helper_createEditRoundParams( - block.timestamp + 3 days, - block.timestamp + 4 days, - 2000, - address(0), - bytes("test"), - true, - ILM_PC_FundingPot_v1.AccumulationMode.All - ); - - vm.expectRevert( - abi.encodeWithSelector( - ILM_PC_FundingPot_v1 - .Module__LM_PC_FundingPot__HookContractRequiredWithHookFunction - .selector - ) - ); - fundingPot.editRound( - roundId, - params.roundStart, - params.roundEnd, - params.roundCap, - params.hookContract, - params.hookFunction, - params.autoClosure, - params.accumulationMode - ); - } /* Test editRound() └── Given a round has been created @@ -741,7 +631,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { function testEditRound() public { testCreateRound(); - uint32 lastRoundId = fundingPot.getRoundCount(); + uint32 lastRoundId = fundingPot.roundCount(); RoundParams memory params = _editedRoundParams; @@ -812,7 +702,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { │ └── Then it should not revert └── Given all the valid parameters and access criteria is set └── When user attempts to edit access criteria - └── Then it should not revert + └── Then it should not revert */ function testFuzzSetAccessCriteria_revertsGivenUserDoesNotHaveFundingPotAdminRole( @@ -823,7 +713,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { vm.assume(user_ != address(0) && user_ != address(this)); testCreateRound(); - uint32 roundId = fundingPot.getRoundCount(); + uint32 roundId = fundingPot.roundCount(); ( address nftContract, @@ -846,7 +736,8 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { 0, nftContract, merkleRoot, - allowedAddresses + allowedAddresses, + removedAddresses ); vm.stopPrank(); } @@ -856,7 +747,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { ) public { vm.assume(accessCriteriaEnum >= 0 && accessCriteriaEnum <= 4); - uint32 roundId = fundingPot.getRoundCount(); + uint32 roundId = fundingPot.roundCount(); ( address nftContract, @@ -877,7 +768,8 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { 0, nftContract, merkleRoot, - allowedAddresses + allowedAddresses, + removedAddresses ); } @@ -887,7 +779,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { vm.assume(accessCriteriaEnum >= 0 && accessCriteriaEnum <= 4); testCreateRound(); - uint32 roundId = fundingPot.getRoundCount(); + uint32 roundId = fundingPot.roundCount(); ( address nftContract, @@ -901,7 +793,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { vm.expectRevert( abi.encodeWithSelector( ILM_PC_FundingPot_v1 - .Module__LM_PC_FundingPot__RoundAlreadyStarted + .Module__LM_PC_FundingPot__InvalidInput .selector ) ); @@ -911,7 +803,8 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { 0, nftContract, merkleRoot, - allowedAddresses + allowedAddresses, + removedAddresses ); } @@ -921,7 +814,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { vm.assume(accessCriteriaEnum > 4); testCreateRound(); - uint32 roundId = fundingPot.getRoundCount(); + uint32 roundId = fundingPot.roundCount(); ( address nftContract, @@ -935,7 +828,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { vm.expectRevert( abi.encodeWithSelector( ILM_PC_FundingPot_v1 - .Module__LM_PC_FundingPot__InvalidAccessCriteriaId + .Module__LM_PC_FundingPot__InvalidAccessCriteriaType .selector ) ); @@ -945,7 +838,8 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { 0, nftContract, merkleRoot, - allowedAddresses + allowedAddresses, + removedAddresses ); } @@ -955,7 +849,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { uint8(ILM_PC_FundingPot_v1.AccessCriteriaType.NFT); testCreateRound(); - uint32 roundId = fundingPot.getRoundCount(); + uint32 roundId = fundingPot.roundCount(); ( address nftContract, @@ -977,7 +871,8 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { 0, nftContract, merkleRoot, - allowedAddresses + allowedAddresses, + removedAddresses ); } @@ -987,7 +882,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { uint8(ILM_PC_FundingPot_v1.AccessCriteriaType.MERKLE); testCreateRound(); - uint32 roundId = fundingPot.getRoundCount(); + uint32 roundId = fundingPot.roundCount(); ( address nftContract, @@ -1009,7 +904,8 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { 0, nftContract, merkleRoot, - allowedAddresses + allowedAddresses, + removedAddresses ); } @@ -1019,7 +915,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { uint8(ILM_PC_FundingPot_v1.AccessCriteriaType.LIST); testCreateRound(); - uint32 roundId = fundingPot.getRoundCount(); + uint32 roundId = fundingPot.roundCount(); ( address nftContract, @@ -1041,7 +937,8 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { 0, nftContract, merkleRoot, - allowedAddresses + allowedAddresses, + removedAddresses ); } @@ -1049,7 +946,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { vm.assume(accessCriteriaEnum >= 0 && accessCriteriaEnum <= 4); testCreateRound(); - uint32 roundId = fundingPot.getRoundCount(); + uint32 roundId = fundingPot.roundCount(); uint8 accessCriteriaId = 1; ( @@ -1064,7 +961,8 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { 0, nftContract, merkleRoot, - allowedAddresses + allowedAddresses, + removedAddresses ); ( @@ -1096,7 +994,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { testFuzzSetAccessCriteria(oldAccessCriteriaEnum); - uint32 roundId = fundingPot.getRoundCount(); + uint32 roundId = fundingPot.roundCount(); ( address nftContract, bytes32 merkleRoot, @@ -1104,8 +1002,8 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { ) = _helper_createAccessCriteria(newAccessCriteriaEnum, roundId); vm.expectEmit(true, true, true, false); - emit ILM_PC_FundingPot_v1.AccessCriteriaEdited( - roundId, uint8(newAccessCriteriaEnum) + emit ILM_PC_FundingPot_v1.AccessUpdated( + true, roundId, uint8(newAccessCriteriaEnum) ); fundingPot.setAccessCriteria( roundId, @@ -1113,7 +1011,8 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { 1, nftContract, merkleRoot, - allowedAddresses + allowedAddresses, + removedAddresses ); } @@ -1136,7 +1035,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { */ function testRemoveAllowlistedAddresses() public { testCreateRound(); - uint32 roundId = fundingPot.getRoundCount(); + uint32 roundId = fundingPot.roundCount(); uint8 accessCriteriaId = 1; uint8 accessType = uint8(ILM_PC_FundingPot_v1.AccessCriteriaType.LIST); @@ -1147,7 +1046,13 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { ) = _helper_createAccessCriteria(accessType, roundId); fundingPot.setAccessCriteria( - roundId, accessType, 0, nftContract, merkleRoot, allowedAddresses + roundId, + accessType, + 0, + nftContract, + merkleRoot, + allowedAddresses, + removedAddresses ); address[] memory addressesToRemove = new address[](2); @@ -1172,6 +1077,76 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { assertTrue(otherAddressesHaveAccess); } + /* + ├── Given the round exists + | ├── Given an initial access criteria list with addresses [0x1, 0x2, 0x3] + │ │ └── When checking access for address 0x3 + │ │ └── Then access should be granted + │ │ + │ └── Given an update to the access criteria + │ ├── When adding new addresses [0x4, 0x5] + │ ├── And removing address [0x3] + │ │ └── Then access for address 0x3 should be revoked + │ └── And the final allowed list should contain [0x1, 0x2, 0x4, 0x5] + */ + function testRemoveAllowAddressesSetAccessCriteria() public { + testCreateRound(); + uint32 roundId = fundingPot.roundCount(); + + uint8 accessCriteriaId = 1; + uint8 accessType = uint8(ILM_PC_FundingPot_v1.AccessCriteriaType.LIST); + ( + address nftContract, + bytes32 merkleRoot, + address[] memory allowedAddresses + ) = _helper_createAccessCriteria(accessType, roundId); + + allowedAddresses = new address[](3); + allowedAddresses[0] = address(0x1); + allowedAddresses[1] = address(0x2); + allowedAddresses[2] = address(0x3); + + fundingPot.setAccessCriteria( + roundId, + accessType, + 0, + nftContract, + merkleRoot, + allowedAddresses, + removedAddresses + ); + + bool hasAccess = fundingPot.exposed_checkAccessCriteriaEligibility( + roundId, accessCriteriaId, new bytes32[](0), address(0x3) + ); + assertTrue(hasAccess); + //Admin wants to give access to two new users and removed one user + allowedAddresses = new address[](4); + allowedAddresses[0] = address(0x1); + allowedAddresses[1] = address(0x2); + allowedAddresses[2] = address(0x4); + allowedAddresses[3] = address(0x5); + + removedAddresses = new address[](1); + removedAddresses[0] = address(0x3); + + //Edit the AccessCriteria + fundingPot.setAccessCriteria( + roundId, + accessType, + 1, + nftContract, + merkleRoot, + allowedAddresses, + removedAddresses + ); + + hasAccess = fundingPot.exposed_checkAccessCriteriaEligibility( + roundId, accessCriteriaId, new bytes32[](0), address(0x3) + ); + assertFalse(hasAccess); + } + /* Test: setAccessCriteriaPrivileges() ├── Given user does not have FUNDING_POT_ADMIN_ROLE │ └── When user attempts to set access criteria privileges @@ -1213,7 +1188,8 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { 0, nftContract, merkleRoot, - allowedAddresses + allowedAddresses, + removedAddresses ); vm.startPrank(user_); @@ -1258,7 +1234,8 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { 0, nftContract, merkleRoot, - allowedAddresses + allowedAddresses, + removedAddresses ); fundingPot.setAccessCriteriaPrivileges( @@ -1286,7 +1263,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { ├── Given the access criteria does not exist │ └── When user attempts to get access criteria privileges │ └── Then it should return default values - + */ function testFuzzGetRoundAccessCriteriaPrivileges_returnsDefaultValuesGivenInvalidAccessCriteriaId( uint8 accessCriteriaEnum @@ -1355,20 +1332,33 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { │ │ └── When the user contributes to the round │ │ └── Then the transaction should revert │ │ - │ └── Given a user has already contributed up to their personal cap - │ └── When the user attempts to contribute again - │ └── Then the transaction should revert - │ + │ ├── Given a user has already contributed up to their personal cap + │ │ └── When the user attempts to contribute again + │ │ └── Then the transaction should revert + │ │ + │ ├── Given the user tries to use unspent caps not from a previous round(i.e. using the current or a future round's ID) + │ │ └── When the user attempts to contribute + │ │ └── Then the transaction should revert + │ │ + │ ├── Given the user tries to use unspent caps with round IDs that are not strictly increasing + │ │ └── When the user attempts to contribute + │ │ └── Then the transaction should revert + │ │ + │ ├── Given the user tries to use unspent caps with non-contiguous round IDs + │ │ └── When the user attempts to contribute + │ │ └── Then the transaction should revert + │ │ └── Given the round contribution cap is reached └── When the user attempts to contribute └── Then the transaction should revert + */ function testContributeToRoundFor_revertsGivenContributionIsBeforeRoundStart( ) public { testCreateRound(); - uint32 roundId = fundingPot.getRoundCount(); + uint32 roundId = fundingPot.roundCount(); uint8 accessCriteriaId = 1; uint8 accessType = uint8(ILM_PC_FundingPot_v1.AccessCriteriaType.OPEN); uint amount = 250; @@ -1380,7 +1370,13 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { ) = _helper_createAccessCriteria(accessType, roundId); fundingPot.setAccessCriteria( - roundId, accessType, 0, nftContract, merkleRoot, allowedAddresses + roundId, + accessType, + 0, + nftContract, + merkleRoot, + allowedAddresses, + removedAddresses ); fundingPot.setAccessCriteriaPrivileges( roundId, accessCriteriaId, 500, false, 0, 0, 0 @@ -1400,7 +1396,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { vm.prank(contributor1_); fundingPot.contributeToRoundFor( - contributor1_, roundId, amount, accessCriteriaId, new bytes32[](0) + contributor1_, roundId, amount, accessCriteriaId, new bytes32[](0), _unspentPersonalRoundCaps ); } @@ -1409,7 +1405,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { { testCreateRound(); - uint32 roundId = fundingPot.getRoundCount(); + uint32 roundId = fundingPot.roundCount(); uint8 accessCriteriaId = 1; uint8 accessType = uint8(ILM_PC_FundingPot_v1.AccessCriteriaType.OPEN); @@ -1422,7 +1418,13 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { ) = _helper_createAccessCriteria(accessType, roundId); fundingPot.setAccessCriteria( - roundId, accessType, 0, nftContract, merkleRoot, allowedAddresses + roundId, + accessType, + 0, + nftContract, + merkleRoot, + allowedAddresses, + removedAddresses ); fundingPot.setAccessCriteriaPrivileges( roundId, accessCriteriaId, 500, false, 0, 0, 0 @@ -1445,7 +1447,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { vm.prank(contributor1_); fundingPot.contributeToRoundFor( - contributor1_, roundId, amount, accessCriteriaId, new bytes32[](0) + contributor1_, roundId, amount, accessCriteriaId, new bytes32[](0), _unspentPersonalRoundCaps ); } @@ -1456,7 +1458,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { uint8 accessType = uint8(ILM_PC_FundingPot_v1.AccessCriteriaType.NFT); _helper_setupRoundWithAccessCriteria(accessType); - uint32 roundId = fundingPot.getRoundCount(); + uint32 roundId = fundingPot.roundCount(); uint amount = 250; @@ -1472,14 +1474,14 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { vm.expectRevert( abi.encodeWithSelector( ILM_PC_FundingPot_v1 - .Module__LM_PC_FundingPot__AccessCriteriaNftFailed + .Module__LM_PC_FundingPot__AccessCriteriaFailed .selector ) ); vm.prank(contributor1_); fundingPot.contributeToRoundFor( - contributor1_, roundId, amount, accessCriteriaId, new bytes32[](0) + contributor1_, roundId, amount, accessCriteriaId, new bytes32[](0), _unspentPersonalRoundCaps ); } @@ -1487,7 +1489,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { ) public { testCreateRound(); - uint32 roundId = fundingPot.getRoundCount(); + uint32 roundId = fundingPot.roundCount(); uint8 accessCriteriaId = 1; uint8 accessType = uint8(ILM_PC_FundingPot_v1.AccessCriteriaType.MERKLE); uint amount = 250; @@ -1503,7 +1505,13 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { ) = _helper_createAccessCriteria(accessType, roundId); fundingPot.setAccessCriteria( - roundId, accessType, 0, nftContract, merkleRoot, allowedAddresses + roundId, + accessType, + 0, + nftContract, + merkleRoot, + allowedAddresses, + removedAddresses ); fundingPot.setAccessCriteriaPrivileges( roundId, accessCriteriaId, 500, false, 0, 0, 0 @@ -1519,14 +1527,14 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { vm.expectRevert( abi.encodeWithSelector( ILM_PC_FundingPot_v1 - .Module__LM_PC_FundingPot__AccessCriteriaMerkleFailed + .Module__LM_PC_FundingPot__AccessCriteriaFailed .selector ) ); vm.prank(contributor1_); fundingPot.contributeToRoundFor( - contributor1_, roundId, amount, accessCriteriaId, proofB + contributor1_, roundId, amount, accessCriteriaId, proofB, _unspentPersonalRoundCaps ); } @@ -1534,7 +1542,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { ) public { testCreateRound(); - uint32 roundId = fundingPot.getRoundCount(); + uint32 roundId = fundingPot.roundCount(); uint8 accessCriteriaId = 1; uint8 accessType = uint8(ILM_PC_FundingPot_v1.AccessCriteriaType.LIST); uint amount = 250; @@ -1546,7 +1554,13 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { ) = _helper_createAccessCriteria(accessType, roundId); fundingPot.setAccessCriteria( - roundId, accessType, 0, nftContract, merkleRoot, allowedAddresses + roundId, + accessType, + 0, + nftContract, + merkleRoot, + allowedAddresses, + removedAddresses ); fundingPot.setAccessCriteriaPrivileges( roundId, accessCriteriaId, 500, false, 0, 0, 0 @@ -1562,86 +1576,224 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { vm.expectRevert( abi.encodeWithSelector( ILM_PC_FundingPot_v1 - .Module__LM_PC_FundingPot__AccessCriteriaListFailed + .Module__LM_PC_FundingPot__AccessCriteriaFailed .selector ) ); vm.prank(contributor1_); fundingPot.contributeToRoundFor( - contributor1_, roundId, amount, accessCriteriaId, new bytes32[](0) + contributor1_, roundId, amount, accessCriteriaId, new bytes32[](0), _unspentPersonalRoundCaps ); } - function testContributeToRoundFor_revertsGivenPreviousContributionExceedsPersonalCap( + function testContributeToRoundFor_revertsGivenUnspentCapsIsNotFromPreviousRounds( ) public { - testCreateRound(); + RoundParams memory params1 = _defaultRoundParams; + params1.accumulationMode = ILM_PC_FundingPot_v1.AccumulationMode.All; + + fundingPot.createRound( + params1.roundStart, + params1.roundEnd, + params1.roundCap, + params1.hookContract, + params1.hookFunction, + params1.autoClosure, + params1.accumulationMode + ); + uint32 round1Id = fundingPot.roundCount(); - uint32 roundId = fundingPot.getRoundCount(); uint8 accessCriteriaId = 1; uint8 accessType = uint8(ILM_PC_FundingPot_v1.AccessCriteriaType.NFT); - uint amount = 500; ( address nftContract, bytes32 merkleRoot, address[] memory allowedAddresses - ) = _helper_createAccessCriteria(accessType, roundId); + ) = _helper_createAccessCriteria(accessType, round1Id); fundingPot.setAccessCriteria( - roundId, accessType, 0, nftContract, merkleRoot, allowedAddresses + round1Id, + accessType, + 0, + nftContract, + merkleRoot, + allowedAddresses, + removedAddresses ); fundingPot.setAccessCriteriaPrivileges( - roundId, accessCriteriaId, 500, false, 0, 0, 0 + round1Id, accessCriteriaId, 500, false, 0, 0, 0 ); mockNFTContract.mint(contributor1_); - (uint roundStart,,,,,,) = fundingPot.getRoundGenericParameters(roundId); - vm.warp(roundStart + 1); + RoundParams memory params2 = _defaultRoundParams; + params2.roundStart = _defaultRoundParams.roundStart + 3 days; + params2.roundEnd = _defaultRoundParams.roundEnd + 3 days; + params2.accumulationMode = ILM_PC_FundingPot_v1.AccumulationMode.All; - // Approve - vm.prank(contributor1_); - _token.approve(address(fundingPot), 1000); + fundingPot.createRound( + params2.roundStart, + params2.roundEnd, + params2.roundCap, + params2.hookContract, + params2.hookFunction, + params2.autoClosure, + params2.accumulationMode + ); + uint32 round2Id = fundingPot.roundCount(); - vm.prank(contributor1_); - fundingPot.contributeToRoundFor( - contributor1_, roundId, amount, accessCriteriaId, new bytes32[](0) + fundingPot.setAccessCriteria( + round2Id, + accessType, + 0, + nftContract, + merkleRoot, + allowedAddresses, + removedAddresses + ); + fundingPot.setAccessCriteriaPrivileges( + round2Id, accessCriteriaId, 400, false, 0, 0, 0 ); - // Attempt to contribute beyond personal cap + vm.warp(params2.roundStart + 1); + + //Attempt to use current round's ID + ILM_PC_FundingPot_v1.UnspentPersonalRoundCap[] memory + invalidUnspentCaps1 = + new ILM_PC_FundingPot_v1.UnspentPersonalRoundCap[](1); + invalidUnspentCaps1[0] = ILM_PC_FundingPot_v1.UnspentPersonalRoundCap({ + roundId: round2Id, + accessCriteriaId: accessCriteriaId, + merkleProof: new bytes32[](0) + }); + + vm.startPrank(contributor1_); + _token.approve(address(fundingPot), 700); + vm.expectRevert( abi.encodeWithSelector( ILM_PC_FundingPot_v1 - .Module__LM_PC_FundingPot__PersonalCapReached + .Module__LM_PC_FundingPot__UnspentCapsMustBeFromPreviousRounds .selector ) ); - vm.prank(contributor1_); + fundingPot.contributeToRoundFor( + contributor1_, + round2Id, + 700, + accessCriteriaId, + new bytes32[](0), + invalidUnspentCaps1 + ); + + //Attempt to use future round's ID + uint32 round3Id = round2Id + 1; + + ILM_PC_FundingPot_v1.UnspentPersonalRoundCap[] memory + invalidUnspentCaps2 = + new ILM_PC_FundingPot_v1.UnspentPersonalRoundCap[](1); + invalidUnspentCaps2[0] = ILM_PC_FundingPot_v1.UnspentPersonalRoundCap({ + roundId: round3Id, + accessCriteriaId: accessCriteriaId, + merkleProof: new bytes32[](0) + }); + vm.expectRevert( + abi.encodeWithSelector( + ILM_PC_FundingPot_v1 + .Module__LM_PC_FundingPot__UnspentCapsMustBeFromPreviousRounds + .selector + ) + ); fundingPot.contributeToRoundFor( - contributor1_, roundId, 251, accessCriteriaId, new bytes32[](0) + contributor1_, + round2Id, + 700, + accessCriteriaId, + new bytes32[](0), + invalidUnspentCaps2 ); + + vm.stopPrank(); } - /* Test: contributeToRoundFor() happy paths - ├── Given a round has been configured with generic round configuration and access criteria - │ And the round has started - │ And the user fulfills the access criteria - │ And the user doesn't violate any privileges - │ And the user doesn't violate generic round parameters - │ And the user has approved the collateral token - │ └── When the user contributes to the round - │ └── Then the funds are transferred to the funding pot - │ And the contribution is recorded - │ - ├── Given the access criteria is NFT - │ And the user fulfills the access criteria - │ └── When the user contributes to the round - │ └── Then the funds are transferred to the funding pot - │ And the contribution is recorded + + function testContributeToRoundFor_revertsGivenPreviousContributionExceedsPersonalCap( + ) public { + testCreateRound(); + + uint32 roundId = fundingPot.roundCount(); + uint8 accessCriteriaId = 1; + uint8 accessType = uint8(ILM_PC_FundingPot_v1.AccessCriteriaType.NFT); + uint amount = 500; + + ( + address nftContract, + bytes32 merkleRoot, + address[] memory allowedAddresses + ) = _helper_createAccessCriteria(accessType, roundId); + + fundingPot.setAccessCriteria( + roundId, + accessType, + 0, + nftContract, + merkleRoot, + allowedAddresses, + removedAddresses + ); + fundingPot.setAccessCriteriaPrivileges( + roundId, accessCriteriaId, 500, false, 0, 0, 0 + ); + + mockNFTContract.mint(contributor1_); + + (uint roundStart,,,,,,) = fundingPot.getRoundGenericParameters(roundId); + vm.warp(roundStart + 1); + + // Approve + vm.prank(contributor1_); + _token.approve(address(fundingPot), 1000); + + vm.prank(contributor1_); + fundingPot.contributeToRoundFor( + contributor1_, roundId, amount, accessCriteriaId, new bytes32[](0), _unspentPersonalRoundCaps + ); + + // Attempt to contribute beyond personal cap + vm.expectRevert( + abi.encodeWithSelector( + ILM_PC_FundingPot_v1 + .Module__LM_PC_FundingPot__PersonalCapReached + .selector + ) + ); + vm.prank(contributor1_); + + fundingPot.contributeToRoundFor( + contributor1_, roundId, 251, accessCriteriaId, new bytes32[](0), _unspentPersonalRoundCaps + ); + } + + /* Test: contributeToRoundFor() happy paths + ├── Given a round has been configured with generic round configuration and access criteria + │ And the round has started + │ And the user fulfills the access criteria + │ And the user doesn't violate any privileges + │ And the user doesn't violate generic round parameters + │ And the user has approved the collateral token + │ └── When the user contributes to the round + │ └── Then the funds are transferred to the funding pot + │ And the contribution is recorded + │ + ├── Given the access criteria is NFT + │ And the user fulfills the access criteria + │ └── When the user contributes to the round + │ └── Then the funds are transferred to the funding pot + │ And the contribution is recorded │ - ├── Given the access criteria is MERKLE + ├── Given the access criteria is MERKLE │ And the user fulfills the access criteria │ └── When the user contributes to the round │ └── Then the funds are transferred to the funding pot @@ -1711,6 +1863,13 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { │ └── When calculating effective total cap │ └── Then unspent total capacity from all applicable previous rounds should expand the effective cap │ + └── Given the user has unspent caps from previous contiguous rounds + │ └── When the user attempts to contribute using valid unspent caps from previous rounds + │ └── Then the contribution should succeed + │ And the unspent caps should be applied to expand their effective personal cap + │ And the funds should be transferred to the funding pot + │ And the contribution should be recorded + │ ├── Given target round's AccumulationMode is Disabled │ └── When globalAccumulationStartRoundId is set to allow previous rounds │ └── Then no accumulation (personal or total) should occur for the target round @@ -1769,7 +1928,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { uint8 accessCriteriaId = 1; uint8 accessType = uint8(ILM_PC_FundingPot_v1.AccessCriteriaType.NFT); - uint32 roundId = fundingPot.getRoundCount(); + uint32 roundId = fundingPot.roundCount(); uint amount = 250; ( @@ -1779,21 +1938,19 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { ) = _helper_createAccessCriteria(accessType, roundId); fundingPot.setAccessCriteria( - roundId, accessType, 0, nftContract, merkleRoot, allowedAddresses + roundId, + accessType, + 0, + nftContract, + merkleRoot, + allowedAddresses, + removedAddresses ); fundingPot.setAccessCriteriaPrivileges( roundId, accessCriteriaId, 1000, false, 0, 0, 0 ); mockNFTContract.mint(contributor1_); - (bool isEligible, uint remainingAmountAllowedToContribute) = fundingPot - .getUserEligibility( - roundId, accessCriteriaId, new bytes32[](0), contributor1_ - ); - - assertTrue(isEligible); - assertEq(remainingAmountAllowedToContribute, 1000); - vm.warp(_defaultRoundParams.roundStart + 1); // Approve @@ -1807,15 +1964,16 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { vm.prank(contributor1_); fundingPot.contributeToRoundFor( - contributor1_, roundId, amount, accessCriteriaId, new bytes32[](0) + contributor1_, roundId, amount, accessCriteriaId, new bytes32[](0), _unspentPersonalRoundCaps ); - uint totalContributions = fundingPot.getTotalRoundContribution(roundId); + uint totalContributions = + fundingPot.roundIdToTotalContributions(roundId); assertEq(totalContributions, amount); uint personalContributions = - fundingPot.getUserContributionToRound(roundId, contributor1_); + fundingPot.roundIdToUserToContribution(roundId, contributor1_); assertEq(personalContributions, amount); } @@ -1831,7 +1989,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { uint8 accessCriteriaId = 1; _helper_setupRoundWithAccessCriteria(accessCriteriaId); - uint32 roundId = fundingPot.getRoundCount(); + uint32 roundId = fundingPot.roundCount(); mockNFTContract.mint(contributor1_); @@ -1848,15 +2006,16 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { emit ILM_PC_FundingPot_v1.ContributionMade(roundId, contributor1_, 250); fundingPot.contributeToRoundFor( - contributor1_, roundId, 250, accessCriteriaId, new bytes32[](0) + contributor1_, roundId, 250, accessCriteriaId, new bytes32[](0), _unspentPersonalRoundCaps ); vm.stopPrank(); uint userContribution = - fundingPot.getUserContributionToRound(roundId, contributor1_); + fundingPot.roundIdToUserToContribution(roundId, contributor1_); assertEq(userContribution, 250); - uint totalContributions = fundingPot.getTotalRoundContribution(roundId); + uint totalContributions = + fundingPot.roundIdToTotalContributions(roundId); assertEq(totalContributions, 250); } @@ -1868,7 +2027,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { _helper_setupRoundWithAccessCriteria(accessType); - uint32 roundId = fundingPot.getRoundCount(); + uint32 roundId = fundingPot.roundCount(); (,,,, bytes32[] memory proofB) = _helper_generateMerkleTreeForTwoLeaves( contributor1_, contributor2_, roundId @@ -1889,16 +2048,17 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { roundId, contributor2_, contributionAmount ); fundingPot.contributeToRoundFor( - contributor2_, roundId, contributionAmount, accessCriteriaId, proofB + contributor2_, roundId, contributionAmount, accessCriteriaId, proofB, _unspentPersonalRoundCaps ); vm.stopPrank(); uint userContribution = - fundingPot.getUserContributionToRound(roundId, contributor2_); + fundingPot.roundIdToUserToContribution(roundId, contributor2_); assertEq(userContribution, contributionAmount); - uint totalContributions = fundingPot.getTotalRoundContribution(roundId); + uint totalContributions = + fundingPot.roundIdToTotalContributions(roundId); assertEq(totalContributions, contributionAmount); } @@ -1927,7 +2087,13 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { ) = _helper_createAccessCriteria(accessType, roundId); fundingPot.setAccessCriteria( - roundId, accessType, 0, nftContract, merkleRoot, allowedAddresses + roundId, + accessType, + 0, + nftContract, + merkleRoot, + allowedAddresses, + removedAddresses ); uint personalCap = 200; @@ -1943,31 +2109,31 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { vm.startPrank(contributor1_); _token.approve(address(fundingPot), 100); fundingPot.contributeToRoundFor( - contributor1_, roundId, 100, accessCriteriaId, new bytes32[](0) + contributor1_, roundId, 100, accessCriteriaId, new bytes32[](0), _unspentPersonalRoundCaps ); vm.stopPrank(); vm.startPrank(contributor2_); _token.approve(address(fundingPot), 100); fundingPot.contributeToRoundFor( - contributor2_, roundId, 100, accessCriteriaId, new bytes32[](0) + contributor2_, roundId, 100, accessCriteriaId, new bytes32[](0), _unspentPersonalRoundCaps ); vm.stopPrank(); uint contribution = - fundingPot.getUserContributionToRound(roundId, contributor2_); + fundingPot.roundIdToUserToContribution(roundId, contributor2_); assertEq(contribution, 50); - uint totalContribution = fundingPot.getTotalRoundContribution(roundId); + uint totalContribution = fundingPot.roundIdToTotalContributions(roundId); assertEq(totalContribution, _defaultRoundParams.roundCap); - assertTrue(fundingPot.isRoundClosed(roundId)); + assertTrue(fundingPot.roundIdToClosedStatus(roundId)); } function testContributeToRoundFor_worksGivenContributionPartiallyExceedingPersonalCap( ) public { testCreateRound(); - uint32 roundId = fundingPot.getRoundCount(); + uint32 roundId = fundingPot.roundCount(); uint8 accessCriteriaId = 1; uint8 accessType = uint8(ILM_PC_FundingPot_v1.AccessCriteriaType.NFT); @@ -1981,7 +2147,13 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { ) = _helper_createAccessCriteria(accessType, roundId); fundingPot.setAccessCriteria( - roundId, accessType, 0, nftContract, merkleRoot, allowedAddresses + roundId, + accessType, + 0, + nftContract, + merkleRoot, + allowedAddresses, + removedAddresses ); fundingPot.setAccessCriteriaPrivileges( roundId, accessCriteriaId, personalCap, false, 0, 0, 0 @@ -2008,7 +2180,8 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { roundId, firstAmount, accessCriteriaId, - new bytes32[](0) + new bytes32[](0), + _unspentPersonalRoundCaps ); uint secondAmount = 200; @@ -2025,11 +2198,12 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { roundId, secondAmount, accessCriteriaId, - new bytes32[](0) + new bytes32[](0), + _unspentPersonalRoundCaps ); uint totalContribution = - fundingPot.getUserContributionToRound(roundId, contributor1_); + fundingPot.roundIdToUserToContribution(roundId, contributor1_); assertEq(totalContribution, personalCap); } @@ -2039,7 +2213,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { { testCreateRound(); - uint32 roundId = fundingPot.getRoundCount(); + uint32 roundId = fundingPot.roundCount(); uint8 accessCriteriaId = 1; uint8 accessType = uint8(ILM_PC_FundingPot_v1.AccessCriteriaType.NFT); uint amount = 250; @@ -2051,7 +2225,13 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { ) = _helper_createAccessCriteria(accessType, roundId); fundingPot.setAccessCriteria( - roundId, accessType, 0, nftContract, merkleRoot, allowedAddresses + roundId, + accessType, + 0, + nftContract, + merkleRoot, + allowedAddresses, + removedAddresses ); // Set privileges with override capability @@ -2078,11 +2258,11 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { // This should succeed despite being after round end, due to override privilege vm.prank(contributor1_); fundingPot.contributeToRoundFor( - contributor1_, roundId, amount, accessCriteriaId, new bytes32[](0) + contributor1_, roundId, amount, accessCriteriaId, new bytes32[](0), _unspentPersonalRoundCaps ); // Verify the contribution was recorded - uint totalContribution = fundingPot.getTotalRoundContribution(roundId); + uint totalContribution = fundingPot.roundIdToTotalContributions(roundId); assertEq(totalContribution, amount); } @@ -2101,7 +2281,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { _defaultRoundParams.accumulationMode ); - uint32 round1Id = fundingPot.getRoundCount(); + uint32 round1Id = fundingPot.roundCount(); uint8 accessCriteriaId = 1; uint8 accessCriteriaType = @@ -2117,7 +2297,8 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { 0, nftContract, merkleRoot, - allowedAddresses + allowedAddresses, + removedAddresses ); fundingPot.setAccessCriteriaPrivileges( @@ -2135,7 +2316,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { _defaultRoundParams.autoClosure, _defaultRoundParams.accumulationMode ); - uint32 round2Id = fundingPot.getRoundCount(); + uint32 round2Id = fundingPot.roundCount(); fundingPot.setAccessCriteria( round2Id, @@ -2143,7 +2324,8 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { 0, nftContract, merkleRoot, - allowedAddresses + allowedAddresses, + removedAddresses ); // Set personal cap of 400 for round 2 @@ -2155,7 +2337,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { vm.startPrank(contributor1_); _token.approve(address(fundingPot), 1000); fundingPot.contributeToRoundFor( - contributor1_, round1Id, 200, accessCriteriaId, new bytes32[](0) + contributor1_, round1Id, 200, accessCriteriaId, new bytes32[](0), _unspentPersonalRoundCaps ); // Warp to round 2 @@ -2185,11 +2367,11 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { vm.stopPrank(); assertEq( - fundingPot.getUserContributionToRound(round1Id, contributor1_), 200 + fundingPot.roundIdToUserToContribution(round1Id, contributor1_), 200 ); assertEq( - fundingPot.getUserContributionToRound(round2Id, contributor1_), 700 + fundingPot.roundIdToUserToContribution(round2Id, contributor1_), 700 ); } @@ -2209,7 +2391,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { _defaultRoundParams.autoClosure, _defaultRoundParams.accumulationMode ); - uint32 round1Id = fundingPot.getRoundCount(); + uint32 round1Id = fundingPot.roundCount(); uint8 accessCriteriaId = 1; uint8 accessType = uint8(ILM_PC_FundingPot_v1.AccessCriteriaType.OPEN); @@ -2220,7 +2402,13 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { address[] memory allowedAddresses ) = _helper_createAccessCriteria(accessType, round1Id); fundingPot.setAccessCriteria( - round1Id, accessType, 0, nftContract, merkleRoot, allowedAddresses + round1Id, + accessType, + 0, + nftContract, + merkleRoot, + allowedAddresses, + removedAddresses ); fundingPot.setAccessCriteriaPrivileges( round1Id, accessCriteriaId, 500, false, 0, 0, 0 @@ -2237,9 +2425,15 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { _defaultRoundParams.autoClosure, _defaultRoundParams.accumulationMode ); - uint32 round2Id = fundingPot.getRoundCount(); + uint32 round2Id = fundingPot.roundCount(); fundingPot.setAccessCriteria( - round2Id, accessType, 0, nftContract, merkleRoot, allowedAddresses + round2Id, + accessType, + 0, + nftContract, + merkleRoot, + allowedAddresses, + removedAddresses ); fundingPot.setAccessCriteriaPrivileges( round2Id, accessCriteriaId, 500, false, 0, 0, 0 @@ -2251,14 +2445,14 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { vm.startPrank(contributor1_); _token.approve(address(fundingPot), 300); fundingPot.contributeToRoundFor( - contributor1_, round1Id, 300, accessCriteriaId, new bytes32[](0) + contributor1_, round1Id, 300, accessCriteriaId, new bytes32[](0), _unspentPersonalRoundCaps ); vm.stopPrank(); vm.startPrank(contributor2_); _token.approve(address(fundingPot), 200); fundingPot.contributeToRoundFor( - contributor2_, round1Id, 200, accessCriteriaId, new bytes32[](0) + contributor2_, round1Id, 200, accessCriteriaId, new bytes32[](0), _unspentPersonalRoundCaps ); vm.stopPrank(); @@ -2268,34 +2462,34 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { vm.startPrank(contributor2_); _token.approve(address(fundingPot), 400); fundingPot.contributeToRoundFor( - contributor2_, round2Id, 400, accessCriteriaId, new bytes32[](0) + contributor2_, round2Id, 400, accessCriteriaId, new bytes32[](0), _unspentPersonalRoundCaps ); vm.stopPrank(); vm.startPrank(contributor3_); _token.approve(address(fundingPot), 300); fundingPot.contributeToRoundFor( - contributor3_, round2Id, 300, accessCriteriaId, new bytes32[](0) + contributor3_, round2Id, 300, accessCriteriaId, new bytes32[](0), _unspentPersonalRoundCaps ); vm.stopPrank(); - assertEq(fundingPot.getTotalRoundContribution(round1Id), 500); + assertEq(fundingPot.roundIdToTotalContributions(round1Id), 500); assertEq( - fundingPot.getUserContributionToRound(round1Id, contributor1_), 300 + fundingPot.roundIdToUserToContribution(round1Id, contributor1_), 300 ); assertEq( - fundingPot.getUserContributionToRound(round1Id, contributor2_), 200 + fundingPot.roundIdToUserToContribution(round1Id, contributor2_), 200 ); - assertEq(fundingPot.getTotalRoundContribution(round2Id), 700); + assertEq(fundingPot.roundIdToTotalContributions(round2Id), 700); assertEq( - fundingPot.getUserContributionToRound(round2Id, contributor2_), 400 + fundingPot.roundIdToUserToContribution(round2Id, contributor2_), 400 ); assertEq( - fundingPot.getUserContributionToRound(round2Id, contributor3_), 300 + fundingPot.roundIdToUserToContribution(round2Id, contributor3_), 300 ); - assertEq(fundingPot.getTotalRoundContribution(round2Id), 700); + assertEq(fundingPot.roundIdToTotalContributions(round2Id), 700); } function testContributeToRoundFor_globalStartRestrictsPersonalAccumulation() @@ -2329,7 +2523,13 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { ILM_PC_FundingPot_v1.AccumulationMode.Personal ); fundingPot.setAccessCriteria( - round1Id, 1, 0, address(0), bytes32(0), new address[](0) + round1Id, + 1, + 0, + address(0), + bytes32(0), + new address[](0), + removedAddresses ); // Open access fundingPot.setAccessCriteriaPrivileges( round1Id, 1, r1PersonalCap, false, 0, 0, 0 @@ -2346,7 +2546,13 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { ILM_PC_FundingPot_v1.AccumulationMode.Personal ); fundingPot.setAccessCriteria( - round2Id, 1, 0, address(0), bytes32(0), new address[](0) + round2Id, + 1, + 0, + address(0), + bytes32(0), + new address[](0), + removedAddresses ); fundingPot.setAccessCriteriaPrivileges( round2Id, 1, r2PersonalCap, false, 0, 0, 0 @@ -2363,7 +2569,13 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { ILM_PC_FundingPot_v1.AccumulationMode.Personal ); fundingPot.setAccessCriteria( - round3Id, 1, 0, address(0), bytes32(0), new address[](0) + round3Id, + 1, + 0, + address(0), + bytes32(0), + new address[](0), + removedAddresses ); fundingPot.setAccessCriteriaPrivileges( round3Id, 1, r3BasePersonalCap, false, 0, 0, 0 @@ -2375,18 +2587,18 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { vm.warp(initialTimestamp + 1 days + 1 hours); // Enter Round 1 fundingPot.contributeToRoundFor( - contributor1_, round1Id, r1Contribution, 1, new bytes32[](0) + contributor1_, round1Id, r1Contribution, 1, new bytes32[](0), _unspentPersonalRoundCaps ); vm.warp(initialTimestamp + 3 days + 1 hours); // Enter Round 2 fundingPot.contributeToRoundFor( - contributor1_, round2Id, r2Contribution, 1, new bytes32[](0) + contributor1_, round2Id, r2Contribution, 1, new bytes32[](0), _unspentPersonalRoundCaps ); vm.stopPrank(); // --- Set Global Start --- fundingPot.setGlobalAccumulationStart(2); - assertEq(fundingPot.getGlobalAccumulationStartRoundId(), 2); + assertEq(fundingPot.globalAccumulationStartRoundId(), 2); // --- Attempt Contribution in Round 3 --- vm.warp(initialTimestamp + 5 days + 1 hours); // Enter Round 3 @@ -2418,7 +2630,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { // --- Assertion --- assertEq( - fundingPot.getUserContributionToRound(round3Id, contributor1_), + fundingPot.roundIdToUserToContribution(round3Id, contributor1_), expectedR3PersonalCap, "R3 personal contribution incorrect" ); @@ -2456,7 +2668,13 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { ILM_PC_FundingPot_v1.AccumulationMode.Total ); fundingPot.setAccessCriteria( - round1Id, 1, 0, address(0), bytes32(0), new address[](0) + round1Id, + 1, + 0, + address(0), + bytes32(0), + new address[](0), + removedAddresses ); // Open access fundingPot.setAccessCriteriaPrivileges( round1Id, 1, r1BaseCap, false, 0, 0, 0 @@ -2473,7 +2691,13 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { ILM_PC_FundingPot_v1.AccumulationMode.Total ); fundingPot.setAccessCriteria( - round2Id, 1, 0, address(0), bytes32(0), new address[](0) + round2Id, + 1, + 0, + address(0), + bytes32(0), + new address[](0), + removedAddresses ); fundingPot.setAccessCriteriaPrivileges( round2Id, 1, r2BaseCap, false, 0, 0, 0 @@ -2490,7 +2714,13 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { ILM_PC_FundingPot_v1.AccumulationMode.Total ); fundingPot.setAccessCriteria( - round3Id, 1, 0, address(0), bytes32(0), new address[](0) + round3Id, + 1, + 0, + address(0), + bytes32(0), + new address[](0), + removedAddresses ); fundingPot.setAccessCriteriaPrivileges( round3Id, 1, r3BaseCap + r2Contribution, false, 0, 0, 0 @@ -2502,18 +2732,18 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { vm.warp(initialTimestamp + 1 days + 1 hours); // Enter Round 1 fundingPot.contributeToRoundFor( - contributor1_, round1Id, r1Contribution, 1, new bytes32[](0) + contributor1_, round1Id, r1Contribution, 1, new bytes32[](0), _unspentPersonalRoundCaps ); vm.warp(initialTimestamp + 3 days + 1 hours); // Enter Round 2 fundingPot.contributeToRoundFor( - contributor1_, round2Id, r2Contribution, 1, new bytes32[](0) + contributor1_, round2Id, r2Contribution, 1, new bytes32[](0), _unspentPersonalRoundCaps ); vm.stopPrank(); // --- Set Global Start --- fundingPot.setGlobalAccumulationStart(2); - assertEq(fundingPot.getGlobalAccumulationStartRoundId(), 2); + assertEq(fundingPot.globalAccumulationStartRoundId(), 2); // --- Attempt Contribution in Round 3 --- vm.warp(initialTimestamp + 5 days + 1 hours); // Enter Round 3 @@ -2522,19 +2752,20 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { vm.startPrank(contributor1_); // Attempt to contribute up to the expected new effective cap + fundingPot.contributeToRoundFor( - contributor1_, round3Id, expectedR3EffectiveCap, 1, new bytes32[](0) + contributor1_, round3Id, expectedR3EffectiveCap, 1, new bytes32[](0), _unspentPersonalRoundCaps ); vm.stopPrank(); // --- Assertion --- assertEq( - fundingPot.getTotalRoundContribution(round3Id), + fundingPot.roundIdToTotalContributions(round3Id), expectedR3EffectiveCap, "R3 total contribution incorrect, effective cap not as expected" ); assertEq( - fundingPot.getUserContributionToRound(round3Id, contributor1_), + fundingPot.roundIdToUserToContribution(round3Id, contributor1_), expectedR3EffectiveCap, "R3 user contribution incorrect" ); @@ -2545,23 +2776,24 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { // SCENARIO: Default globalAccumulationStartRoundId = 1 allows accumulation from R1 for R2 (Personal mode) // 1. Setup: Round 1 (Personal), Round 2 (Personal). // Partial contribution by C1 in R1. - // 2. Action: Verify getGlobalAccumulationStartRoundId() == 1 (default). + // 2. Action: Verify globalAccumulationStartRoundId() == 1 (default). // 3. Verification: For C1's contribution to R2, unused personal capacity from R1 rolls over. - uint initialTimestamp = block.timestamp; - uint8 accessId = 1; // Open access + // --- Round Parameters & Contributions for C1 --- uint r1PersonalCapC1 = 500; uint r1ContributionC1 = 100; // C1 leaves 400 personal unused from R1 uint r2BasePersonalCapC1 = 300; // C1's base personal cap in R2 + ILM_PC_FundingPot_v1.UnspentPersonalRoundCap[] memory + unspentPersonalRoundCaps = new ILM_PC_FundingPot_v1.UnspentPersonalRoundCap[](0); // --- Approvals --- vm.startPrank(contributor1_); _token.approve(address(fundingPot), type(uint).max); vm.stopPrank(); - + uint initialTimestamp = block.timestamp; // --- Create Round 1 (Personal Mode) --- uint32 round1Id = fundingPot.createRound( initialTimestamp + 1 days, @@ -2573,10 +2805,16 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { ILM_PC_FundingPot_v1.AccumulationMode.Personal ); fundingPot.setAccessCriteria( - round1Id, accessId, 0, address(0), bytes32(0), new address[](0) + round1Id, + 1, + 0, + address(0), + bytes32(0), + new address[](0), + removedAddresses ); fundingPot.setAccessCriteriaPrivileges( - round1Id, accessId, r1PersonalCapC1, false, 0, 0, 0 + round1Id, 1, r1PersonalCapC1, false, 0, 0, 0 ); // --- Create Round 2 (Personal Mode) --- @@ -2590,10 +2828,16 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { ILM_PC_FundingPot_v1.AccumulationMode.Personal ); fundingPot.setAccessCriteria( - round2Id, accessId, 0, address(0), bytes32(0), new address[](0) + round2Id, + 1, + 0, + address(0), + bytes32(0), + new address[](0), + removedAddresses ); fundingPot.setAccessCriteriaPrivileges( - round2Id, accessId, r2BasePersonalCapC1, false, 0, 0, 0 + round2Id, 1, r2BasePersonalCapC1, false, 0, 0, 0 ); // --- Contribution by C1 to Round 1 --- @@ -2603,14 +2847,15 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { contributor1_, round1Id, r1ContributionC1, - accessId, - new bytes32[](0) + 1, + new bytes32[](0), + unspentPersonalRoundCaps ); vm.stopPrank(); // --- Verify Default Global Start Round ID --- assertEq( - fundingPot.getGlobalAccumulationStartRoundId(), + fundingPot.globalAccumulationStartRoundId(), 1, "Default global start round ID should be 1" ); @@ -2622,7 +2867,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { new ILM_PC_FundingPot_v1.UnspentPersonalRoundCap[](1); unspentCapsC1[0] = ILM_PC_FundingPot_v1.UnspentPersonalRoundCap( round1Id, - accessId, + 1, new bytes32[](0) // Should be counted ); @@ -2632,7 +2877,6 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { uint expectedC1EffectivePersonalCapR2 = r2BasePersonalCapC1 + r1UnusedPersonalC1; - uint c1AttemptR2 = expectedC1EffectivePersonalCapR2 + 50; // Try to contribute slightly more uint expectedC1ContributionR2 = expectedC1EffectivePersonalCapR2; // Should be clamped // Ensure the attempt is not clamped by the round cap (which is large) @@ -2645,8 +2889,8 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { fundingPot.contributeToRoundFor( contributor1_, round2Id, - c1AttemptR2, - accessId, + r2BasePersonalCapC1 + r1UnusedPersonalC1 + 50, + 1, new bytes32[](0), unspentCapsC1 ); @@ -2654,7 +2898,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { // --- Assertion --- assertEq( - fundingPot.getUserContributionToRound(round2Id, contributor1_), + fundingPot.roundIdToUserToContribution(round2Id, contributor1_), expectedC1ContributionR2, "R2 C1 personal contribution incorrect (should use R1 unused)" ); @@ -2671,7 +2915,6 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { // --- Round 1 Parameters & Contribution --- uint r1BaseCap = 1000; uint r1ContributionC1 = 600; // Leaves 400 unused total from R1 - uint r1PersonalCap = 1000; // --- Round 2 Parameters --- uint r2BaseCap = 500; @@ -2692,24 +2935,32 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { ILM_PC_FundingPot_v1.AccumulationMode.Total ); fundingPot.setAccessCriteria( - round1Id, accessId, 0, address(0), bytes32(0), new address[](0) + round1Id, + accessId, + 0, + address(0), + bytes32(0), + new address[](0), + removedAddresses ); fundingPot.setAccessCriteriaPrivileges( - round1Id, accessId, r1PersonalCap, false, 0, 0, 0 + round1Id, accessId, r1BaseCap, false, 0, 0, 0 ); // --- Contribution by C1 to Round 1 --- vm.warp(initialTimestamp + 1 days + 1 hours); vm.startPrank(contributor1_); + ILM_PC_FundingPot_v1.UnspentPersonalRoundCap[] memory + unspentPersonalRoundCaps = new ILM_PC_FundingPot_v1.UnspentPersonalRoundCap[](0); fundingPot.contributeToRoundFor( contributor1_, round1Id, r1ContributionC1, accessId, - new bytes32[](0) + new bytes32[](0), + _unspentPersonalRoundCaps ); vm.stopPrank(); - uint r1UnusedTotal = r1BaseCap - r1ContributionC1; // Should be 400 // --- Create Round 2 (Total Mode) --- uint32 round2Id = fundingPot.createRound( @@ -2722,17 +2973,23 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { ILM_PC_FundingPot_v1.AccumulationMode.Total ); fundingPot.setAccessCriteria( - round2Id, accessId, 0, address(0), bytes32(0), new address[](0) + round2Id, + accessId, + 0, + address(0), + bytes32(0), + new address[](0), + removedAddresses ); // Set personal cap for R2 to be at least the expected effective total cap - uint r2ExpectedEffectiveTotalCap = r2BaseCap + r1UnusedTotal; // 500 + 400 = 900 + uint r2ExpectedEffectiveTotalCap = r2BaseCap + r1BaseCap - r1ContributionC1; // 500 + 400 = 900 fundingPot.setAccessCriteriaPrivileges( round2Id, accessId, r2ExpectedEffectiveTotalCap, false, 0, 0, 0 ); // --- Verify Default Global Start Round ID --- assertEq( - fundingPot.getGlobalAccumulationStartRoundId(), + fundingPot.globalAccumulationStartRoundId(), 1, "Default global start round ID should be 1" ); @@ -2751,18 +3008,18 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { vm.startPrank(contributor1_); fundingPot.contributeToRoundFor( - contributor1_, round2Id, c1AttemptR2, accessId, new bytes32[](0) + contributor1_, round2Id, c1AttemptR2, accessId, new bytes32[](0), _unspentPersonalRoundCaps ); vm.stopPrank(); // --- Assertions --- assertEq( - fundingPot.getUserContributionToRound(round2Id, contributor1_), + fundingPot.roundIdToUserToContribution(round2Id, contributor1_), c1AttemptR2, "R2 C1 contribution incorrect" ); assertEq( - fundingPot.getTotalRoundContribution(round2Id), + fundingPot.roundIdToTotalContributions(round2Id), c1AttemptR2, "R2 Total contributions after C1 incorrect" ); @@ -2776,13 +3033,14 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { round2Id, remainingToFill, accessId, - new bytes32[](0) + new bytes32[](0), + unspentPersonalRoundCaps ); vm.stopPrank(); } assertEq( - fundingPot.getTotalRoundContribution(round2Id), + fundingPot.roundIdToTotalContributions(round2Id), r2ExpectedEffectiveTotalCap, "R2 final total contributions should match effective total cap" ); @@ -2822,7 +3080,13 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { ILM_PC_FundingPot_v1.AccumulationMode.Personal ); fundingPot.setAccessCriteria( - round1Id, accessId, 0, address(0), bytes32(0), new address[](0) + round1Id, + accessId, + 0, + address(0), + bytes32(0), + new address[](0), + removedAddresses ); fundingPot.setAccessCriteriaPrivileges( round1Id, accessId, r1PersonalCapC1, false, 0, 0, 0 @@ -2836,7 +3100,8 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { round1Id, r1ContributionC1, accessId, - new bytes32[](0) + new bytes32[](0), + _unspentPersonalRoundCaps ); vm.stopPrank(); @@ -2851,7 +3116,13 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { ILM_PC_FundingPot_v1.AccumulationMode.Disabled ); fundingPot.setAccessCriteria( - round2Id, accessId, 0, address(0), bytes32(0), new address[](0) + round2Id, + accessId, + 0, + address(0), + bytes32(0), + new address[](0), + removedAddresses ); fundingPot.setAccessCriteriaPrivileges( round2Id, accessId, r2BasePersonalCapC1, false, 0, 0, 0 @@ -2860,7 +3131,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { // --- Set Global Start Round ID to allow R1 (to show it's ignored by R2's Disabled mode) --- fundingPot.setGlobalAccumulationStart(1); assertEq( - fundingPot.getGlobalAccumulationStartRoundId(), + fundingPot.globalAccumulationStartRoundId(), 1, "Global start round ID should be 1" ); @@ -2889,132 +3160,284 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { // --- Assertions --- assertEq( - fundingPot.getUserContributionToRound(round2Id, contributor1_), + fundingPot.roundIdToUserToContribution(round2Id, contributor1_), r2BasePersonalCapC1, "R2 C1 personal contribution should be clamped by R2's base personal cap (Disabled mode)" ); assertEq( - fundingPot.getTotalRoundContribution(round2Id), + fundingPot.roundIdToTotalContributions(round2Id), r2BasePersonalCapC1, "R2 Total contributions should not be expanded by R1 (Disabled mode)" ); assertTrue( - fundingPot.getTotalRoundContribution(round2Id) <= r2BaseCap, + fundingPot.roundIdToTotalContributions(round2Id) <= r2BaseCap, "R2 Total contributions exceeded R2's original base cap (Disabled mode)" ); } - function testContributeToRoundFor_noAccumulationWhenGlobalStartEqualsTargetRound( + function testContributeToRoundFor_worksGivenUnspentCapsWithContiguousRoundIds( ) public { - // SCENARIO: If globalAccumulationStartRoundId is set to the target round's ID (R2), - // no accumulation from any previous round (R1) occurs for R2, even if R2's mode would allow it. - - uint initialTimestamp = block.timestamp; - uint8 accessId = 1; // Open access - - // --- Round 1 Parameters --- - uint r1PersonalCapC1 = 500; - uint r1ContributionC1 = 100; - uint r1BaseCap = 1000; - - // --- Round 2 Parameters (Mode that would normally allow accumulation, e.g., Personal) --- - uint r2BasePersonalCapC1 = 50; - uint r2BaseCap = 200; - - // --- Approvals --- - vm.startPrank(contributor1_); - _token.approve(address(fundingPot), type(uint).max); - vm.stopPrank(); + uint8 accessCriteriaId = 1; + uint personalCap = 300; - // --- Create Round 1 (Personal Mode to generate unused personal capacity) --- + // Round 1 - All accumulation enabled uint32 round1Id = fundingPot.createRound( - initialTimestamp + 1 days, - initialTimestamp + 2 days, - r1BaseCap, + block.timestamp + 1 days, + block.timestamp + 2 days, + 1000, address(0), bytes(""), false, - ILM_PC_FundingPot_v1.AccumulationMode.Personal - ); - fundingPot.setAccessCriteria( - round1Id, accessId, 0, address(0), bytes32(0), new address[](0) - ); - fundingPot.setAccessCriteriaPrivileges( - round1Id, accessId, r1PersonalCapC1, false, 0, 0, 0 + ILM_PC_FundingPot_v1.AccumulationMode.All ); - - // --- Contribution by C1 to Round 1 --- - vm.warp(initialTimestamp + 1 days + 1 hours); - vm.startPrank(contributor1_); - fundingPot.contributeToRoundFor( - contributor1_, + _helper_setupAccessCriteriaForRound( round1Id, - r1ContributionC1, - accessId, - new bytes32[](0) + uint8(ILM_PC_FundingPot_v1.AccessCriteriaType.OPEN), + accessCriteriaId, + personalCap ); - vm.stopPrank(); - // --- Create Round 2 (Personal Mode - would normally allow accumulation from R1) --- + // Round 2 - Personal accumulation enabled uint32 round2Id = fundingPot.createRound( - initialTimestamp + 3 days, - initialTimestamp + 4 days, - r2BaseCap, + block.timestamp + 3 days, + block.timestamp + 4 days, + 1000, address(0), bytes(""), false, ILM_PC_FundingPot_v1.AccumulationMode.Personal ); - fundingPot.setAccessCriteria( - round2Id, accessId, 0, address(0), bytes32(0), new address[](0) - ); - fundingPot.setAccessCriteriaPrivileges( - round2Id, accessId, r2BasePersonalCapC1, false, 0, 0, 0 - ); - - // --- Set Global Start Round ID to be Round 2's ID --- - fundingPot.setGlobalAccumulationStart(round2Id); - assertEq( - fundingPot.getGlobalAccumulationStartRoundId(), + _helper_setupAccessCriteriaForRound( round2Id, - "Global start round ID not set to R2 ID" + uint8(ILM_PC_FundingPot_v1.AccessCriteriaType.OPEN), + accessCriteriaId, + personalCap ); - // --- Attempt Contribution in Round 2 by C1 --- - vm.warp(initialTimestamp + 3 days + 1 hours); + // Round 3 - Target round with personal accumulation + uint32 round3Id = fundingPot.createRound( + block.timestamp + 5 days, + block.timestamp + 6 days, + 1000, + address(0), + bytes(""), + false, + ILM_PC_FundingPot_v1.AccumulationMode.Personal + ); + _helper_setupAccessCriteriaForRound( + round3Id, + uint8(ILM_PC_FundingPot_v1.AccessCriteriaType.OPEN), + accessCriteriaId, + personalCap + ); - uint c1AttemptR2 = r2BasePersonalCapC1 + 100; + // Contribute to previous rounds + vm.warp(block.timestamp + 1 days + 1 hours); // Enter Round 1 + vm.startPrank(contributor1_); + _token.approve(address(fundingPot), 1000); - ILM_PC_FundingPot_v1.UnspentPersonalRoundCap[] memory unspentCapsC1 = - new ILM_PC_FundingPot_v1.UnspentPersonalRoundCap[](1); - unspentCapsC1[0] = ILM_PC_FundingPot_v1.UnspentPersonalRoundCap( - round1Id, accessId, new bytes32[](0) + fundingPot.contributeToRoundFor( + contributor1_, + round1Id, + 100, // Contributed 100 out of 300 cap + accessCriteriaId, + new bytes32[](0), + _unspentPersonalRoundCaps ); + vm.stopPrank(); + vm.warp(block.timestamp + 2 days); // Enter Round 2 (3 days total from start) vm.startPrank(contributor1_); fundingPot.contributeToRoundFor( contributor1_, round2Id, - c1AttemptR2, - accessId, + 150, // Contributed 150 out of 300 cap + accessCriteriaId, new bytes32[](0), - unspentCapsC1 + _unspentPersonalRoundCaps + ); + vm.stopPrank(); + + // Now contribute to round 3 using unspent caps from previous rounds + vm.warp(block.timestamp + 2 days); // Enter Round 3 (5 days total from start) + + // Create unspent caps array for rounds 1 and 2 + ILM_PC_FundingPot_v1.UnspentPersonalRoundCap[] memory unspentCaps = + new ILM_PC_FundingPot_v1.UnspentPersonalRoundCap[](2); + unspentCaps[0] = ILM_PC_FundingPot_v1.UnspentPersonalRoundCap({ + roundId: round1Id, + accessCriteriaId: accessCriteriaId, + merkleProof: new bytes32[](0) + }); + unspentCaps[1] = ILM_PC_FundingPot_v1.UnspentPersonalRoundCap({ + roundId: round2Id, + accessCriteriaId: accessCriteriaId, + merkleProof: new bytes32[](0) + }); + + vm.startPrank(contributor1_); + + // Should be able to contribute more than the base personal cap + // Round 1: 300 cap - 100 spent = 200 unused + // Round 2: 300 cap - 150 spent = 150 unused + // Total unspent = 350 + // Round 3 base cap = 300 + // Total effective cap for round 3 = 300 + 350 = 650 + + uint initialBalance = _token.balanceOf(contributor1_); + + fundingPot.contributeToRoundFor( + contributor1_, + round3Id, + 500, // Should work because effective cap is 650 + accessCriteriaId, + new bytes32[](0), + unspentCaps + ); + + vm.stopPrank(); + + // Verify the contribution was recorded + assertEq( + fundingPot.roundIdToUserToContribution(round3Id, contributor1_), + 500, + "User contribution should be 500" + ); + + // Verify tokens were transferred + assertEq( + _token.balanceOf(contributor1_), + initialBalance - 500, + "Tokens should have been transferred from contributor" + ); + } + + function testContributeToRoundFor_noAccumulationWhenGlobalStartEqualsTargetRound( + ) public { + // SCENARIO: If globalAccumulationStartRoundId is set to the target round's ID (R2), + // no accumulation from any previous round (R1) occurs for R2, even if R2's mode would allow it. + + uint initialTimestamp = block.timestamp; + uint8 accessId = 1; // Open access + + // --- Round 1 Parameters --- + uint r1PersonalCapC1 = 500; + uint r1ContributionC1 = 100; + uint r1BaseCap = 1000; + + // --- Round 2 Parameters (Mode that would normally allow accumulation, e.g., Personal) --- + uint r2BasePersonalCapC1 = 50; + uint r2BaseCap = 200; + + // --- Approvals --- + vm.startPrank(contributor1_); + _token.approve(address(fundingPot), type(uint).max); + vm.stopPrank(); + + // --- Create Round 1 (Personal Mode to generate unused personal capacity) --- + uint32 round1Id = fundingPot.createRound( + initialTimestamp + 1 days, + initialTimestamp + 2 days, + r1BaseCap, + address(0), + bytes(""), + false, + ILM_PC_FundingPot_v1.AccumulationMode.Personal + ); + fundingPot.setAccessCriteria( + round1Id, + accessId, + 0, + address(0), + bytes32(0), + new address[](0), + removedAddresses + ); + fundingPot.setAccessCriteriaPrivileges( + round1Id, accessId, r1PersonalCapC1, false, 0, 0, 0 + ); + + // --- Contribution by C1 to Round 1 --- + vm.warp(initialTimestamp + 1 days + 1 hours); + vm.startPrank(contributor1_); + ILM_PC_FundingPot_v1.UnspentPersonalRoundCap[] memory + unspentPersonalRoundCaps = new ILM_PC_FundingPot_v1.UnspentPersonalRoundCap[](0); + fundingPot.contributeToRoundFor( + contributor1_, + round1Id, + r1ContributionC1, + accessId, + new bytes32[](0), + unspentPersonalRoundCaps + ); + vm.stopPrank(); + + // --- Create Round 2 (Personal Mode - would normally allow accumulation from R1) --- + uint32 round2Id = fundingPot.createRound( + initialTimestamp + 3 days, + initialTimestamp + 4 days, + r2BaseCap, + address(0), + bytes(""), + false, + ILM_PC_FundingPot_v1.AccumulationMode.Personal + ); + fundingPot.setAccessCriteria( + round2Id, + accessId, + 0, + address(0), + bytes32(0), + new address[](0), + removedAddresses + ); + fundingPot.setAccessCriteriaPrivileges( + round2Id, accessId, r2BasePersonalCapC1, false, 0, 0, 0 + ); + + // --- Set Global Start Round ID to be Round 2's ID --- + fundingPot.setGlobalAccumulationStart(round2Id); + assertEq( + fundingPot.globalAccumulationStartRoundId(), + round2Id, + "Global start round ID not set to R2 ID" + ); + + // --- Attempt Contribution in Round 2 by C1 --- + vm.warp(initialTimestamp + 3 days + 1 hours); + + uint c1AttemptR2 = r2BasePersonalCapC1 + 100; + + ILM_PC_FundingPot_v1.UnspentPersonalRoundCap[] memory unspentCapsC1 = + new ILM_PC_FundingPot_v1.UnspentPersonalRoundCap[](1); + unspentCapsC1[0] = ILM_PC_FundingPot_v1.UnspentPersonalRoundCap( + round1Id, accessId, new bytes32[](0) + ); + + vm.startPrank(contributor1_); + fundingPot.contributeToRoundFor( + contributor1_, + round2Id, + c1AttemptR2, + accessId, + new bytes32[](0), + unspentCapsC1 ); vm.stopPrank(); // --- Assertions --- assertEq( - fundingPot.getUserContributionToRound(round2Id, contributor1_), + fundingPot.roundIdToUserToContribution(round2Id, contributor1_), r2BasePersonalCapC1, "R2 C1 personal contribution should be clamped by R2's base personal cap (global start = R2)" ); assertEq( - fundingPot.getTotalRoundContribution(round2Id), + fundingPot.roundIdToTotalContributions(round2Id), r2BasePersonalCapC1, "R2 Total contributions should not be expanded by R1 (global start = R2)" ); assertTrue( - fundingPot.getTotalRoundContribution(round2Id) <= r2BaseCap, + fundingPot.roundIdToTotalContributions(round2Id) <= r2BaseCap, "R2 Total contributions exceeded R2's original base cap (global start = R2)" ); } @@ -3056,7 +3479,13 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { ILM_PC_FundingPot_v1.AccumulationMode.Personal ); fundingPot.setAccessCriteria( - round1Id, 1, 0, address(0), bytes32(0), new address[](0) + round1Id, + 1, + 0, + address(0), + bytes32(0), + new address[](0), + removedAddresses ); fundingPot.setAccessCriteriaPrivileges( round1Id, 1, r1PersonalCapC1, false, 0, 0, 0 @@ -3066,11 +3495,11 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { vm.warp(initialTimestamp + 1 days + 1 hours); vm.startPrank(contributor1_); fundingPot.contributeToRoundFor( - contributor1_, round1Id, r1ContributionC1, 1, new bytes32[](0) + contributor1_, round1Id, r1ContributionC1, 1, new bytes32[](0), _unspentPersonalRoundCaps ); vm.stopPrank(); assertEq( - fundingPot.getUserContributionToRound(round1Id, contributor1_), + fundingPot.roundIdToUserToContribution(round1Id, contributor1_), r1ContributionC1 ); @@ -3085,7 +3514,13 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { ILM_PC_FundingPot_v1.AccumulationMode.Personal ); fundingPot.setAccessCriteria( - round2Id, 1, 0, address(0), bytes32(0), new address[](0) + round2Id, + 1, + 0, + address(0), + bytes32(0), + new address[](0), + removedAddresses ); fundingPot.setAccessCriteriaPrivileges( round2Id, 1, r2PersonalCapC1, false, 0, 0, 0 @@ -3095,11 +3530,11 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { vm.warp(initialTimestamp + 3 days + 1 hours); vm.startPrank(contributor1_); fundingPot.contributeToRoundFor( - contributor1_, round2Id, r2ContributionC1, 1, new bytes32[](0) + contributor1_, round2Id, r2ContributionC1, 1, new bytes32[](0), _unspentPersonalRoundCaps ); vm.stopPrank(); assertEq( - fundingPot.getUserContributionToRound(round2Id, contributor1_), + fundingPot.roundIdToUserToContribution(round2Id, contributor1_), r2ContributionC1 ); @@ -3114,7 +3549,13 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { ILM_PC_FundingPot_v1.AccumulationMode.Personal ); fundingPot.setAccessCriteria( - round3Id, 1, 0, address(0), bytes32(0), new address[](0) + round3Id, + 1, + 0, + address(0), + bytes32(0), + new address[](0), + removedAddresses ); fundingPot.setAccessCriteriaPrivileges( round3Id, 1, r3BasePersonalCapC1, false, 0, 0, 0 @@ -3122,7 +3563,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { // --- Verify Global Start Round ID --- assertEq( - fundingPot.getGlobalAccumulationStartRoundId(), + fundingPot.globalAccumulationStartRoundId(), 1, "Default global start round ID should be 1" ); @@ -3158,12 +3599,12 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { // --- Assertions --- assertEq( - fundingPot.getUserContributionToRound(round3Id, contributor1_), + fundingPot.roundIdToUserToContribution(round3Id, contributor1_), expectedR3PersonalCapC1, "R3 C1 personal contribution incorrect (should use R1 & R2 unused)" ); assertEq( - fundingPot.getTotalRoundContribution(round3Id), + fundingPot.roundIdToTotalContributions(round3Id), expectedR3PersonalCapC1, "R3 total contributions incorrect after C1" ); @@ -3225,7 +3666,13 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { ILM_PC_FundingPot_v1.AccumulationMode.Total ); fundingPot.setAccessCriteria( - round1Id, 1, 0, address(0), bytes32(0), new address[](0) + round1Id, + 1, + 0, + address(0), + bytes32(0), + new address[](0), + removedAddresses ); fundingPot.setAccessCriteriaPrivileges( round1Id, 1, r1BaseCap, false, 0, 0, 0 @@ -3234,12 +3681,14 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { // --- Contribution by C1 to Round 1 --- vm.warp(initialTimestamp + 1 days + 1 hours); vm.startPrank(contributor1_); + ILM_PC_FundingPot_v1.UnspentPersonalRoundCap[] memory + unspentPersonalRoundCaps = new ILM_PC_FundingPot_v1.UnspentPersonalRoundCap[](0); fundingPot.contributeToRoundFor( - contributor1_, round1Id, r1ContributionC1, 1, new bytes32[](0) + contributor1_, round1Id, r1ContributionC1, 1, new bytes32[](0), unspentPersonalRoundCaps ); vm.stopPrank(); assertEq( - fundingPot.getTotalRoundContribution(round1Id), r1ContributionC1 + fundingPot.roundIdToTotalContributions(round1Id), r1ContributionC1 ); // --- Create Round 2 (Total Mode) --- @@ -3253,7 +3702,13 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { ILM_PC_FundingPot_v1.AccumulationMode.Total ); fundingPot.setAccessCriteria( - round2Id, 1, 0, address(0), bytes32(0), new address[](0) + round2Id, + 1, + 0, + address(0), + bytes32(0), + new address[](0), + removedAddresses ); fundingPot.setAccessCriteriaPrivileges( round2Id, 1, r2BaseCap, false, 0, 0, 0 @@ -3262,12 +3717,13 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { // --- Contribution by C2 to Round 2 --- vm.warp(initialTimestamp + 3 days + 1 hours); vm.startPrank(contributor2_); + fundingPot.contributeToRoundFor( - contributor2_, round2Id, r2ContributionC2, 1, new bytes32[](0) + contributor2_, round2Id, r2ContributionC2, 1, new bytes32[](0), unspentPersonalRoundCaps ); vm.stopPrank(); assertEq( - fundingPot.getTotalRoundContribution(round2Id), r2ContributionC2 + fundingPot.roundIdToTotalContributions(round2Id), r2ContributionC2 ); // --- Create Round 3 (Total Mode) --- @@ -3285,14 +3741,15 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { (address nftR3, bytes32 merkleR3, address[] memory allowedR3) = _helper_createAccessCriteria(1, round3Id); - // TODO - fundingPot.setAccessCriteria(round3Id, 1, 0, nftR3, merkleR3, allowedR3); + fundingPot.setAccessCriteria( + round3Id, 1, 0, nftR3, merkleR3, allowedR3, removedAddresses + ); fundingPot.setAccessCriteriaPrivileges( round3Id, 1, r3ExpectedEffectiveCap, false, 0, 0, 0 ); assertEq( - fundingPot.getGlobalAccumulationStartRoundId(), + fundingPot.globalAccumulationStartRoundId(), 1, "Default global start round ID should be 1" ); @@ -3302,18 +3759,18 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { vm.startPrank(contributor3_); fundingPot.contributeToRoundFor( - contributor3_, round3Id, r3ExpectedEffectiveCap, 1, new bytes32[](0) + contributor3_, round3Id, r3ExpectedEffectiveCap, 1, new bytes32[](0), unspentPersonalRoundCaps ); vm.stopPrank(); // --- Assertions --- assertEq( - fundingPot.getTotalRoundContribution(round3Id), + fundingPot.roundIdToTotalContributions(round3Id), r3ExpectedEffectiveCap, "R3 total contributions should match effective cap with rollover from R1 and R2" ); assertEq( - fundingPot.getUserContributionToRound(round3Id, contributor3_), + fundingPot.roundIdToUserToContribution(round3Id, contributor3_), r3ExpectedEffectiveCap, "R3 C3 contribution incorrect" ); @@ -3328,7 +3785,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { ) ); fundingPot.contributeToRoundFor( - contributor1_, round3Id, 1, 1, new bytes32[](0) + contributor1_, round3Id, 1, 1, new bytes32[](0), unspentPersonalRoundCaps ); vm.stopPrank(); } @@ -3355,7 +3812,13 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { ILM_PC_FundingPot_v1.AccumulationMode.All ); fundingPot.setAccessCriteria( - round1Id, accessId, 0, address(0), bytes32(0), new address[](0) + round1Id, + accessId, + 0, + address(0), + bytes32(0), + new address[](0), + removedAddresses ); fundingPot.setAccessCriteriaPrivileges( round1Id, accessId, r1PersonalCapC1, false, 0, 0, 0 @@ -3364,12 +3827,15 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { vm.warp(initialTimestamp + 1 days + 1 hours); vm.startPrank(contributor1_); _token.approve(address(fundingPot), type(uint).max); + ILM_PC_FundingPot_v1.UnspentPersonalRoundCap[] memory + unspentPersonalRoundCaps = new ILM_PC_FundingPot_v1.UnspentPersonalRoundCap[](0); fundingPot.contributeToRoundFor( contributor1_, round1Id, r1ContributionC1, accessId, - new bytes32[](0) + new bytes32[](0), + unspentPersonalRoundCaps ); vm.stopPrank(); uint r1UnusedPersonalForC1 = r1PersonalCapC1 - r1ContributionC1; @@ -3386,7 +3852,13 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { ILM_PC_FundingPot_v1.AccumulationMode.All ); fundingPot.setAccessCriteria( - round2Id, accessId, 0, address(0), bytes32(0), new address[](0) + round2Id, + accessId, + 0, + address(0), + bytes32(0), + new address[](0), + removedAddresses ); fundingPot.setAccessCriteriaPrivileges( round2Id, accessId, r2BasePersonalCapC1, false, 0, 0, 0 @@ -3394,7 +3866,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { // --- Global Start ID Check --- assertEq( - fundingPot.getGlobalAccumulationStartRoundId(), + fundingPot.globalAccumulationStartRoundId(), 1, "Default global start ID is 1" ); @@ -3424,12 +3896,12 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { // --- Assertions --- assertEq( - fundingPot.getUserContributionToRound(round2Id, contributor1_), + fundingPot.roundIdToUserToContribution(round2Id, contributor1_), expectedEffectivePersonalCapC1R2, "R2 C1 personal contribution incorrect" ); assertEq( - fundingPot.getTotalRoundContribution(round2Id), + fundingPot.roundIdToTotalContributions(round2Id), expectedEffectivePersonalCapC1R2, "R2 Total contributions incorrect" ); @@ -3466,7 +3938,13 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { ILM_PC_FundingPot_v1.AccumulationMode.All ); fundingPot.setAccessCriteria( - round1Id, accessId, 0, address(0), bytes32(0), new address[](0) + round1Id, + accessId, + 0, + address(0), + bytes32(0), + new address[](0), + removedAddresses ); fundingPot.setAccessCriteriaPrivileges( round1Id, accessId, r1C1PersonalCap, false, 0, 0, 0 @@ -3475,12 +3953,16 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { // --- Contribution by C1 to Round 1 --- vm.warp(initialTimestamp + 1 days + 1 hours); vm.startPrank(contributor1_); + ILM_PC_FundingPot_v1.UnspentPersonalRoundCap[] memory + unspentPersonalRoundCaps = + new ILM_PC_FundingPot_v1.UnspentPersonalRoundCap[](0); fundingPot.contributeToRoundFor( contributor1_, round1Id, r1C1Contribution, accessId, - new bytes32[](0) + new bytes32[](0), + unspentPersonalRoundCaps ); vm.stopPrank(); uint r1UnusedTotal = r1BaseTotalCap - r1C1Contribution; @@ -3496,7 +3978,13 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { ILM_PC_FundingPot_v1.AccumulationMode.All ); fundingPot.setAccessCriteria( - round2Id, accessId, 0, address(0), bytes32(0), new address[](0) + round2Id, + accessId, + 0, + address(0), + bytes32(0), + new address[](0), + removedAddresses ); uint r2ExpectedEffectiveTotalCap = r2BaseTotalCap + r1UnusedTotal; fundingPot.setAccessCriteriaPrivileges( @@ -3505,7 +3993,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { // --- Ensure Global Start Round ID is 1 --- assertEq( - fundingPot.getGlobalAccumulationStartRoundId(), + fundingPot.globalAccumulationStartRoundId(), 1, "Global start round ID should be 1 by default" ); @@ -3517,18 +4005,18 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { vm.startPrank(contributor1_); fundingPot.contributeToRoundFor( - contributor1_, round2Id, c1AttemptR2, accessId, new bytes32[](0) + contributor1_, round2Id, c1AttemptR2, accessId, new bytes32[](0), unspentPersonalRoundCaps ); vm.stopPrank(); // --- Assertions --- assertEq( - fundingPot.getUserContributionToRound(round2Id, contributor1_), + fundingPot.roundIdToUserToContribution(round2Id, contributor1_), c1AttemptR2, "R2 C1 contribution should match attempt (filling effective total cap)" ); assertEq( - fundingPot.getTotalRoundContribution(round2Id), + fundingPot.roundIdToTotalContributions(round2Id), r2ExpectedEffectiveTotalCap, "R2 Total contributions should match effective total cap (All mode, global_start=1)" ); @@ -3567,7 +4055,13 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { ILM_PC_FundingPot_v1.AccumulationMode.All ); fundingPot.setAccessCriteria( - round1Id, accessId, 0, address(0), bytes32(0), new address[](0) + round1Id, + accessId, + 0, + address(0), + bytes32(0), + new address[](0), + removedAddresses ); fundingPot.setAccessCriteriaPrivileges( round1Id, accessId, r1PersonalCapC1, false, 0, 0, 0 @@ -3581,7 +4075,8 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { round1Id, r1ContributionC1, accessId, - new bytes32[](0) + new bytes32[](0), + _unspentPersonalRoundCaps ); vm.stopPrank(); @@ -3596,7 +4091,13 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { ILM_PC_FundingPot_v1.AccumulationMode.All ); fundingPot.setAccessCriteria( - round2Id, accessId, 0, address(0), bytes32(0), new address[](0) + round2Id, + accessId, + 0, + address(0), + bytes32(0), + new address[](0), + removedAddresses ); fundingPot.setAccessCriteriaPrivileges( round2Id, accessId, r2BasePersonalCapC1, false, 0, 0, 0 @@ -3605,7 +4106,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { // --- Set Global Start Round ID to Round 2's ID --- fundingPot.setGlobalAccumulationStart(round2Id); assertEq( - fundingPot.getGlobalAccumulationStartRoundId(), + fundingPot.globalAccumulationStartRoundId(), round2Id, "Global start ID not set to R2 ID" ); @@ -3634,12 +4135,12 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { // --- Assertions --- assertEq( - fundingPot.getUserContributionToRound(round2Id, contributor1_), + fundingPot.roundIdToUserToContribution(round2Id, contributor1_), r2BasePersonalCapC1, "R2 C1 personal contribution should be clamped by R2 base personal cap (All mode, global_start=R2)" ); assertEq( - fundingPot.getTotalRoundContribution(round2Id), + fundingPot.roundIdToTotalContributions(round2Id), r2BasePersonalCapC1, "R2 Total contributions should be C1's clamped amount (All mode, global_start=R2)" ); @@ -3677,7 +4178,13 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { ILM_PC_FundingPot_v1.AccumulationMode.All ); fundingPot.setAccessCriteria( - round1Id, accessId, 0, address(0), bytes32(0), new address[](0) + round1Id, + accessId, + 0, + address(0), + bytes32(0), + new address[](0), + removedAddresses ); fundingPot.setAccessCriteriaPrivileges( round1Id, accessId, r1C1PersonalCap, false, 0, 0, 0 @@ -3691,7 +4198,8 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { round1Id, r1C1Contribution, accessId, - new bytes32[](0) + new bytes32[](0), + _unspentPersonalRoundCaps ); vm.stopPrank(); @@ -3706,7 +4214,13 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { ILM_PC_FundingPot_v1.AccumulationMode.All ); fundingPot.setAccessCriteria( - round2Id, accessId, 0, address(0), bytes32(0), new address[](0) + round2Id, + accessId, + 0, + address(0), + bytes32(0), + new address[](0), + removedAddresses ); fundingPot.setAccessCriteriaPrivileges( round2Id, accessId, r2BaseTotalCap, false, 0, 0, 0 @@ -3715,7 +4229,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { // --- Set Global Start Round ID to Round 2's ID --- fundingPot.setGlobalAccumulationStart(round2Id); assertEq( - fundingPot.getGlobalAccumulationStartRoundId(), + fundingPot.globalAccumulationStartRoundId(), round2Id, "Global start ID not set to R2 ID" ); @@ -3727,186 +4241,45 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { vm.startPrank(contributor1_); fundingPot.contributeToRoundFor( - contributor1_, round2Id, c1AttemptR2, accessId, new bytes32[](0) + contributor1_, round2Id, c1AttemptR2, accessId, new bytes32[](0), _unspentPersonalRoundCaps ); vm.stopPrank(); // --- Assertions --- assertEq( - fundingPot.getUserContributionToRound(round2Id, contributor1_), + fundingPot.roundIdToUserToContribution(round2Id, contributor1_), r2BaseTotalCap, "R2 C1 contribution should be clamped by R2 base total cap (All mode, global_start=R2)" ); assertEq( - fundingPot.getTotalRoundContribution(round2Id), + fundingPot.roundIdToTotalContributions(round2Id), r2BaseTotalCap, "R2 Total contributions should be R2 base total cap (All mode, global_start=R2)" ); } - function testContributeToRoundFor_revertsGivenUnspentCapsRoundIdsNotStrictlyIncreasing( - ) public { - // Setup: Round 1 (Personal), Round 2 (Personal), Round 3 (Personal for contribution) - uint initialTimestamp = block.timestamp; - uint8 accessId = 1; // Open access - uint personalCap = 500; - uint roundCap = 10_000; - - vm.startPrank(contributor1_); - _token.approve(address(fundingPot), type(uint).max); - vm.stopPrank(); - - // Round 1 - uint32 round1Id = fundingPot.createRound( - initialTimestamp + 1 days, - initialTimestamp + 2 days, - roundCap, - address(0), - bytes(""), - false, - ILM_PC_FundingPot_v1.AccumulationMode.Personal - ); - fundingPot.setAccessCriteria( - round1Id, accessId, 0, address(0), bytes32(0), new address[](0) - ); - fundingPot.setAccessCriteriaPrivileges( - round1Id, accessId, personalCap, false, 0, 0, 0 - ); - - // Round 2 - uint32 round2Id = fundingPot.createRound( - initialTimestamp + 3 days, - initialTimestamp + 4 days, - roundCap, - address(0), - bytes(""), - false, - ILM_PC_FundingPot_v1.AccumulationMode.Personal - ); - fundingPot.setAccessCriteria( - round2Id, accessId, 0, address(0), bytes32(0), new address[](0) - ); - fundingPot.setAccessCriteriaPrivileges( - round2Id, accessId, personalCap, false, 0, 0, 0 - ); - - // Round 3 (target for contribution) - uint32 round3Id = fundingPot.createRound( - initialTimestamp + 5 days, - initialTimestamp + 6 days, - roundCap, - address(0), - bytes(""), - false, - ILM_PC_FundingPot_v1.AccumulationMode.Personal - ); - fundingPot.setAccessCriteria( - round3Id, accessId, 0, address(0), bytes32(0), new address[](0) - ); - fundingPot.setAccessCriteriaPrivileges( - round3Id, accessId, personalCap, false, 0, 0, 0 - ); - - vm.warp(initialTimestamp + 5 days + 1 hours); // Enter Round 3 - - // Case 1: Out of order - ILM_PC_FundingPot_v1.UnspentPersonalRoundCap[] memory - unspentCapsOutOfOrder = - new ILM_PC_FundingPot_v1.UnspentPersonalRoundCap[](2); - unspentCapsOutOfOrder[0] = ILM_PC_FundingPot_v1.UnspentPersonalRoundCap( - round2Id, accessId, new bytes32[](0) - ); - unspentCapsOutOfOrder[1] = ILM_PC_FundingPot_v1.UnspentPersonalRoundCap( - round1Id, accessId, new bytes32[](0) - ); - - vm.startPrank(contributor1_); - vm.expectRevert( - ILM_PC_FundingPot_v1 - .Module__LM_PC_FundingPot__UnspentCapsRoundIdsNotStrictlyIncreasing - .selector - ); - fundingPot.contributeToRoundFor( - contributor1_, - round3Id, - 100, - accessId, - new bytes32[](0), - unspentCapsOutOfOrder - ); - vm.stopPrank(); - // Case 2: Duplicate roundId - ILM_PC_FundingPot_v1.UnspentPersonalRoundCap[] memory - unspentCapsDuplicate = - new ILM_PC_FundingPot_v1.UnspentPersonalRoundCap[](2); - unspentCapsDuplicate[0] = ILM_PC_FundingPot_v1.UnspentPersonalRoundCap( - round1Id, accessId, new bytes32[](0) - ); - unspentCapsDuplicate[1] = ILM_PC_FundingPot_v1.UnspentPersonalRoundCap( - round1Id, accessId, new bytes32[](0) - ); - vm.startPrank(contributor1_); - vm.expectRevert( - ILM_PC_FundingPot_v1 - .Module__LM_PC_FundingPot__UnspentCapsRoundIdsNotStrictlyIncreasing - .selector - ); - fundingPot.contributeToRoundFor( - contributor1_, - round3Id, - 100, - accessId, - new bytes32[](0), - unspentCapsDuplicate - ); - vm.stopPrank(); - - // Case 3: Correct order but first element's roundId is 0 (if lastSeenRoundId starts at 0) - // This specific case won't be hit if round IDs must be >0, but good to be aware. - // Assuming valid round IDs start from 1, this case might not be directly testable if 0 isn't a valid roundId. - // The current check `currentProcessingRoundId <= lastSeenRoundId` covers this if roundId can be 0. - // If round IDs are always >= 1, then an initial lastSeenRoundId=0 is fine. - - // Case 4: Empty array (should not revert with this specific error, but pass) - ILM_PC_FundingPot_v1.UnspentPersonalRoundCap[] memory unspentCapsEmpty = - new ILM_PC_FundingPot_v1.UnspentPersonalRoundCap[](0); - vm.startPrank(contributor1_); - fundingPot.contributeToRoundFor( // This should pass (or revert with a different error if amount is 0 etc.) - contributor1_, - round3Id, - 100, - accessId, - new bytes32[](0), - unspentCapsEmpty - ); - vm.stopPrank(); - assertEq( - fundingPot.getUserContributionToRound(round3Id, contributor1_), 100 - ); - } - - function testContributeToRoundFor_personalModeOnlyAccumulatesPersonalCaps() + function testContributeToRoundFor_totalModeOnlyAccumulatesTotalCaps() public { - // 1. Create the first round with AccumulationMode.Personal + // 1. Create the first round with AccumulationMode.Total _defaultRoundParams.accumulationMode = - ILM_PC_FundingPot_v1.AccumulationMode.Personal; + ILM_PC_FundingPot_v1.AccumulationMode.Total; fundingPot.createRound( _defaultRoundParams.roundStart, _defaultRoundParams.roundEnd, - 1000, // Round cap of 1000 + 1000, // Round 1 cap of 1000 _defaultRoundParams.hookContract, _defaultRoundParams.hookFunction, _defaultRoundParams.autoClosure, _defaultRoundParams.accumulationMode ); - uint32 round1Id = fundingPot.getRoundCount(); + uint32 round1Id = fundingPot.roundCount(); - // Set up access criteria for round 1 - uint8 accessCriteriaId = 1; // Open access + // Set up access criteria for round 1 (Open) + uint8 accessCriteriaId = 1; ( address nftContract, bytes32 merkleRoot, @@ -3921,24 +4294,24 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { 0, // accessCriteriaId (0 for new) nftContract, merkleRoot, - allowedAddresses + allowedAddresses, + removedAddresses ); - // Set a personal cap of 500 for round 1 + // Set a personal cap of 800 for round 1 fundingPot.setAccessCriteriaPrivileges( - round1Id, accessCriteriaId, 500, false, 0, 0, 0 + round1Id, accessCriteriaId, 800, false, 0, 0, 0 ); - // 2. Create the second round, also with AccumulationMode.Personal - // Use different start and end times to avoid overlap + // 2. Create the second round, also with AccumulationMode.Total RoundParams memory params = _helper_createEditRoundParams( _defaultRoundParams.roundStart + 3 days, _defaultRoundParams.roundEnd + 3 days, - 500, // Round cap of 500 + 500, // Round 2 base cap of 500 _defaultRoundParams.hookContract, _defaultRoundParams.hookFunction, _defaultRoundParams.autoClosure, - ILM_PC_FundingPot_v1.AccumulationMode.Personal + ILM_PC_FundingPot_v1.AccumulationMode.Total ); fundingPot.createRound( @@ -3950,43 +4323,77 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { params.autoClosure, params.accumulationMode ); - uint32 round2Id = fundingPot.getRoundCount(); + uint32 round2Id = fundingPot.roundCount(); - // Set up access criteria for round 2 + // Set up access criteria for round 2 (Open) fundingPot.setAccessCriteria( round2Id, uint8(ILM_PC_FundingPot_v1.AccessCriteriaType.OPEN), // accessCriteriaType 0, // accessCriteriaId (0 for new) nftContract, merkleRoot, - allowedAddresses + allowedAddresses, + removedAddresses ); - // Set a personal cap of 400 for round 2 + // Set a personal cap of 300 for round 2 fundingPot.setAccessCriteriaPrivileges( - round2Id, accessCriteriaId, 400, false, 0, 0, 0 + round2Id, accessCriteriaId, 300, false, 0, 0, 0 ); - // First round contribution: user contributes 200 out of their 500 personal cap + // Round 1 contribution: contributor1 contributes 600 (less than round cap 1000, less than personal 800) + // Undersubscription: 1000 - 600 = 400 vm.warp(_defaultRoundParams.roundStart + 1); - vm.startPrank(contributor1_); _token.approve(address(fundingPot), 1000); + ILM_PC_FundingPot_v1.UnspentPersonalRoundCap[] memory + unspentPersonalRoundCaps = + new ILM_PC_FundingPot_v1.UnspentPersonalRoundCap[](0); fundingPot.contributeToRoundFor( - contributor1_, round1Id, 200, accessCriteriaId, new bytes32[](0) + contributor1_, round1Id, 600, accessCriteriaId, new bytes32[](0), unspentPersonalRoundCaps ); vm.stopPrank(); // Verify contribution to round 1 assertEq( - fundingPot.getUserContributionToRound(round1Id, contributor1_), 200 + fundingPot.roundIdToUserToContribution(round1Id, contributor1_), 600 ); + assertEq(fundingPot.roundIdToTotalContributions(round1Id), 600); // Move to round 2 vm.warp(_defaultRoundParams.roundStart + 3 days + 1); - // ------------ PART 1: VERIFY PERSONAL CAP ACCUMULATION ------------ - // Create unspent capacity structure + // ------------ PART 1: VERIFY TOTAL CAP ACCUMULATION ------------ + // Effective Round 2 Cap = Base Cap (500) + Unused from Round 1 (400) = 900 + vm.startPrank(contributor2_); + _token.approve(address(fundingPot), 1000); // Approve enough + + // Contributor 2 attempts to contribute 700. + // Personal Cap (R2) is 300. Gets clamped to 300. + fundingPot.contributeToRoundFor( + contributor2_, round2Id, 700, accessCriteriaId, new bytes32[](0), unspentPersonalRoundCaps + ); + // Verify contributor 2's contribution was clamped by personal cap. + assertEq( + fundingPot.roundIdToUserToContribution(round2Id, contributor2_), + 300, + "C2 contribution should be clamped by personal cap" + ); + vm.stopPrank(); + + // Verify total contributions after C2 is 300 + assertEq( + fundingPot.roundIdToTotalContributions(round2Id), + 300, + "Total after C2 should be 300" + ); + + // ------------ PART 2: VERIFY PERSONAL CAP NON-ACCUMULATION ------------ + // Contributor 1 had 800 personal cap in R1, contributed 600, unused = 200. + // Contributor 1 has 300 personal cap in R2. + // In Total mode, personal cap does NOT roll over. Max contribution is 300. + + // Prepare unspent caps struct (even though it shouldn't work for personal in Total mode) ILM_PC_FundingPot_v1.UnspentPersonalRoundCap[] memory unspentCaps = new ILM_PC_FundingPot_v1.UnspentPersonalRoundCap[](1); unspentCaps[0] = ILM_PC_FundingPot_v1.UnspentPersonalRoundCap({ @@ -3995,281 +4402,317 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { merkleProof: new bytes32[](0) }); - // Try to contribute more than the round 2 personal cap (400) - // In Personal mode, this should succeed up to the personal cap (400) + unspent from round 1 (300) = 700 - // But capped by round cap of 500 vm.startPrank(contributor1_); + _token.approve(address(fundingPot), 500); + + // Attempt to contribute 400 ( > R2 personal cap 300) + // Total contributions = 300. Effective Round Cap = 900. Remaining Round Cap = 600. + // Personal Cap (R2) = 300. Unspent (R1) = 200, ignored in Total mode. + // Min(Remaining Round Cap, Remaining Personal Cap) = Min(600, 300) = 300. + // Should be clamped to 300. fundingPot.contributeToRoundFor( contributor1_, round2Id, - 450, // More than the personal cap of round 2 + 400, accessCriteriaId, new bytes32[](0), - unspentCaps + unspentCaps // Provide unspent caps, although they should be ignored for personal limit + ); + // Verify contributor 1's contribution was clamped to their R2 personal cap. + assertEq( + fundingPot.roundIdToUserToContribution(round2Id, contributor1_), + 300, + "C1 contribution should be clamped by personal cap" ); vm.stopPrank(); - // Verify contributions to round 2 - should be more than the personal cap of round 2 (400) - // This verifies personal caps DO accumulate - uint contributionAmount = - fundingPot.getUserContributionToRound(round2Id, contributor1_); - assertEq(contributionAmount, 450); - assertTrue(contributionAmount > 400, "Personal cap should accumulate"); - - // ------------ PART 2: VERIFY TOTAL CAP NON-ACCUMULATION ------------ - // Attempt to contribute more than the remaining round cap - vm.startPrank(contributor2_); - _token.approve(address(fundingPot), 200); + // Verify total round contributions: 300 (C2) + 300 (C1) = 600 + assertEq( + fundingPot.roundIdToTotalContributions(round2Id), + 600, + "Total after C1 and C2 should be 600" + ); + // Effective cap 900, current total 600. Remaining = 300. - // Contributor 2 attempts to contribute 100. - // Since contributor1 contributed 450 and round cap is 500, only 50 is remaining. - // The contribution should be clamped to 50. + // Contributor 3 contributes 300. Personal Cap = 300. Remaining Round Cap = 300. Should succeed. + vm.startPrank(contributor3_); + _token.approve(address(fundingPot), 300); fundingPot.contributeToRoundFor( - contributor2_, round2Id, 100, accessCriteriaId, new bytes32[](0) + contributor3_, round2Id, 300, accessCriteriaId, new bytes32[](0), unspentPersonalRoundCaps ); - // Verify contributor 2's contribution was clamped to the remaining 50. + // Verify C3 contributed 300 assertEq( - fundingPot.getUserContributionToRound(round2Id, contributor2_), 50 + fundingPot.roundIdToUserToContribution(round2Id, contributor3_), + 300, + "C3 contributes remaining 300" ); vm.stopPrank(); - // Verify total contributions to round 2 is exactly the round cap (450 + 50 = 500). - assertEq(fundingPot.getTotalRoundContribution(round2Id), 500); + // Total contributions should now be 900 (300 + 300 + 300), matching the effective cap. + assertEq( + fundingPot.roundIdToTotalContributions(round2Id), + 900, + "Total should match effective cap after C3" + ); - // Additional contributor3 should not be able to contribute anything as the cap is full. - // Attempting to contribute when the cap is already full should revert. - vm.startPrank(contributor3_); - _token.approve(address(fundingPot), 100); + // Now the effective cap is full. Try contributing 1 again. + vm.startPrank(contributor3_); // Can use C3 or another contributor + _token.approve(address(fundingPot), 1); - // Expect revert because the round cap (500) is already met. + // Try contributing 1, expect revert as cap is full vm.expectRevert( abi.encodeWithSelector( ILM_PC_FundingPot_v1 .Module__LM_PC_FundingPot__RoundCapReached .selector ) - ); + ); fundingPot.contributeToRoundFor( - contributor3_, round2Id, 1, accessCriteriaId, new bytes32[](0) + contributor3_, round2Id, 1, accessCriteriaId, new bytes32[](0), unspentPersonalRoundCaps ); vm.stopPrank(); - // Final check that total contributions remain at the round cap. - assertEq(fundingPot.getTotalRoundContribution(round2Id), 500); + // Final total check should remain 900 + assertEq( + fundingPot.roundIdToTotalContributions(round2Id), + 900, + "Final total should be effective cap" + ); } - function testContributeToRoundFor_totalModeOnlyAccumulatesTotalCaps() - public - { - // 1. Create the first round with AccumulationMode.Total - _defaultRoundParams.accumulationMode = - ILM_PC_FundingPot_v1.AccumulationMode.Total; + function testContributeToRoundFor_UsedUnspentCapsIsSet() public { + // Step 1: Create round 1 and round 2 + uint32 round1 = fundingPot.createRound({ + roundStart_: block.timestamp + 1, + roundEnd_: block.timestamp + 1 days, + roundCap_: 1000, + hookContract_: address(0), + hookFunction_: "", + autoClosure_: false, + accumulationMode_: ILM_PC_FundingPot_v1.AccumulationMode.Personal + }); - fundingPot.createRound( - _defaultRoundParams.roundStart, - _defaultRoundParams.roundEnd, - 1000, // Round 1 cap of 1000 - _defaultRoundParams.hookContract, - _defaultRoundParams.hookFunction, - _defaultRoundParams.autoClosure, - _defaultRoundParams.accumulationMode - ); - uint32 round1Id = fundingPot.getRoundCount(); + uint8 accessType = uint8(ILM_PC_FundingPot_v1.AccessCriteriaType.OPEN); - // Set up access criteria for round 1 (Open) - uint8 accessCriteriaId = 1; ( address nftContract, bytes32 merkleRoot, address[] memory allowedAddresses - ) = _helper_createAccessCriteria( - uint8(ILM_PC_FundingPot_v1.AccessCriteriaType.OPEN), round1Id - ); + ) = _helper_createAccessCriteria(accessType, round1); fundingPot.setAccessCriteria( - round1Id, - uint8(ILM_PC_FundingPot_v1.AccessCriteriaType.OPEN), // accessCriteriaType - 0, // accessCriteriaId (0 for new) - nftContract, - merkleRoot, - allowedAddresses + round1, + uint8(ILM_PC_FundingPot_v1.AccessCriteriaType.OPEN), + 0, + address(0), + 0, + allowedAddresses, + removedAddresses ); - - // Set a personal cap of 800 for round 1 fundingPot.setAccessCriteriaPrivileges( - round1Id, accessCriteriaId, 800, false, 0, 0, 0 + round1, 1, 200, false, block.timestamp, 0, block.timestamp + 1 days ); - // 2. Create the second round, also with AccumulationMode.Total - RoundParams memory params = _helper_createEditRoundParams( - _defaultRoundParams.roundStart + 3 days, - _defaultRoundParams.roundEnd + 3 days, - 500, // Round 2 base cap of 500 - _defaultRoundParams.hookContract, - _defaultRoundParams.hookFunction, - _defaultRoundParams.autoClosure, - ILM_PC_FundingPot_v1.AccumulationMode.Total - ); + vm.warp(block.timestamp + 2); - fundingPot.createRound( - params.roundStart, - params.roundEnd, - params.roundCap, - params.hookContract, - params.hookFunction, - params.autoClosure, - params.accumulationMode + // Contribute in round 1 + vm.startPrank(contributor1_); + _token.approve(address(fundingPot), 100); + ILM_PC_FundingPot_v1.UnspentPersonalRoundCap[] memory + unspentPersonalRoundCaps = + new ILM_PC_FundingPot_v1.UnspentPersonalRoundCap[](0); + fundingPot.contributeToRoundFor( + contributor1_, round1, 100, 1, new bytes32[](0), unspentPersonalRoundCaps ); - uint32 round2Id = fundingPot.getRoundCount(); + vm.stopPrank(); + + // Step 2: Create round 2 with accumulationMode enabled + uint32 round2 = fundingPot.createRound({ + roundStart_: block.timestamp + 1, + roundEnd_: block.timestamp + 2 days, + roundCap_: 1000, + hookContract_: address(0), + hookFunction_: "", + autoClosure_: false, + accumulationMode_: ILM_PC_FundingPot_v1.AccumulationMode.Personal + }); - // Set up access criteria for round 2 (Open) fundingPot.setAccessCriteria( - round2Id, - uint8(ILM_PC_FundingPot_v1.AccessCriteriaType.OPEN), // accessCriteriaType - 0, // accessCriteriaId (0 for new) - nftContract, - merkleRoot, - allowedAddresses + round2, + uint8(ILM_PC_FundingPot_v1.AccessCriteriaType.OPEN), + 0, + address(0), + 0, + allowedAddresses, + removedAddresses ); - - // Set a personal cap of 300 for round 2 fundingPot.setAccessCriteriaPrivileges( - round2Id, accessCriteriaId, 300, false, 0, 0, 0 + round2, 1, 200, false, block.timestamp, 0, block.timestamp + 1 days ); - // Round 1 contribution: contributor1 contributes 600 (less than round cap 1000, less than personal 800) - // Undersubscription: 1000 - 600 = 400 - vm.warp(_defaultRoundParams.roundStart + 1); + // Step 3: Contribute to round 2 using unspent cap from round 1 + ILM_PC_FundingPot_v1.UnspentPersonalRoundCap[] memory caps = + new ILM_PC_FundingPot_v1.UnspentPersonalRoundCap[](1); + + caps[0] = ILM_PC_FundingPot_v1.UnspentPersonalRoundCap({ + roundId: round1, + accessCriteriaId: 1, + merkleProof: new bytes32[](0) + }); + + vm.warp(block.timestamp + 2); + vm.startPrank(contributor1_); - _token.approve(address(fundingPot), 1000); + _token.approve(address(fundingPot), 100); fundingPot.contributeToRoundFor( - contributor1_, round1Id, 600, accessCriteriaId, new bytes32[](0) + contributor1_, round2, 100, 1, new bytes32[](0), caps ); vm.stopPrank(); - // Verify contribution to round 1 - assertEq( - fundingPot.getUserContributionToRound(round1Id, contributor1_), 600 - ); - assertEq(fundingPot.getTotalRoundContribution(round1Id), 600); + // Step 4: Validate that usedUnspentCaps is set to true + bool isUsed = fundingPot.usedUnspentCaps(contributor1_, round1, 1); // expose via helper function if needed + assertTrue(isUsed, "usedUnspentCaps should be true after contribution"); + } - // Move to round 2 - vm.warp(_defaultRoundParams.roundStart + 3 days + 1); + function testContributeToRoundFor_UsedUnspentCapsSkippedIfAlreadyUsed() + public + { + // Step 1: Create round 1 and round 2 + uint32 round1 = fundingPot.createRound({ + roundStart_: block.timestamp + 1, + roundEnd_: block.timestamp + 1 days, + roundCap_: 1000, + hookContract_: address(0), + hookFunction_: "", + autoClosure_: false, + accumulationMode_: ILM_PC_FundingPot_v1.AccumulationMode.Personal + }); - // ------------ PART 1: VERIFY TOTAL CAP ACCUMULATION ------------ - // Effective Round 2 Cap = Base Cap (500) + Unused from Round 1 (400) = 900 - vm.startPrank(contributor2_); - _token.approve(address(fundingPot), 1000); // Approve enough + uint8 accessType = uint8(ILM_PC_FundingPot_v1.AccessCriteriaType.OPEN); + (address nftContract,, address[] memory allowedAddresses) = + _helper_createAccessCriteria(accessType, round1); + + fundingPot.setAccessCriteria( + round1, + accessType, + 0, + address(0), + 0, + allowedAddresses, + removedAddresses + ); + fundingPot.setAccessCriteriaPrivileges( + round1, 1, 300, false, block.timestamp, 0, block.timestamp + 1 days + ); - // Contributor 2 attempts to contribute 700. - // Personal Cap (R2) is 300. Gets clamped to 300. + vm.warp(block.timestamp + 2); + + // Step 1b: Contribute in round 1 + vm.startPrank(contributor1_); + _token.approve(address(fundingPot), 100); + ILM_PC_FundingPot_v1.UnspentPersonalRoundCap[] memory + unspentPersonalRoundCaps = + new ILM_PC_FundingPot_v1.UnspentPersonalRoundCap[](0); fundingPot.contributeToRoundFor( - contributor2_, round2Id, 700, accessCriteriaId, new bytes32[](0) - ); - // Verify contributor 2's contribution was clamped by personal cap. - assertEq( - fundingPot.getUserContributionToRound(round2Id, contributor2_), - 300, - "C2 contribution should be clamped by personal cap" + contributor1_, round1, 100, 1, new bytes32[](0), unspentPersonalRoundCaps ); vm.stopPrank(); - // Verify total contributions after C2 is 300 - assertEq( - fundingPot.getTotalRoundContribution(round2Id), - 300, - "Total after C2 should be 300" - ); + // Step 2: Create round 2 + uint32 round2 = fundingPot.createRound({ + roundStart_: block.timestamp + 1, + roundEnd_: block.timestamp + 2 days, + roundCap_: 1000, + hookContract_: address(0), + hookFunction_: "", + autoClosure_: false, + accumulationMode_: ILM_PC_FundingPot_v1.AccumulationMode.Personal + }); - // ------------ PART 2: VERIFY PERSONAL CAP NON-ACCUMULATION ------------ - // Contributor 1 had 800 personal cap in R1, contributed 600, unused = 200. - // Contributor 1 has 300 personal cap in R2. - // In Total mode, personal cap does NOT roll over. Max contribution is 300. + fundingPot.setAccessCriteria( + round2, + accessType, + 0, + address(0), + 0, + allowedAddresses, + removedAddresses + ); + fundingPot.setAccessCriteriaPrivileges( + round2, 1, 200, false, block.timestamp, 0, block.timestamp + 1 days + ); - // Prepare unspent caps struct (even though it shouldn't work for personal in Total mode) - ILM_PC_FundingPot_v1.UnspentPersonalRoundCap[] memory unspentCaps = + // Step 2b: Contribute using round1 cap → sets usedUnspentCaps + ILM_PC_FundingPot_v1.UnspentPersonalRoundCap[] memory caps1 = new ILM_PC_FundingPot_v1.UnspentPersonalRoundCap[](1); - unspentCaps[0] = ILM_PC_FundingPot_v1.UnspentPersonalRoundCap({ - roundId: round1Id, - accessCriteriaId: accessCriteriaId, + caps1[0] = ILM_PC_FundingPot_v1.UnspentPersonalRoundCap({ + roundId: round1, + accessCriteriaId: 1, merkleProof: new bytes32[](0) }); + vm.warp(block.timestamp + 2); vm.startPrank(contributor1_); - _token.approve(address(fundingPot), 500); - - // Attempt to contribute 400 ( > R2 personal cap 300) - // Total contributions = 300. Effective Round Cap = 900. Remaining Round Cap = 600. - // Personal Cap (R2) = 300. Unspent (R1) = 200, ignored in Total mode. - // Min(Remaining Round Cap, Remaining Personal Cap) = Min(600, 300) = 300. - // Should be clamped to 300. + _token.approve(address(fundingPot), 200); fundingPot.contributeToRoundFor( - contributor1_, - round2Id, - 400, - accessCriteriaId, - new bytes32[](0), - unspentCaps // Provide unspent caps, although they should be ignored for personal limit - ); - // Verify contributor 1's contribution was clamped to their R2 personal cap. - assertEq( - fundingPot.getUserContributionToRound(round2Id, contributor1_), - 300, - "C1 contribution should be clamped by personal cap" + contributor1_, round2, 200, 1, new bytes32[](0), caps1 ); vm.stopPrank(); - // Verify total round contributions: 300 (C2) + 300 (C1) = 600 - assertEq( - fundingPot.getTotalRoundContribution(round2Id), - 600, - "Total after C1 and C2 should be 600" - ); - // Effective cap 900, current total 600. Remaining = 300. + // Step 3: Create round 3 + uint32 round3 = fundingPot.createRound({ + roundStart_: block.timestamp + 1, + roundEnd_: block.timestamp + 2 days, + roundCap_: 1000, + hookContract_: address(0), + hookFunction_: "", + autoClosure_: false, + accumulationMode_: ILM_PC_FundingPot_v1.AccumulationMode.Personal + }); - // Contributor 3 contributes 300. Personal Cap = 300. Remaining Round Cap = 300. Should succeed. - vm.startPrank(contributor3_); - _token.approve(address(fundingPot), 300); - fundingPot.contributeToRoundFor( - contributor3_, round2Id, 300, accessCriteriaId, new bytes32[](0) - ); - // Verify C3 contributed 300 - assertEq( - fundingPot.getUserContributionToRound(round2Id, contributor3_), - 300, - "C3 contributes remaining 300" + fundingPot.setAccessCriteria( + round3, + accessType, + 0, + address(0), + 0, + allowedAddresses, + removedAddresses ); - vm.stopPrank(); - - // Total contributions should now be 900 (300 + 300 + 300), matching the effective cap. - assertEq( - fundingPot.getTotalRoundContribution(round2Id), - 900, - "Total should match effective cap after C3" + fundingPot.setAccessCriteriaPrivileges( + round3, 1, 300, false, block.timestamp, 0, block.timestamp + 1 days ); - // Now the effective cap is full. Try contributing 1 again. - vm.startPrank(contributor3_); // Can use C3 or another contributor - _token.approve(address(fundingPot), 1); + // Step 4: Try reusing round1 cap again → should skip because it's already used + ILM_PC_FundingPot_v1.UnspentPersonalRoundCap[] memory caps2 = + new ILM_PC_FundingPot_v1.UnspentPersonalRoundCap[](1); + caps2[0] = ILM_PC_FundingPot_v1.UnspentPersonalRoundCap({ + roundId: round1, + accessCriteriaId: 1, + merkleProof: new bytes32[](0) + }); - // Try contributing 1, expect revert as cap is full - vm.expectRevert( - abi.encodeWithSelector( - ILM_PC_FundingPot_v1 - .Module__LM_PC_FundingPot__RoundCapReached - .selector - ) - ); + vm.warp(block.timestamp + 2); + vm.startPrank(contributor1_); + _token.approve(address(fundingPot), 100); fundingPot.contributeToRoundFor( - contributor3_, round2Id, 1, accessCriteriaId, new bytes32[](0) + contributor1_, round3, 100, 1, new bytes32[](0), caps2 ); vm.stopPrank(); - // Final total check should remain 900 - assertEq( - fundingPot.getTotalRoundContribution(round2Id), - 900, - "Final total should be effective cap" + // Step 5: Check that contribution in round3 is only based on round3 cap (not reused from round1) + uint contributed = + fundingPot.roundIdToUserToContribution(round3, contributor1_); + assertLe( + contributed, + 300, + "Should not include unspent cap from already-used round" + ); + + // Confirm usedUnspentCaps[round1] is still true, not overwritten or reused + bool isStillUsed = fundingPot.usedUnspentCaps(contributor1_, round1, 1); + assertTrue( + isStillUsed, "usedUnspentCaps should still be true from earlier use" ); } @@ -4332,7 +4775,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { vm.assume(user_ != address(0) && user_ != address(this)); testCreateRound(); - uint32 roundId = fundingPot.getRoundCount(); + uint32 roundId = fundingPot.roundCount(); vm.startPrank(user_); bytes32 roleId = _authorizer.generateRoleId( @@ -4352,7 +4795,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { ) public { vm.assume(accessCriteriaEnum >= 0 && accessCriteriaEnum <= 4); - uint32 roundId = fundingPot.getRoundCount(); + uint32 roundId = fundingPot.roundCount(); _helper_createAccessCriteria(accessCriteriaEnum, roundId); @@ -4366,53 +4809,14 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { fundingPot.closeRound(roundId); } - function testCloseRound_revertsGivenHookExecutionFails() public { - uint8 accessCriteriaId = - uint8(ILM_PC_FundingPot_v1.AccessCriteriaType.OPEN); - - uint32 roundId = fundingPot.createRound( - _defaultRoundParams.roundStart, - _defaultRoundParams.roundEnd, - _defaultRoundParams.roundCap, - address(failingHook), - abi.encodeWithSignature("executeHook()"), - _defaultRoundParams.autoClosure, - _defaultRoundParams.accumulationMode - ); - - ( - address nftContract, - bytes32 merkleRoot, - address[] memory allowedAddresses - ) = _helper_createAccessCriteria(accessCriteriaId, roundId); - - fundingPot.setAccessCriteria( - roundId, - accessCriteriaId, - 0, - nftContract, - merkleRoot, - allowedAddresses - ); - - fundingPot.setAccessCriteriaPrivileges(roundId, 0, 1000, false, 0, 0, 0); - - vm.warp(_defaultRoundParams.roundEnd + 1); - vm.expectRevert( - ILM_PC_FundingPot_v1 - .Module__LM_PC_FundingPot__HookExecutionFailed - .selector - ); - fundingPot.closeRound(roundId); - } function testCloseRound_revertsGivenClosureConditionsNotMet() public { uint8 accessCriteriaId = uint8(ILM_PC_FundingPot_v1.AccessCriteriaType.OPEN); _helper_setupRoundWithAccessCriteria(accessCriteriaId); - uint32 roundId = fundingPot.getRoundCount(); + uint32 roundId = fundingPot.roundCount(); fundingPot.setAccessCriteriaPrivileges(roundId, 0, 1000, false, 0, 0, 0); @@ -4429,7 +4833,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { uint8(ILM_PC_FundingPot_v1.AccessCriteriaType.OPEN); _helper_setupRoundWithAccessCriteria(accessCriteriaId); - uint32 roundId = fundingPot.getRoundCount(); + uint32 roundId = fundingPot.roundCount(); fundingPot.setAccessCriteriaPrivileges( roundId, accessCriteriaId, 1000, false, 0, 0, 0 ); @@ -4440,8 +4844,11 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { vm.startPrank(contributor1_); _token.approve(address(fundingPot), 1000); + ILM_PC_FundingPot_v1.UnspentPersonalRoundCap[] memory + unspentPersonalRoundCaps = + new ILM_PC_FundingPot_v1.UnspentPersonalRoundCap[](0); fundingPot.contributeToRoundFor( - contributor1_, roundId, 1000, accessCriteriaId, new bytes32[](0) + contributor1_, roundId, 1000, accessCriteriaId, new bytes32[](0), unspentPersonalRoundCaps ); vm.stopPrank(); @@ -4456,7 +4863,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { function testCloseRound_worksGivenRoundHasStartedButNotEnded() public { testCreateRound(); - uint32 roundId = fundingPot.getRoundCount(); + uint32 roundId = fundingPot.roundCount(); uint8 accessCriteriaId = 1; uint8 accessType = uint8(ILM_PC_FundingPot_v1.AccessCriteriaType.OPEN); @@ -4467,7 +4874,13 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { ) = _helper_createAccessCriteria(accessType, roundId); fundingPot.setAccessCriteria( - roundId, accessType, 0, nftContract, merkleRoot, allowedAddresses + roundId, + accessType, + 0, + nftContract, + merkleRoot, + allowedAddresses, + removedAddresses ); fundingPot.setAccessCriteriaPrivileges( roundId, accessCriteriaId, 1000, false, 0, 0, 0 @@ -4480,8 +4893,11 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { // Make a contribution vm.startPrank(contributor1_); _token.approve(address(fundingPot), 1000); + ILM_PC_FundingPot_v1.UnspentPersonalRoundCap[] memory + unspentPersonalRoundCaps = + new ILM_PC_FundingPot_v1.UnspentPersonalRoundCap[](0); fundingPot.contributeToRoundFor( - contributor1_, roundId, 1000, accessCriteriaId, new bytes32[](0) + contributor1_, roundId, 1000, accessCriteriaId, new bytes32[](0), unspentPersonalRoundCaps ); vm.stopPrank(); @@ -4489,12 +4905,12 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { fundingPot.closeRound(roundId); // Verify round is closed - assertEq(fundingPot.isRoundClosed(roundId), true); + assertEq(fundingPot.roundIdToClosedStatus(roundId), true); } function testCloseRound_worksGivenRoundHasEnded() public { testCreateRound(); - uint32 roundId = fundingPot.getRoundCount(); + uint32 roundId = fundingPot.roundCount(); uint8 accessCriteriaId = 1; uint8 accessType = uint8(ILM_PC_FundingPot_v1.AccessCriteriaType.OPEN); @@ -4506,7 +4922,13 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { ) = _helper_createAccessCriteria(accessType, roundId); fundingPot.setAccessCriteria( - roundId, accessType, 0, nftContract, merkleRoot, allowedAddresses + roundId, + accessType, + 0, + nftContract, + merkleRoot, + allowedAddresses, + removedAddresses ); fundingPot.setAccessCriteriaPrivileges( roundId, accessCriteriaId, 1000, false, 0, 0, 0 @@ -4519,8 +4941,11 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { vm.startPrank(contributor1_); _token.approve(address(fundingPot), 500); + ILM_PC_FundingPot_v1.UnspentPersonalRoundCap[] memory + unspentPersonalRoundCaps = + new ILM_PC_FundingPot_v1.UnspentPersonalRoundCap[](0); fundingPot.contributeToRoundFor( - contributor1_, roundId, 500, accessCriteriaId, new bytes32[](0) + contributor1_, roundId, 500, accessCriteriaId, new bytes32[](0), unspentPersonalRoundCaps ); vm.stopPrank(); @@ -4531,13 +4956,13 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { fundingPot.closeRound(roundId); // Verify round is closed - assertEq(fundingPot.isRoundClosed(roundId), true); + assertEq(fundingPot.roundIdToClosedStatus(roundId), true); } function testCloseRound_worksGivenRoundCapHasBeenReached() public { testCreateRound(); - uint32 roundId = fundingPot.getRoundCount(); + uint32 roundId = fundingPot.roundCount(); uint8 accessCriteriaId = 1; uint8 accessType = uint8(ILM_PC_FundingPot_v1.AccessCriteriaType.NFT); @@ -4550,7 +4975,13 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { ) = _helper_createAccessCriteria(accessType, roundId); fundingPot.setAccessCriteria( - roundId, accessType, 0, nftContract, merkleRoot, allowedAddresses + roundId, + accessType, + 0, + nftContract, + merkleRoot, + allowedAddresses, + removedAddresses ); fundingPot.setAccessCriteriaPrivileges( roundId, accessCriteriaId, 1000, false, 0, 0, 0 @@ -4566,19 +4997,22 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { _token.approve(address(fundingPot), 1000); vm.prank(contributor1_); + ILM_PC_FundingPot_v1.UnspentPersonalRoundCap[] memory + unspentPersonalRoundCaps = + new ILM_PC_FundingPot_v1.UnspentPersonalRoundCap[](0); fundingPot.contributeToRoundFor( - contributor1_, roundId, amount, accessCriteriaId, new bytes32[](0) + contributor1_, roundId, amount, accessCriteriaId, new bytes32[](0), unspentPersonalRoundCaps ); - assertEq(fundingPot.isRoundClosed(roundId), false); + assertEq(fundingPot.roundIdToClosedStatus(roundId), false); fundingPot.closeRound(roundId); - assertEq(fundingPot.isRoundClosed(roundId), true); + assertEq(fundingPot.roundIdToClosedStatus(roundId), true); } function testCloseRound_worksGivenRoundisAutoClosure() public { testEditRound(); - uint32 roundId = fundingPot.getRoundCount(); + uint32 roundId = fundingPot.roundCount(); uint8 accessCriteriaId = 1; uint8 accessType = uint8(ILM_PC_FundingPot_v1.AccessCriteriaType.OPEN); @@ -4591,7 +5025,13 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { ) = _helper_createAccessCriteria(accessType, roundId); fundingPot.setAccessCriteria( - roundId, accessType, 0, nftContract, merkleRoot, allowedAddresses + roundId, + accessType, + 0, + nftContract, + merkleRoot, + allowedAddresses, + removedAddresses ); fundingPot.setAccessCriteriaPrivileges( @@ -4607,16 +5047,19 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { _token.approve(address(fundingPot), 2000); vm.prank(contributor1_); + ILM_PC_FundingPot_v1.UnspentPersonalRoundCap[] memory + unspentPersonalRoundCaps = + new ILM_PC_FundingPot_v1.UnspentPersonalRoundCap[](0); fundingPot.contributeToRoundFor( - contributor1_, roundId, amount, accessCriteriaId, new bytes32[](0) + contributor1_, roundId, amount, accessCriteriaId, new bytes32[](0), unspentPersonalRoundCaps ); - assertEq(fundingPot.isRoundClosed(roundId), true); + assertEq(fundingPot.roundIdToClosedStatus(roundId), true); } function testCloseRound_worksWithMultipleContributors() public { testCreateRound(); - uint32 roundId = fundingPot.getRoundCount(); + uint32 roundId = fundingPot.roundCount(); // Set up access criteria uint8 accessCriteriaId = 1; @@ -4629,12 +5072,22 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { ) = _helper_createAccessCriteria(accessType, roundId); fundingPot.setAccessCriteria( - roundId, accessType, 0, nftContract, merkleRoot, allowedAddresses + roundId, + accessType, + 0, + nftContract, + merkleRoot, + allowedAddresses, + removedAddresses ); fundingPot.setAccessCriteriaPrivileges( roundId, accessCriteriaId, 1000, false, 0, 0, 0 ); + ILM_PC_FundingPot_v1.UnspentPersonalRoundCap[] memory + unspentPersonalRoundCaps = + new ILM_PC_FundingPot_v1.UnspentPersonalRoundCap[](0); + // Warp to round start (uint roundStart,,,,,,) = fundingPot.getRoundGenericParameters(roundId); vm.warp(roundStart + 1); @@ -4643,21 +5096,22 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { vm.startPrank(contributor1_); _token.approve(address(fundingPot), 500); fundingPot.contributeToRoundFor( - contributor1_, roundId, 500, accessCriteriaId, new bytes32[](0) + contributor1_, roundId, 500, accessCriteriaId, new bytes32[](0), unspentPersonalRoundCaps ); vm.stopPrank(); vm.startPrank(contributor2_); _token.approve(address(fundingPot), 200); fundingPot.contributeToRoundFor( - contributor2_, roundId, 200, accessCriteriaId, new bytes32[](0) + contributor2_, roundId, 200, accessCriteriaId, new bytes32[](0), unspentPersonalRoundCaps ); vm.stopPrank(); vm.startPrank(contributor3_); _token.approve(address(fundingPot), 300); + fundingPot.contributeToRoundFor( - contributor3_, roundId, 300, accessCriteriaId, new bytes32[](0) + contributor3_, roundId, 300, accessCriteriaId, new bytes32[](0), unspentPersonalRoundCaps ); vm.stopPrank(); @@ -4665,7 +5119,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { fundingPot.closeRound(roundId); // Verify round is closed - assertEq(fundingPot.isRoundClosed(roundId), true); + assertEq(fundingPot.roundIdToClosedStatus(roundId), true); } //------------------------------------------------------------------------- @@ -4722,7 +5176,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { function testCreatePaymentOrdersForContributorsBatch_revertsGivenRoundIsNotClosed( ) public { testContributeToRoundFor_worksGivenGenericConfigAndAccessCriteria(); - uint32 roundId = fundingPot.getRoundCount(); + uint32 roundId = fundingPot.roundCount(); vm.expectRevert( abi.encodeWithSelector( @@ -4734,25 +5188,11 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { fundingPot.createPaymentOrdersForContributorsBatch(roundId, 1); } - function testCreatePaymentOrdersForContributorsBatch_revertsGivenBatchSizeIsZero( - ) public { - testCloseRound_worksWithMultipleContributors(); - uint32 roundId = fundingPot.getRoundCount(); - - vm.expectRevert( - abi.encodeWithSelector( - ILM_PC_FundingPot_v1 - .Module__LM_PC_FundingPot__InvalidBatchParameters - .selector - ) - ); - fundingPot.createPaymentOrdersForContributorsBatch(roundId, 0); - } function testCreatePaymentOrdersForContributorsBatch_revertsGivenUserDoesNotHaveFundingPotAdminRole( ) public { testCloseRound_worksWithMultipleContributors(); - uint32 roundId = fundingPot.getRoundCount(); + uint32 roundId = fundingPot.roundCount(); vm.startPrank(contributor1_); bytes32 roleId = _authorizer.generateRoleId( @@ -4772,7 +5212,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { function testCreatePaymentOrdersForContributorsBatch_worksGivenBatchSizeIsGreaterThanContributorCount( ) public { testCloseRound_worksWithMultipleContributors(); - uint32 roundId = fundingPot.getRoundCount(); + uint32 roundId = fundingPot.roundCount(); fundingPot.createPaymentOrdersForContributorsBatch(roundId, 999); assertEq(fundingPot.paymentOrders().length, 3); @@ -4781,7 +5221,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { function testCreatePaymentOrdersForContributorsBatch_worksGivenRoundIsAutoClosure( ) public { testCloseRound_worksGivenRoundisAutoClosure(); - uint32 roundId = fundingPot.getRoundCount(); + uint32 roundId = fundingPot.roundCount(); fundingPot.createPaymentOrdersForContributorsBatch(roundId, 1); assertEq(fundingPot.paymentOrders().length, 1); @@ -4790,7 +5230,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { function testCreatePaymentOrdersForContributorsBatch_worksGivenRoundIsManualClosure( ) public { testCloseRound_worksWithMultipleContributors(); - uint32 roundId = fundingPot.getRoundCount(); + uint32 roundId = fundingPot.roundCount(); fundingPot.createPaymentOrdersForContributorsBatch(roundId, 3); assertEq(fundingPot.paymentOrders().length, 3); @@ -4805,7 +5245,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { bool canOverrideContributionSpan_, uint unspentPersonalCap_ ) external { - vm.assume(roundId_ > 0 && roundId_ >= fundingPot.getRoundCount()); + vm.assume(roundId_ > 0 && roundId_ >= fundingPot.roundCount()); vm.assume(amount_ <= 1000); vm.assume(accessCriteriaId_ <= 4); vm.assume(unspentPersonalCap_ >= 0); @@ -4847,22 +5287,6 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { } } - function testFuzz_ValidTimes(uint start, uint cliff, uint end) public { - vm.assume(cliff <= type(uint).max - start); - - bool isValid = fundingPot.exposed_validTimes(start, cliff, end); - - assertEq(isValid, start + cliff <= end); - - if (start > end) { - assertFalse(isValid); - } - - if (start == end) { - assertEq(isValid, cliff == 0); - } - } - // ------------------------------------------------------------------------- // Test: _calculateUnusedCapacityFromPreviousRounds @@ -4915,7 +5339,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { vm.assume(accessCriteriaEnum > 4); testCreateRound(); - uint32 roundId = fundingPot.getRoundCount(); + uint32 roundId = fundingPot.roundCount(); (uint roundStart,,,,,,) = fundingPot.getRoundGenericParameters(roundId); vm.warp(roundStart + 1); @@ -4925,7 +5349,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { vm.expectRevert( abi.encodeWithSelector( ILM_PC_FundingPot_v1 - .Module__LM_PC_FundingPot__InvalidAccessCriteriaId + .Module__LM_PC_FundingPot__InvalidAccessCriteriaType .selector ) ); @@ -4968,7 +5392,13 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { address[] memory allowedAddresses ) = _helper_createAccessCriteria(accessType, roundId); fundingPot.setAccessCriteria( - roundId, accessType, 0, nftContract, merkleRoot, allowedAddresses + roundId, + accessType, + 0, + nftContract, + merkleRoot, + allowedAddresses, + removedAddresses ); fundingPot.setAccessCriteriaPrivileges( roundId, accessCriteriaId, 1000, false, 0, 0, 0 @@ -4977,12 +5407,16 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { vm.warp(params.roundStart + 1); vm.startPrank(contributor1_); _token.approve(address(fundingPot), params.roundCap); + ILM_PC_FundingPot_v1.UnspentPersonalRoundCap[] memory + unspentPersonalRoundCaps = + new ILM_PC_FundingPot_v1.UnspentPersonalRoundCap[](0); fundingPot.contributeToRoundFor( contributor1_, roundId, params.roundCap, accessCriteriaId, - new bytes32[](0) + new bytes32[](0), + unspentPersonalRoundCaps ); vm.stopPrank(); @@ -5057,7 +5491,13 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { address[] memory allowedAddresses ) = _helper_createAccessCriteria(accessType, roundId); fundingPot.setAccessCriteria( - roundId, accessType, 0, nftContract, merkleRoot, allowedAddresses + roundId, + accessType, + 0, + nftContract, + merkleRoot, + allowedAddresses, + removedAddresses ); fundingPot.setAccessCriteriaPrivileges( roundId, @@ -5076,12 +5516,16 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { vm.warp(params.roundStart + 1); vm.startPrank(contributor1_); _token.approve(address(fundingPot), params.roundCap); + ILM_PC_FundingPot_v1.UnspentPersonalRoundCap[] memory + unspentPersonalRoundCaps = + new ILM_PC_FundingPot_v1.UnspentPersonalRoundCap[](0); fundingPot.contributeToRoundFor( contributor1_, roundId, params.roundCap, accessCriteriaId, - new bytes32[](0) + new bytes32[](0), + unspentPersonalRoundCaps ); vm.stopPrank(); @@ -5119,7 +5563,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { function test_closeRound_worksGivenCapReached() public { testCreateRound(); - uint32 roundId = fundingPot.getRoundCount(); + uint32 roundId = fundingPot.roundCount(); uint8 accessCriteriaId = 1; uint8 accessType = uint8(ILM_PC_FundingPot_v1.AccessCriteriaType.OPEN); @@ -5132,7 +5576,13 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { ) = _helper_createAccessCriteria(accessType, roundId); fundingPot.setAccessCriteria( - roundId, accessType, 0, nftContract, merkleRoot, allowedAddresses + roundId, + accessType, + 0, + nftContract, + merkleRoot, + allowedAddresses, + removedAddresses ); fundingPot.setAccessCriteriaPrivileges( roundId, accessCriteriaId, 1000, false, 0, 0, 0 @@ -5148,8 +5598,11 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { _token.approve(address(fundingPot), 1000); vm.prank(contributor1_); + ILM_PC_FundingPot_v1.UnspentPersonalRoundCap[] memory + unspentPersonalRoundCaps = + new ILM_PC_FundingPot_v1.UnspentPersonalRoundCap[](0); fundingPot.contributeToRoundFor( - contributor1_, roundId, amount, accessCriteriaId, new bytes32[](0) + contributor1_, roundId, amount, accessCriteriaId, new bytes32[](0), unspentPersonalRoundCaps ); assertTrue( @@ -5164,14 +5617,14 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { uint32(roundId), startIndex, batchSize ); - assertTrue(fundingPot.isRoundClosed(roundId)); + assertTrue(fundingPot.roundIdToClosedStatus(roundId)); } // ------------------------------------------------------------------------- // Test: _buyBondingCurveToken function test_buyBondingCurveToken_revertsGivenNoContributions() public { testCreateRound(); - uint32 roundId = fundingPot.getRoundCount(); + uint32 roundId = fundingPot.roundCount(); (uint roundStart,,,,,,) = fundingPot.getRoundGenericParameters(roundId); vm.warp(roundStart + 1); @@ -5322,7 +5775,196 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { 0, nftContract, merkleRoot, - allowedAddresses + allowedAddresses, + removedAddresses + ); + } + + // Helper function to set up access criteria for an existing round + function _helper_setupAccessCriteriaForRound( + uint32 roundId_, + uint8 accessCriteriaEnum_, + uint8 accessCriteriaId_, + uint personalCap_ + ) internal { + ( + address nftContract, + bytes32 merkleRoot, + address[] memory allowedAddresses + ) = _helper_createAccessCriteria(accessCriteriaEnum_, roundId_); + + fundingPot.setAccessCriteria( + roundId_, + accessCriteriaEnum_, + 0, + nftContract, + merkleRoot, + allowedAddresses, + removedAddresses + ); + fundingPot.setAccessCriteriaPrivileges( + roundId_, accessCriteriaId_, personalCap_, false, 0, 0, 0 + ); + } + + // ========================================================================= + // Test: contributeToRoundFor Authorization + // ========================================================================= + + function testContributeToRoundFor_revertsWhenNonOwnerTriesToUseUnspentCaps() public { + // Setup: Create two users - Alice and Bob + address alice = address(0x1111); + address bob = address(0x2222); + + // Give both users some tokens + vm.deal(alice, 10 ether); + vm.deal(bob, 10 ether); + _token.mint(alice, 1000); + _token.mint(bob, 1000); + + // Create first round where Alice contributes + vm.startPrank(address(this)); + uint32 round1 = fundingPot.createRound( + block.timestamp + 1 days, // start + block.timestamp + 7 days, // end + 500, // cap + address(0), // hookContract + "", // hookFunction + false, // autoClosure + ILM_PC_FundingPot_v1.AccumulationMode.Personal // allow personal accumulation + ); + + // Set up access criteria for round 1 + address[] memory allowedAddresses = new address[](2); + allowedAddresses[0] = alice; + allowedAddresses[1] = bob; + address[] memory localRemovedAddresses; + + fundingPot.setAccessCriteria( + round1, + uint8(ILM_PC_FundingPot_v1.AccessCriteriaType.LIST), + 0, // new access criteria + address(0), + bytes32(0), + allowedAddresses, + localRemovedAddresses ); + + // Set personal cap for access criteria + fundingPot.setAccessCriteriaPrivileges( + round1, + 1, // accessCriteriaId + 200, // personalCap + false, // overrideContributionSpan + 0, 0, 0 // time parameters + ); + vm.stopPrank(); + + // Alice contributes to round 1 (only partially using her cap) + vm.warp(block.timestamp + 1 days + 1); // move to round start + vm.startPrank(alice); + _token.approve(address(fundingPot), 100); + + ILM_PC_FundingPot_v1.UnspentPersonalRoundCap[] memory emptyUnspentCaps; + bytes32[] memory emptyProof; + + fundingPot.contributeToRoundFor( + alice, + round1, + 100, // only use 100 out of 200 cap + 1, // accessCriteriaId + emptyProof, + emptyUnspentCaps + ); + vm.stopPrank(); + + // Close round 1 + vm.warp(block.timestamp + 7 days); + vm.prank(address(this)); + fundingPot.closeRound(round1); + + // Create second round with personal accumulation + vm.startPrank(address(this)); + uint32 round2 = fundingPot.createRound( + block.timestamp + 1 days, + block.timestamp + 7 days, + 500, + address(0), + "", + false, + ILM_PC_FundingPot_v1.AccumulationMode.Personal + ); + + // Set up same access criteria for round 2 + fundingPot.setAccessCriteria( + round2, + uint8(ILM_PC_FundingPot_v1.AccessCriteriaType.LIST), + 0, + address(0), + bytes32(0), + allowedAddresses, + localRemovedAddresses + ); + + fundingPot.setAccessCriteriaPrivileges( + round2, + 1, + 200, // same personal cap + false, + 0, 0, 0 + ); + vm.stopPrank(); + + // Create Alice's unspent cap data from round 1 + ILM_PC_FundingPot_v1.UnspentPersonalRoundCap[] memory aliceUnspentCaps = + new ILM_PC_FundingPot_v1.UnspentPersonalRoundCap[](1); + aliceUnspentCaps[0] = ILM_PC_FundingPot_v1.UnspentPersonalRoundCap({ + roundId: round1, + accessCriteriaId: 1, + merkleProof: emptyProof + }); + + // Move to round 2 start + vm.warp(block.timestamp + 1 days + 1); + + // Test: Bob tries to use Alice's unspent caps for his own contribution + vm.startPrank(bob); + _token.approve(address(fundingPot), 50); + + // Should revert with OnlyOwnerCanUseUnspentCaps + vm.expectRevert( + ILM_PC_FundingPot_v1.Module__LM_PC_FundingPot__OnlyOwnerCanUseUnspentCaps.selector + ); + + fundingPot.contributeToRoundFor( + alice, // Bob contributing FOR Alice + round2, + 50, + 1, + emptyProof, + aliceUnspentCaps // Using Alice's unspent caps but called by Bob + ); + vm.stopPrank(); + + // Verify: Alice can still use her own unspent caps + vm.startPrank(alice); + _token.approve(address(fundingPot), 150); + + // This should work - Alice using her own unspent caps + fundingPot.contributeToRoundFor( + alice, + round2, + 150, // Alice can contribute more than base cap due to unspent caps + 1, + emptyProof, + aliceUnspentCaps + ); + vm.stopPrank(); + + // Verify Alice's contribution succeeded + assertEq(fundingPot.roundIdToUserToContribution(round2, alice), 150); + + // Verify Bob has no contributions (since his attack failed) + assertEq(fundingPot.roundIdToUserToContribution(round2, bob), 0); } } From 3305f5ac0e3d860783933bf1a96c0e49ab34d031 Mon Sep 17 00:00:00 2001 From: 0xNuggan <82726722+0xNuggan@users.noreply.github.com> Date: Tue, 16 Sep 2025 09:32:50 +0200 Subject: [PATCH 130/130] chore: format --- .../logicModule/LM_PC_FundingPot_v1.sol | 63 +-- .../interfaces/ILM_PC_FundingPot_v1.sol | 3 +- test/e2e/logicModule/FundingPotE2E.t.sol | 23 +- .../logicModule/LM_PC_FundingPot_v1.t.sol | 448 ++++++++++++++---- 4 files changed, 396 insertions(+), 141 deletions(-) diff --git a/src/modules/logicModule/LM_PC_FundingPot_v1.sol b/src/modules/logicModule/LM_PC_FundingPot_v1.sol index b987a6e28..862ffdfa8 100644 --- a/src/modules/logicModule/LM_PC_FundingPot_v1.sol +++ b/src/modules/logicModule/LM_PC_FundingPot_v1.sol @@ -288,8 +288,6 @@ contract LM_PC_FundingPot_v1 is ); } - - // ------------------------------------------------------------------------- // Public - Mutating @@ -487,9 +485,7 @@ contract LM_PC_FundingPot_v1 is = false; } - emit AllowlistedAddressesRemoved( - - ); + emit AllowlistedAddressesRemoved(); } /// @inheritdoc ILM_PC_FundingPot_v1 @@ -531,7 +527,6 @@ contract LM_PC_FundingPot_v1 is ); } - /// @inheritdoc ILM_PC_FundingPot_v1 function contributeToRoundFor( address user_, @@ -545,12 +540,11 @@ contract LM_PC_FundingPot_v1 is if (unspentPersonalRoundCaps_.length > 0 && _msgSender() != user_) { revert Module__LM_PC_FundingPot__OnlyOwnerCanUseUnspentCaps(); } - + uint unspentPersonalCap = _calculateUnspentPersonalCap( user_, roundId_, unspentPersonalRoundCaps_ ); - _contributeToRoundFor( user_, roundId_, @@ -609,8 +603,6 @@ contract LM_PC_FundingPot_v1 is EnumerableSet.values(contributorsByRound[roundId_]); uint contributorCount = contributors.length; - - // If autoClosure is false, only admin can process contributors if (!round.autoClosure) { _checkRoleModifier( @@ -663,13 +655,12 @@ contract LM_PC_FundingPot_v1 is uint32 roundId_, UnspentPersonalRoundCap[] calldata unspentPersonalRoundCaps_ ) internal returns (uint unspentPersonalCap) { - - uint totalAggregatedPersonalCap = 0; uint totalSpentInPastRounds = 0; for (uint i = 0; i < unspentPersonalRoundCaps_.length; i++) { - UnspentPersonalRoundCap memory roundCapInfo = unspentPersonalRoundCaps_[i]; + UnspentPersonalRoundCap memory roundCapInfo = + unspentPersonalRoundCaps_[i]; uint32 currentProcessingRoundId = roundCapInfo.roundId; // Skip if this round is before the global accumulation start round @@ -679,39 +670,54 @@ contract LM_PC_FundingPot_v1 is // Skip if round is current or future round if (currentProcessingRoundId >= roundId_) { - revert Module__LM_PC_FundingPot__UnspentCapsMustBeFromPreviousRounds(); + revert + Module__LM_PC_FundingPot__UnspentCapsMustBeFromPreviousRounds(); } // Skip if cap was already used - if (usedUnspentCaps[user_][currentProcessingRoundId][roundCapInfo.accessCriteriaId]) { + if ( + usedUnspentCaps[user_][currentProcessingRoundId][roundCapInfo + .accessCriteriaId] + ) { continue; } // For PERSONAL cap rollover, the PREVIOUS round must have allowed it (Personal or All) - if (rounds[currentProcessingRoundId].accumulationMode != AccumulationMode.Personal - && rounds[currentProcessingRoundId].accumulationMode != AccumulationMode.All) { + if ( + rounds[currentProcessingRoundId].accumulationMode + != AccumulationMode.Personal + && rounds[currentProcessingRoundId].accumulationMode + != AccumulationMode.All + ) { continue; } // Only count spent amounts from rounds that meet the accumulation criteria - totalSpentInPastRounds += roundIdToUserToContribution[currentProcessingRoundId][user_]; + totalSpentInPastRounds += + roundIdToUserToContribution[currentProcessingRoundId][user_]; // Check eligibility for the past round - if (_checkAccessCriteriaEligibility( - currentProcessingRoundId, - roundCapInfo.accessCriteriaId, - roundCapInfo.merkleProof, - user_ - )) { - AccessCriteriaPrivileges storage privileges = roundIdToAccessCriteriaIdToPrivileges[currentProcessingRoundId][roundCapInfo.accessCriteriaId]; + if ( + _checkAccessCriteriaEligibility( + currentProcessingRoundId, + roundCapInfo.accessCriteriaId, + roundCapInfo.merkleProof, + user_ + ) + ) { + AccessCriteriaPrivileges storage privileges = + roundIdToAccessCriteriaIdToPrivileges[currentProcessingRoundId][roundCapInfo + .accessCriteriaId]; totalAggregatedPersonalCap += privileges.personalCap; } // Mark the specific caps that were used in this contribution - usedUnspentCaps[user_][currentProcessingRoundId][roundCapInfo.accessCriteriaId] = true; + usedUnspentCaps[user_][currentProcessingRoundId][roundCapInfo + .accessCriteriaId] = true; } if (totalAggregatedPersonalCap > totalSpentInPastRounds) { - unspentPersonalCap = totalAggregatedPersonalCap - totalSpentInPastRounds; + unspentPersonalCap = + totalAggregatedPersonalCap - totalSpentInPastRounds; } return unspentPersonalCap; @@ -736,7 +742,6 @@ contract LM_PC_FundingPot_v1 is if (round_.roundEnd > 0 && round_.roundEnd < round_.roundStart) { revert Module__LM_PC_FundingPot__InvalidInput(); } - } /// @notice Validates the round parameters before editing. @@ -785,7 +790,6 @@ contract LM_PC_FundingPot_v1 is } Round storage round = rounds[roundId_]; - if (round.roundEnd == 0 && round.roundCap == 0) { revert Module__LM_PC_FundingPot__RoundNotCreated(); @@ -1042,7 +1046,6 @@ contract LM_PC_FundingPot_v1 is view returns (bool) { - try IERC721(nftContract_).balanceOf(user_) returns (uint balance) { return balance > 0; } catch { diff --git a/src/modules/logicModule/interfaces/ILM_PC_FundingPot_v1.sol b/src/modules/logicModule/interfaces/ILM_PC_FundingPot_v1.sol index dae5bd750..16848ccc5 100644 --- a/src/modules/logicModule/interfaces/ILM_PC_FundingPot_v1.sol +++ b/src/modules/logicModule/interfaces/ILM_PC_FundingPot_v1.sol @@ -226,7 +226,7 @@ interface ILM_PC_FundingPot_v1 is IERC20PaymentClientBase_v2 { /// @notice Access criteria failed. error Module__LM_PC_FundingPot__AccessCriteriaFailed(); - /// @notice User has reached their personal contribution cap. + /// @notice User has reached their personal contribution cap. error Module__LM_PC_FundingPot__PersonalCapReached(); /// @notice Round contribution cap has been reached. @@ -256,7 +256,6 @@ interface ILM_PC_FundingPot_v1 is IERC20PaymentClientBase_v2 { /// @notice Hook execution failed. error Module__LM_PC_FundingPot__HookExecutionFailed(); - // ------------------------------------------------------------------------- // Public - Getters diff --git a/test/e2e/logicModule/FundingPotE2E.t.sol b/test/e2e/logicModule/FundingPotE2E.t.sol index 08d2533d7..0e14453cd 100644 --- a/test/e2e/logicModule/FundingPotE2E.t.sol +++ b/test/e2e/logicModule/FundingPotE2E.t.sol @@ -265,16 +265,26 @@ contract FundingPotE2E is E2ETest { unspentPersonalRoundCaps = new ILM_PC_FundingPot_v1.UnspentPersonalRoundCap[](0); fundingPot.contributeToRoundFor( - contributor1, round1Id, contributor1Amount, 1, new bytes32[](0), unspentPersonalRoundCaps + contributor1, + round1Id, + contributor1Amount, + 1, + new bytes32[](0), + unspentPersonalRoundCaps ); vm.stopPrank(); vm.startPrank(contributor2); contributionToken.approve(address(fundingPot), contributor2Amount); unspentPersonalRoundCaps = - new ILM_PC_FundingPot_v1.UnspentPersonalRoundCap[](0); + new ILM_PC_FundingPot_v1.UnspentPersonalRoundCap[](0); fundingPot.contributeToRoundFor( - contributor2, round1Id, contributor2Amount, 1, new bytes32[](0), unspentPersonalRoundCaps + contributor2, + round1Id, + contributor2Amount, + 1, + new bytes32[](0), + unspentPersonalRoundCaps ); vm.stopPrank(); @@ -282,7 +292,12 @@ contract FundingPotE2E is E2ETest { contributionToken.approve(address(fundingPot), contributor3Amount); fundingPot.contributeToRoundFor( - contributor3, round2Id, contributor3Amount, 1, new bytes32[](0), unspentPersonalRoundCaps + contributor3, + round2Id, + contributor3Amount, + 1, + new bytes32[](0), + unspentPersonalRoundCaps ); vm.stopPrank(); diff --git a/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol b/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol index 247fa865d..41f676f47 100644 --- a/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol +++ b/test/unit/modules/logicModule/LM_PC_FundingPot_v1.t.sol @@ -66,7 +66,8 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { // Default round parameters for testing RoundParams private _defaultRoundParams; RoundParams private _editedRoundParams; - ILM_PC_FundingPot_v1.UnspentPersonalRoundCap[] private _unspentPersonalRoundCaps; + ILM_PC_FundingPot_v1.UnspentPersonalRoundCap[] private + _unspentPersonalRoundCaps; // Struct to hold round parameters struct RoundParams { @@ -294,8 +295,6 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { ); } - - /* Test Fuzz createRound() ├── Given all the valid parameters are provided │ └── When user attempts to create a round @@ -613,8 +612,6 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { ); } - - /* Test editRound() └── Given a round has been created ├── And the round is not active @@ -1396,7 +1393,12 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { vm.prank(contributor1_); fundingPot.contributeToRoundFor( - contributor1_, roundId, amount, accessCriteriaId, new bytes32[](0), _unspentPersonalRoundCaps + contributor1_, + roundId, + amount, + accessCriteriaId, + new bytes32[](0), + _unspentPersonalRoundCaps ); } @@ -1447,7 +1449,12 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { vm.prank(contributor1_); fundingPot.contributeToRoundFor( - contributor1_, roundId, amount, accessCriteriaId, new bytes32[](0), _unspentPersonalRoundCaps + contributor1_, + roundId, + amount, + accessCriteriaId, + new bytes32[](0), + _unspentPersonalRoundCaps ); } @@ -1481,7 +1488,12 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { vm.prank(contributor1_); fundingPot.contributeToRoundFor( - contributor1_, roundId, amount, accessCriteriaId, new bytes32[](0), _unspentPersonalRoundCaps + contributor1_, + roundId, + amount, + accessCriteriaId, + new bytes32[](0), + _unspentPersonalRoundCaps ); } @@ -1534,7 +1546,12 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { vm.prank(contributor1_); fundingPot.contributeToRoundFor( - contributor1_, roundId, amount, accessCriteriaId, proofB, _unspentPersonalRoundCaps + contributor1_, + roundId, + amount, + accessCriteriaId, + proofB, + _unspentPersonalRoundCaps ); } @@ -1583,7 +1600,12 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { vm.prank(contributor1_); fundingPot.contributeToRoundFor( - contributor1_, roundId, amount, accessCriteriaId, new bytes32[](0), _unspentPersonalRoundCaps + contributor1_, + roundId, + amount, + accessCriteriaId, + new bytes32[](0), + _unspentPersonalRoundCaps ); } @@ -1718,7 +1740,6 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { vm.stopPrank(); } - function testContributeToRoundFor_revertsGivenPreviousContributionExceedsPersonalCap( ) public { testCreateRound(); @@ -1758,7 +1779,12 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { vm.prank(contributor1_); fundingPot.contributeToRoundFor( - contributor1_, roundId, amount, accessCriteriaId, new bytes32[](0), _unspentPersonalRoundCaps + contributor1_, + roundId, + amount, + accessCriteriaId, + new bytes32[](0), + _unspentPersonalRoundCaps ); // Attempt to contribute beyond personal cap @@ -1772,7 +1798,12 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { vm.prank(contributor1_); fundingPot.contributeToRoundFor( - contributor1_, roundId, 251, accessCriteriaId, new bytes32[](0), _unspentPersonalRoundCaps + contributor1_, + roundId, + 251, + accessCriteriaId, + new bytes32[](0), + _unspentPersonalRoundCaps ); } @@ -1964,7 +1995,12 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { vm.prank(contributor1_); fundingPot.contributeToRoundFor( - contributor1_, roundId, amount, accessCriteriaId, new bytes32[](0), _unspentPersonalRoundCaps + contributor1_, + roundId, + amount, + accessCriteriaId, + new bytes32[](0), + _unspentPersonalRoundCaps ); uint totalContributions = @@ -2006,7 +2042,12 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { emit ILM_PC_FundingPot_v1.ContributionMade(roundId, contributor1_, 250); fundingPot.contributeToRoundFor( - contributor1_, roundId, 250, accessCriteriaId, new bytes32[](0), _unspentPersonalRoundCaps + contributor1_, + roundId, + 250, + accessCriteriaId, + new bytes32[](0), + _unspentPersonalRoundCaps ); vm.stopPrank(); @@ -2048,7 +2089,12 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { roundId, contributor2_, contributionAmount ); fundingPot.contributeToRoundFor( - contributor2_, roundId, contributionAmount, accessCriteriaId, proofB, _unspentPersonalRoundCaps + contributor2_, + roundId, + contributionAmount, + accessCriteriaId, + proofB, + _unspentPersonalRoundCaps ); vm.stopPrank(); @@ -2109,14 +2155,24 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { vm.startPrank(contributor1_); _token.approve(address(fundingPot), 100); fundingPot.contributeToRoundFor( - contributor1_, roundId, 100, accessCriteriaId, new bytes32[](0), _unspentPersonalRoundCaps + contributor1_, + roundId, + 100, + accessCriteriaId, + new bytes32[](0), + _unspentPersonalRoundCaps ); vm.stopPrank(); vm.startPrank(contributor2_); _token.approve(address(fundingPot), 100); fundingPot.contributeToRoundFor( - contributor2_, roundId, 100, accessCriteriaId, new bytes32[](0), _unspentPersonalRoundCaps + contributor2_, + roundId, + 100, + accessCriteriaId, + new bytes32[](0), + _unspentPersonalRoundCaps ); vm.stopPrank(); @@ -2258,7 +2314,12 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { // This should succeed despite being after round end, due to override privilege vm.prank(contributor1_); fundingPot.contributeToRoundFor( - contributor1_, roundId, amount, accessCriteriaId, new bytes32[](0), _unspentPersonalRoundCaps + contributor1_, + roundId, + amount, + accessCriteriaId, + new bytes32[](0), + _unspentPersonalRoundCaps ); // Verify the contribution was recorded @@ -2337,7 +2398,12 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { vm.startPrank(contributor1_); _token.approve(address(fundingPot), 1000); fundingPot.contributeToRoundFor( - contributor1_, round1Id, 200, accessCriteriaId, new bytes32[](0), _unspentPersonalRoundCaps + contributor1_, + round1Id, + 200, + accessCriteriaId, + new bytes32[](0), + _unspentPersonalRoundCaps ); // Warp to round 2 @@ -2445,14 +2511,24 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { vm.startPrank(contributor1_); _token.approve(address(fundingPot), 300); fundingPot.contributeToRoundFor( - contributor1_, round1Id, 300, accessCriteriaId, new bytes32[](0), _unspentPersonalRoundCaps + contributor1_, + round1Id, + 300, + accessCriteriaId, + new bytes32[](0), + _unspentPersonalRoundCaps ); vm.stopPrank(); vm.startPrank(contributor2_); _token.approve(address(fundingPot), 200); fundingPot.contributeToRoundFor( - contributor2_, round1Id, 200, accessCriteriaId, new bytes32[](0), _unspentPersonalRoundCaps + contributor2_, + round1Id, + 200, + accessCriteriaId, + new bytes32[](0), + _unspentPersonalRoundCaps ); vm.stopPrank(); @@ -2462,14 +2538,24 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { vm.startPrank(contributor2_); _token.approve(address(fundingPot), 400); fundingPot.contributeToRoundFor( - contributor2_, round2Id, 400, accessCriteriaId, new bytes32[](0), _unspentPersonalRoundCaps + contributor2_, + round2Id, + 400, + accessCriteriaId, + new bytes32[](0), + _unspentPersonalRoundCaps ); vm.stopPrank(); vm.startPrank(contributor3_); _token.approve(address(fundingPot), 300); fundingPot.contributeToRoundFor( - contributor3_, round2Id, 300, accessCriteriaId, new bytes32[](0), _unspentPersonalRoundCaps + contributor3_, + round2Id, + 300, + accessCriteriaId, + new bytes32[](0), + _unspentPersonalRoundCaps ); vm.stopPrank(); @@ -2587,12 +2673,22 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { vm.warp(initialTimestamp + 1 days + 1 hours); // Enter Round 1 fundingPot.contributeToRoundFor( - contributor1_, round1Id, r1Contribution, 1, new bytes32[](0), _unspentPersonalRoundCaps + contributor1_, + round1Id, + r1Contribution, + 1, + new bytes32[](0), + _unspentPersonalRoundCaps ); vm.warp(initialTimestamp + 3 days + 1 hours); // Enter Round 2 fundingPot.contributeToRoundFor( - contributor1_, round2Id, r2Contribution, 1, new bytes32[](0), _unspentPersonalRoundCaps + contributor1_, + round2Id, + r2Contribution, + 1, + new bytes32[](0), + _unspentPersonalRoundCaps ); vm.stopPrank(); @@ -2732,12 +2828,22 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { vm.warp(initialTimestamp + 1 days + 1 hours); // Enter Round 1 fundingPot.contributeToRoundFor( - contributor1_, round1Id, r1Contribution, 1, new bytes32[](0), _unspentPersonalRoundCaps + contributor1_, + round1Id, + r1Contribution, + 1, + new bytes32[](0), + _unspentPersonalRoundCaps ); vm.warp(initialTimestamp + 3 days + 1 hours); // Enter Round 2 fundingPot.contributeToRoundFor( - contributor1_, round2Id, r2Contribution, 1, new bytes32[](0), _unspentPersonalRoundCaps + contributor1_, + round2Id, + r2Contribution, + 1, + new bytes32[](0), + _unspentPersonalRoundCaps ); vm.stopPrank(); @@ -2754,7 +2860,12 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { // Attempt to contribute up to the expected new effective cap fundingPot.contributeToRoundFor( - contributor1_, round3Id, expectedR3EffectiveCap, 1, new bytes32[](0), _unspentPersonalRoundCaps + contributor1_, + round3Id, + expectedR3EffectiveCap, + 1, + new bytes32[](0), + _unspentPersonalRoundCaps ); vm.stopPrank(); @@ -2779,15 +2890,14 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { // 2. Action: Verify globalAccumulationStartRoundId() == 1 (default). // 3. Verification: For C1's contribution to R2, unused personal capacity from R1 rolls over. - - // --- Round Parameters & Contributions for C1 --- uint r1PersonalCapC1 = 500; uint r1ContributionC1 = 100; // C1 leaves 400 personal unused from R1 uint r2BasePersonalCapC1 = 300; // C1's base personal cap in R2 ILM_PC_FundingPot_v1.UnspentPersonalRoundCap[] memory - unspentPersonalRoundCaps = new ILM_PC_FundingPot_v1.UnspentPersonalRoundCap[](0); + unspentPersonalRoundCaps = + new ILM_PC_FundingPot_v1.UnspentPersonalRoundCap[](0); // --- Approvals --- vm.startPrank(contributor1_); @@ -2951,7 +3061,8 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { vm.warp(initialTimestamp + 1 days + 1 hours); vm.startPrank(contributor1_); ILM_PC_FundingPot_v1.UnspentPersonalRoundCap[] memory - unspentPersonalRoundCaps = new ILM_PC_FundingPot_v1.UnspentPersonalRoundCap[](0); + unspentPersonalRoundCaps = + new ILM_PC_FundingPot_v1.UnspentPersonalRoundCap[](0); fundingPot.contributeToRoundFor( contributor1_, round1Id, @@ -2982,7 +3093,8 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { removedAddresses ); // Set personal cap for R2 to be at least the expected effective total cap - uint r2ExpectedEffectiveTotalCap = r2BaseCap + r1BaseCap - r1ContributionC1; // 500 + 400 = 900 + uint r2ExpectedEffectiveTotalCap = + r2BaseCap + r1BaseCap - r1ContributionC1; // 500 + 400 = 900 fundingPot.setAccessCriteriaPrivileges( round2Id, accessId, r2ExpectedEffectiveTotalCap, false, 0, 0, 0 ); @@ -3008,7 +3120,12 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { vm.startPrank(contributor1_); fundingPot.contributeToRoundFor( - contributor1_, round2Id, c1AttemptR2, accessId, new bytes32[](0), _unspentPersonalRoundCaps + contributor1_, + round2Id, + c1AttemptR2, + accessId, + new bytes32[](0), + _unspentPersonalRoundCaps ); vm.stopPrank(); @@ -3361,7 +3478,8 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { vm.warp(initialTimestamp + 1 days + 1 hours); vm.startPrank(contributor1_); ILM_PC_FundingPot_v1.UnspentPersonalRoundCap[] memory - unspentPersonalRoundCaps = new ILM_PC_FundingPot_v1.UnspentPersonalRoundCap[](0); + unspentPersonalRoundCaps = + new ILM_PC_FundingPot_v1.UnspentPersonalRoundCap[](0); fundingPot.contributeToRoundFor( contributor1_, round1Id, @@ -3495,7 +3613,12 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { vm.warp(initialTimestamp + 1 days + 1 hours); vm.startPrank(contributor1_); fundingPot.contributeToRoundFor( - contributor1_, round1Id, r1ContributionC1, 1, new bytes32[](0), _unspentPersonalRoundCaps + contributor1_, + round1Id, + r1ContributionC1, + 1, + new bytes32[](0), + _unspentPersonalRoundCaps ); vm.stopPrank(); assertEq( @@ -3530,7 +3653,12 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { vm.warp(initialTimestamp + 3 days + 1 hours); vm.startPrank(contributor1_); fundingPot.contributeToRoundFor( - contributor1_, round2Id, r2ContributionC1, 1, new bytes32[](0), _unspentPersonalRoundCaps + contributor1_, + round2Id, + r2ContributionC1, + 1, + new bytes32[](0), + _unspentPersonalRoundCaps ); vm.stopPrank(); assertEq( @@ -3682,9 +3810,15 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { vm.warp(initialTimestamp + 1 days + 1 hours); vm.startPrank(contributor1_); ILM_PC_FundingPot_v1.UnspentPersonalRoundCap[] memory - unspentPersonalRoundCaps = new ILM_PC_FundingPot_v1.UnspentPersonalRoundCap[](0); + unspentPersonalRoundCaps = + new ILM_PC_FundingPot_v1.UnspentPersonalRoundCap[](0); fundingPot.contributeToRoundFor( - contributor1_, round1Id, r1ContributionC1, 1, new bytes32[](0), unspentPersonalRoundCaps + contributor1_, + round1Id, + r1ContributionC1, + 1, + new bytes32[](0), + unspentPersonalRoundCaps ); vm.stopPrank(); assertEq( @@ -3717,9 +3851,14 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { // --- Contribution by C2 to Round 2 --- vm.warp(initialTimestamp + 3 days + 1 hours); vm.startPrank(contributor2_); - + fundingPot.contributeToRoundFor( - contributor2_, round2Id, r2ContributionC2, 1, new bytes32[](0), unspentPersonalRoundCaps + contributor2_, + round2Id, + r2ContributionC2, + 1, + new bytes32[](0), + unspentPersonalRoundCaps ); vm.stopPrank(); assertEq( @@ -3759,7 +3898,12 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { vm.startPrank(contributor3_); fundingPot.contributeToRoundFor( - contributor3_, round3Id, r3ExpectedEffectiveCap, 1, new bytes32[](0), unspentPersonalRoundCaps + contributor3_, + round3Id, + r3ExpectedEffectiveCap, + 1, + new bytes32[](0), + unspentPersonalRoundCaps ); vm.stopPrank(); @@ -3785,7 +3929,12 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { ) ); fundingPot.contributeToRoundFor( - contributor1_, round3Id, 1, 1, new bytes32[](0), unspentPersonalRoundCaps + contributor1_, + round3Id, + 1, + 1, + new bytes32[](0), + unspentPersonalRoundCaps ); vm.stopPrank(); } @@ -3828,7 +3977,8 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { vm.startPrank(contributor1_); _token.approve(address(fundingPot), type(uint).max); ILM_PC_FundingPot_v1.UnspentPersonalRoundCap[] memory - unspentPersonalRoundCaps = new ILM_PC_FundingPot_v1.UnspentPersonalRoundCap[](0); + unspentPersonalRoundCaps = + new ILM_PC_FundingPot_v1.UnspentPersonalRoundCap[](0); fundingPot.contributeToRoundFor( contributor1_, round1Id, @@ -4005,7 +4155,12 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { vm.startPrank(contributor1_); fundingPot.contributeToRoundFor( - contributor1_, round2Id, c1AttemptR2, accessId, new bytes32[](0), unspentPersonalRoundCaps + contributor1_, + round2Id, + c1AttemptR2, + accessId, + new bytes32[](0), + unspentPersonalRoundCaps ); vm.stopPrank(); @@ -4075,7 +4230,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { round1Id, r1ContributionC1, accessId, - new bytes32[](0), + new bytes32[](0), _unspentPersonalRoundCaps ); vm.stopPrank(); @@ -4241,7 +4396,12 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { vm.startPrank(contributor1_); fundingPot.contributeToRoundFor( - contributor1_, round2Id, c1AttemptR2, accessId, new bytes32[](0), _unspentPersonalRoundCaps + contributor1_, + round2Id, + c1AttemptR2, + accessId, + new bytes32[](0), + _unspentPersonalRoundCaps ); vm.stopPrank(); @@ -4258,8 +4418,6 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { ); } - - function testContributeToRoundFor_totalModeOnlyAccumulatesTotalCaps() public { @@ -4348,9 +4506,14 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { _token.approve(address(fundingPot), 1000); ILM_PC_FundingPot_v1.UnspentPersonalRoundCap[] memory unspentPersonalRoundCaps = - new ILM_PC_FundingPot_v1.UnspentPersonalRoundCap[](0); + new ILM_PC_FundingPot_v1.UnspentPersonalRoundCap[](0); fundingPot.contributeToRoundFor( - contributor1_, round1Id, 600, accessCriteriaId, new bytes32[](0), unspentPersonalRoundCaps + contributor1_, + round1Id, + 600, + accessCriteriaId, + new bytes32[](0), + unspentPersonalRoundCaps ); vm.stopPrank(); @@ -4371,7 +4534,12 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { // Contributor 2 attempts to contribute 700. // Personal Cap (R2) is 300. Gets clamped to 300. fundingPot.contributeToRoundFor( - contributor2_, round2Id, 700, accessCriteriaId, new bytes32[](0), unspentPersonalRoundCaps + contributor2_, + round2Id, + 700, + accessCriteriaId, + new bytes32[](0), + unspentPersonalRoundCaps ); // Verify contributor 2's contribution was clamped by personal cap. assertEq( @@ -4438,7 +4606,12 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { vm.startPrank(contributor3_); _token.approve(address(fundingPot), 300); fundingPot.contributeToRoundFor( - contributor3_, round2Id, 300, accessCriteriaId, new bytes32[](0), unspentPersonalRoundCaps + contributor3_, + round2Id, + 300, + accessCriteriaId, + new bytes32[](0), + unspentPersonalRoundCaps ); // Verify C3 contributed 300 assertEq( @@ -4466,9 +4639,14 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { .Module__LM_PC_FundingPot__RoundCapReached .selector ) - ); + ); fundingPot.contributeToRoundFor( - contributor3_, round2Id, 1, accessCriteriaId, new bytes32[](0), unspentPersonalRoundCaps + contributor3_, + round2Id, + 1, + accessCriteriaId, + new bytes32[](0), + unspentPersonalRoundCaps ); vm.stopPrank(); @@ -4522,7 +4700,12 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { unspentPersonalRoundCaps = new ILM_PC_FundingPot_v1.UnspentPersonalRoundCap[](0); fundingPot.contributeToRoundFor( - contributor1_, round1, 100, 1, new bytes32[](0), unspentPersonalRoundCaps + contributor1_, + round1, + 100, + 1, + new bytes32[](0), + unspentPersonalRoundCaps ); vm.stopPrank(); @@ -4614,7 +4797,12 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { unspentPersonalRoundCaps = new ILM_PC_FundingPot_v1.UnspentPersonalRoundCap[](0); fundingPot.contributeToRoundFor( - contributor1_, round1, 100, 1, new bytes32[](0), unspentPersonalRoundCaps + contributor1_, + round1, + 100, + 1, + new bytes32[](0), + unspentPersonalRoundCaps ); vm.stopPrank(); @@ -4809,8 +4997,6 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { fundingPot.closeRound(roundId); } - - function testCloseRound_revertsGivenClosureConditionsNotMet() public { uint8 accessCriteriaId = uint8(ILM_PC_FundingPot_v1.AccessCriteriaType.OPEN); @@ -4848,7 +5034,12 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { unspentPersonalRoundCaps = new ILM_PC_FundingPot_v1.UnspentPersonalRoundCap[](0); fundingPot.contributeToRoundFor( - contributor1_, roundId, 1000, accessCriteriaId, new bytes32[](0), unspentPersonalRoundCaps + contributor1_, + roundId, + 1000, + accessCriteriaId, + new bytes32[](0), + unspentPersonalRoundCaps ); vm.stopPrank(); @@ -4897,7 +5088,12 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { unspentPersonalRoundCaps = new ILM_PC_FundingPot_v1.UnspentPersonalRoundCap[](0); fundingPot.contributeToRoundFor( - contributor1_, roundId, 1000, accessCriteriaId, new bytes32[](0), unspentPersonalRoundCaps + contributor1_, + roundId, + 1000, + accessCriteriaId, + new bytes32[](0), + unspentPersonalRoundCaps ); vm.stopPrank(); @@ -4945,7 +5141,12 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { unspentPersonalRoundCaps = new ILM_PC_FundingPot_v1.UnspentPersonalRoundCap[](0); fundingPot.contributeToRoundFor( - contributor1_, roundId, 500, accessCriteriaId, new bytes32[](0), unspentPersonalRoundCaps + contributor1_, + roundId, + 500, + accessCriteriaId, + new bytes32[](0), + unspentPersonalRoundCaps ); vm.stopPrank(); @@ -5001,7 +5202,12 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { unspentPersonalRoundCaps = new ILM_PC_FundingPot_v1.UnspentPersonalRoundCap[](0); fundingPot.contributeToRoundFor( - contributor1_, roundId, amount, accessCriteriaId, new bytes32[](0), unspentPersonalRoundCaps + contributor1_, + roundId, + amount, + accessCriteriaId, + new bytes32[](0), + unspentPersonalRoundCaps ); assertEq(fundingPot.roundIdToClosedStatus(roundId), false); @@ -5051,7 +5257,12 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { unspentPersonalRoundCaps = new ILM_PC_FundingPot_v1.UnspentPersonalRoundCap[](0); fundingPot.contributeToRoundFor( - contributor1_, roundId, amount, accessCriteriaId, new bytes32[](0), unspentPersonalRoundCaps + contributor1_, + roundId, + amount, + accessCriteriaId, + new bytes32[](0), + unspentPersonalRoundCaps ); assertEq(fundingPot.roundIdToClosedStatus(roundId), true); @@ -5096,14 +5307,24 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { vm.startPrank(contributor1_); _token.approve(address(fundingPot), 500); fundingPot.contributeToRoundFor( - contributor1_, roundId, 500, accessCriteriaId, new bytes32[](0), unspentPersonalRoundCaps + contributor1_, + roundId, + 500, + accessCriteriaId, + new bytes32[](0), + unspentPersonalRoundCaps ); vm.stopPrank(); vm.startPrank(contributor2_); _token.approve(address(fundingPot), 200); fundingPot.contributeToRoundFor( - contributor2_, roundId, 200, accessCriteriaId, new bytes32[](0), unspentPersonalRoundCaps + contributor2_, + roundId, + 200, + accessCriteriaId, + new bytes32[](0), + unspentPersonalRoundCaps ); vm.stopPrank(); @@ -5111,7 +5332,12 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { _token.approve(address(fundingPot), 300); fundingPot.contributeToRoundFor( - contributor3_, roundId, 300, accessCriteriaId, new bytes32[](0), unspentPersonalRoundCaps + contributor3_, + roundId, + 300, + accessCriteriaId, + new bytes32[](0), + unspentPersonalRoundCaps ); vm.stopPrank(); @@ -5188,7 +5414,6 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { fundingPot.createPaymentOrdersForContributorsBatch(roundId, 1); } - function testCreatePaymentOrdersForContributorsBatch_revertsGivenUserDoesNotHaveFundingPotAdminRole( ) public { testCloseRound_worksWithMultipleContributors(); @@ -5602,7 +5827,12 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { unspentPersonalRoundCaps = new ILM_PC_FundingPot_v1.UnspentPersonalRoundCap[](0); fundingPot.contributeToRoundFor( - contributor1_, roundId, amount, accessCriteriaId, new bytes32[](0), unspentPersonalRoundCaps + contributor1_, + roundId, + amount, + accessCriteriaId, + new bytes32[](0), + unspentPersonalRoundCaps ); assertTrue( @@ -5811,35 +6041,37 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { // Test: contributeToRoundFor Authorization // ========================================================================= - function testContributeToRoundFor_revertsWhenNonOwnerTriesToUseUnspentCaps() public { + function testContributeToRoundFor_revertsWhenNonOwnerTriesToUseUnspentCaps() + public + { // Setup: Create two users - Alice and Bob address alice = address(0x1111); address bob = address(0x2222); - + // Give both users some tokens vm.deal(alice, 10 ether); vm.deal(bob, 10 ether); _token.mint(alice, 1000); _token.mint(bob, 1000); - + // Create first round where Alice contributes vm.startPrank(address(this)); uint32 round1 = fundingPot.createRound( block.timestamp + 1 days, // start - block.timestamp + 7 days, // end - 500, // cap - address(0), // hookContract - "", // hookFunction - false, // autoClosure + block.timestamp + 7 days, // end + 500, // cap + address(0), // hookContract + "", // hookFunction + false, // autoClosure ILM_PC_FundingPot_v1.AccumulationMode.Personal // allow personal accumulation ); - + // Set up access criteria for round 1 address[] memory allowedAddresses = new address[](2); allowedAddresses[0] = alice; allowedAddresses[1] = bob; address[] memory localRemovedAddresses; - + fundingPot.setAccessCriteria( round1, uint8(ILM_PC_FundingPot_v1.AccessCriteriaType.LIST), @@ -5849,25 +6081,27 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { allowedAddresses, localRemovedAddresses ); - + // Set personal cap for access criteria fundingPot.setAccessCriteriaPrivileges( - round1, + round1, 1, // accessCriteriaId - 200, // personalCap + 200, // personalCap false, // overrideContributionSpan - 0, 0, 0 // time parameters + 0, + 0, + 0 // time parameters ); vm.stopPrank(); - + // Alice contributes to round 1 (only partially using her cap) vm.warp(block.timestamp + 1 days + 1); // move to round start vm.startPrank(alice); _token.approve(address(fundingPot), 100); - + ILM_PC_FundingPot_v1.UnspentPersonalRoundCap[] memory emptyUnspentCaps; bytes32[] memory emptyProof; - + fundingPot.contributeToRoundFor( alice, round1, @@ -5877,12 +6111,12 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { emptyUnspentCaps ); vm.stopPrank(); - + // Close round 1 vm.warp(block.timestamp + 7 days); vm.prank(address(this)); fundingPot.closeRound(round1); - + // Create second round with personal accumulation vm.startPrank(address(this)); uint32 round2 = fundingPot.createRound( @@ -5894,7 +6128,7 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { false, ILM_PC_FundingPot_v1.AccumulationMode.Personal ); - + // Set up same access criteria for round 2 fundingPot.setAccessCriteria( round2, @@ -5905,39 +6139,43 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { allowedAddresses, localRemovedAddresses ); - + fundingPot.setAccessCriteriaPrivileges( round2, 1, 200, // same personal cap false, - 0, 0, 0 + 0, + 0, + 0 ); vm.stopPrank(); - + // Create Alice's unspent cap data from round 1 - ILM_PC_FundingPot_v1.UnspentPersonalRoundCap[] memory aliceUnspentCaps = + ILM_PC_FundingPot_v1.UnspentPersonalRoundCap[] memory aliceUnspentCaps = new ILM_PC_FundingPot_v1.UnspentPersonalRoundCap[](1); aliceUnspentCaps[0] = ILM_PC_FundingPot_v1.UnspentPersonalRoundCap({ roundId: round1, accessCriteriaId: 1, merkleProof: emptyProof }); - + // Move to round 2 start vm.warp(block.timestamp + 1 days + 1); - + // Test: Bob tries to use Alice's unspent caps for his own contribution vm.startPrank(bob); _token.approve(address(fundingPot), 50); - + // Should revert with OnlyOwnerCanUseUnspentCaps vm.expectRevert( - ILM_PC_FundingPot_v1.Module__LM_PC_FundingPot__OnlyOwnerCanUseUnspentCaps.selector + ILM_PC_FundingPot_v1 + .Module__LM_PC_FundingPot__OnlyOwnerCanUseUnspentCaps + .selector ); - + fundingPot.contributeToRoundFor( - alice, // Bob contributing FOR Alice + alice, // Bob contributing FOR Alice round2, 50, 1, @@ -5945,11 +6183,11 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { aliceUnspentCaps // Using Alice's unspent caps but called by Bob ); vm.stopPrank(); - + // Verify: Alice can still use her own unspent caps vm.startPrank(alice); _token.approve(address(fundingPot), 150); - + // This should work - Alice using her own unspent caps fundingPot.contributeToRoundFor( alice, @@ -5960,10 +6198,10 @@ contract LM_PC_FundingPot_v1_Test is ModuleTest { aliceUnspentCaps ); vm.stopPrank(); - - // Verify Alice's contribution succeeded + + // Verify Alice's contribution succeeded assertEq(fundingPot.roundIdToUserToContribution(round2, alice), 150); - + // Verify Bob has no contributions (since his attack failed) assertEq(fundingPot.roundIdToUserToContribution(round2, bob), 0); }