Skip to content

Conversation

@IronJam11
Copy link
Contributor

@IronJam11 IronJam11 commented Oct 21, 2025

Summary by CodeRabbit

  • New Features

    • Global pagination across organisations, members/owners, verification requests, proposals, NFTs and verifier details — endpoints now return sliced results plus total counts.
    • Role-specific organisation views and ability to promote members to owners; membership removals propagate role updates.
  • Bug Fixes / Behavior

    • Verification removals now sync and hide verifications consistently; improved ID/limit validations and clearer error handling.
  • Tests

    • Expanded coverage for paginated APIs, role changes, visibility and edge cases.
  • Chores

    • Removed unused deployment variables; CI formatting step now applies formatting.
  • Documentation

    • README replaced with updated project documentation and quickstart.

@coderabbitai
Copy link

coderabbitai bot commented Oct 21, 2025

Walkthrough

Adds pagination (offset, limit, totalCount) to many public getters across Organisation, OrganisationFactory, and TreeNft; records/promotes/removes owner/member relationships in OrganisationFactory; synchronizes verification visibility and optimized removal in TreeNft; updates tests and CI formatting behavior; removes two public variables from the deploy script.

Changes

Cohort / File(s) Summary
Organisation: paginated getters & owner-related flows
src/Organisation.sol
Introduced pagination for getVerificationRequests, getTreePlantingProposals, getMembers, getOwners (now return (array, uint256 totalCount)). Propagates ownership changes to factory: leaveOrganisation and removeMember compute wasOwner and call organisationFactoryContract.removeMemberFromOrganisation(...). makeOwner calls organisationFactoryContract.promoteToOwner(newOwner). Validation/pagination guards added.
OrganisationFactory: role mappings, pagination, promote/remove APIs
src/OrganisationFactory.sol
Added s_userToOrganisationsAsOwner and s_userToOrganisationsAsMember. Converted many getters to paginated forms (getUserOrganisations, getMyOrganisations, getAllOrganisations, getAllOrganisationDetails) returning (..., totalCount). Added getUserOrganisationsAsOwner/AsMember, getMyOrganisationsAsOwner/AsMember, promoteToOwner(address), and removeMemberFromOrganisation(address,bool). Stronger existence guards and error-tolerant detail fetching. paginationLimit present (50).
TreeNft: paginated access & verification visibility sync
src/TreeNft.sol
Added constant maxLimitForPagination. Replaced non-paginated getters with paginated variants (getAllNFTs, getNFTsByUserPaginated, getVerifiedTreesByUserPaginated, getTreeNftVerifiersPaginated, getUserVerifierTokenDetails) returning (array, totalCount). Enhanced verification storage synchronization, added removeVerificationOptimized for index-based atomic removals, and changed removeVerification to mark verifications hidden and update related mappings.
Tests: adapt to paginated APIs & new coverage
test/OrganisationFactory.t.sol, test/TreeNft.t.sol
Updated tests to use offset/limit and unpack (results, totalCount). Added tests for owner/member paginated queries, promote/remove flows, boundary/zero-limit cases, and paginated verifier/token detail behavior. Minor arithmetic adjustment in a token transfer.
Deployment script cleanup
script/DeployAllContracts.s.sol
Removed planterTokenAddress and verifierTokenAddress public state variables.
CI workflow
.github/workflows/test.yml
Changed Forge fmt step from check mode (--check) to apply/format mode (no --check).
Errors / utils
src/utils/errors.sol
Added error MaximumLimitRequestExceeded() to represent pagination-limit violations in some flows.

Sequence Diagram(s)

sequenceDiagram
    actor Admin
    participant Org as Organisation
    participant Factory as OrganisationFactory

    Admin->>Org: makeOwner(newOwner)
    activate Org
    Note right of Org: append newOwner to owners
    Org->>Factory: promoteToOwner(newOwner)
    activate Factory
    Note right of Factory: add org to s_userToOrganisationsAsOwner\nremove from s_userToOrganisationsAsMember if present
    Factory-->>Org: ack
    Org-->>Admin: success
    deactivate Factory
    deactivate Org
Loading
sequenceDiagram
    actor Client
    participant Factory as OrganisationFactory

    Client->>Factory: getMyOrganisationsAsOwner(offset,limit)
    activate Factory
    Note right of Factory: totalCount = len(s_userToOrganisationsAsOwner[user])\nslice = items[offset:offset+limit]
    Factory-->>Client: (OrganisationDetails[], totalCount)
    deactivate Factory
Loading
sequenceDiagram
    actor Verifier
    participant Tree as TreeNft

    Verifier->>Tree: getTreeNftVerifiersPaginated(tokenId,offset,limit)
    activate Tree
    Note right of Tree: compute visible verifications\nreturn slice and counts
    Tree-->>Verifier: (TreeNftVerification[], totalCount, visibleCount)
    deactivate Tree
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested reviewers

  • ceilican
  • Zahnentferner

Poem

🐇 I hopped through lists and trimmed the hay,

Offsets snug, limits set to play.
Owners climb, members softly slid,
Pages yield counts—neat, not hid.
A carrot cheers: small, tidy, spry.

Pre-merge checks and finishing touches

❌ Failed checks (2 warnings)
Check name Status Explanation Resolution
Title Check ⚠️ Warning The title "Function paginations" directly relates to a significant structural theme in this changeset, as pagination support is extended across multiple smart contracts (Organisation.sol, OrganisationFactory.sol, TreeNft.sol) with updated function signatures that include offset/limit parameters and totalCount returns. However, the PR also introduces substantial changes beyond pagination, including role-based organization tracking (owner vs member mappings in OrganisationFactory), ownership promotion mechanics (promoteToOwner function), verification removal optimizations (removeVerificationOptimized), and removal of legacy non-paginated functions. While pagination is clearly a major refactoring theme, the title is somewhat vague and doesn't fully capture the broader scope of architectural changes, which means a developer scanning the history would understand pagination is being added but might miss the significant role-tracking and ownership management enhancements.
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@IronJam11 IronJam11 force-pushed the functionPaginations branch from d67d6cc to 286187d Compare October 21, 2025 10:31
@IronJam11 IronJam11 force-pushed the functionPaginations branch from 286187d to 4af74f6 Compare October 21, 2025 10:50
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/OrganisationFactory.sol (1)

223-236: Bug: removeOrganisation doesn’t flip isActive or clear lookup.

s_isOrganisation is never set to false, and the Organisation lookup isn’t cleared. After removal, getOrganisationInfo will still succeed and “isActive” in details will remain true. Fix as below.

 function removeOrganisation(address _organisationAddress) external onlyOwner {
   // ...
-  if (s_isOrganisation[_organisationAddress] == false) {
+  if (s_isOrganisation[_organisationAddress] == false) {
     revert OrganisationDoesNotExist();
   }
+  // Mark inactive and clear lookup before list removal
+  s_isOrganisation[_organisationAddress] = false;
+  delete s_organisationAddressToOrganisation[_organisationAddress];
   for (uint256 i = 0; i < s_allOrganisations.length; i++) {
     if (s_allOrganisations[i] == _organisationAddress) {
       s_allOrganisations[i] = s_allOrganisations[s_allOrganisations.length - 1];
       s_allOrganisations.pop();
       break;
     }
   }
 }

Also consider emitting an event and optionally pruning the org from role-indexed arrays (may be expensive; soft-deactivation via isActive=false is acceptable if documented).

🧹 Nitpick comments (18)
src/TreeNft.sol (2)

130-145: Cap page size and guard offset+limit overflow in getAllNFTs.

Limit is unchecked and offset+limit can overflow before clamping, risking OOG in contract-to-contract calls. Align with getRecentTreesPaginated’s cap and add an overflow-safe end calculation.

Apply these changes:

@@
-    function getAllNFTs(uint256 offset, uint256 limit) public view returns (Tree[] memory trees, uint256 totalCount) {
+    function getAllNFTs(uint256 offset, uint256 limit) public view returns (Tree[] memory trees, uint256 totalCount) {
         totalCount = s_treeTokenCounter;
-        if (offset >= totalCount) {
+        if (offset >= totalCount) {
             return (new Tree[](0), totalCount);
         }
-        uint256 end = offset + limit;
-        if (end > totalCount) {
-            end = totalCount;
-        }
+        if (limit == 0) {
+            return (new Tree[](0), totalCount);
+        }
+        // Optional: reuse same cap as other endpoints for consistency
+        if (limit > 50) revert PaginationLimitExceeded();
+        uint256 end = offset + limit;
+        // overflow-safe clamp
+        if (end < offset || end > totalCount) {
+            end = totalCount;
+        }
         uint256 resultLength = end - offset;
         trees = new Tree[](resultLength);
         for (uint256 i = 0; i < resultLength; i++) {
             trees[i] = s_tokenIDtoTree[offset + i];
         }
         return (trees, totalCount);
     }

418-450: Avoid full-array copies; add overflow guard and page cap; clarify numberOfTrees semantics.

  • TreeNftVerification[] memory userVerifications copies the entire storage array. Use a storage reference to avoid O(n) copy per call.
  • Guard offset+limit overflow and cap limit like other endpoints.
  • numberOfTrees uses planterToken.balanceOf(userAddress). That returns the user’s current balance for the verifier’s token, not the per-verification tree count. If that’s intended, add a NatSpec note; otherwise return the per-verification amount.

Apply:

-    function getUserVerifierTokenDetails(address userAddress, uint256 offset, uint256 limit)
+    function getUserVerifierTokenDetails(address userAddress, uint256 offset, uint256 limit)
         public
         view
         returns (VerificationDetails[] memory verifierTokenDetails, uint256 totalCount)
     {
-        // This function returns the verifier token details of the user with pagination
-
-        TreeNftVerification[] memory userVerifications = s_userToVerifications[userAddress];
+        // Returns the verifier token details of the user with pagination.
+        // numberOfTrees reflects the user's current balance for each verifier's PlanterToken.
+        TreeNftVerification[] storage userVerifications = s_userToVerifications[userAddress];
         totalCount = userVerifications.length;
-        if (offset >= totalCount) {
+        if (offset >= totalCount) {
             return (new VerificationDetails[](0), totalCount);
         }
-        uint256 end = offset + limit;
-        if (end > totalCount) {
-            end = totalCount;
-        }
+        if (limit == 0) {
+            return (new VerificationDetails[](0), totalCount);
+        }
+        // Optional: unify cap across endpoints
+        if (limit > 50) revert PaginationLimitExceeded();
+        uint256 end = offset + limit;
+        if (end < offset || end > totalCount) {
+            end = totalCount;
+        }
         uint256 resultLength = end - offset;
         verifierTokenDetails = new VerificationDetails[](resultLength);
         for (uint256 i = 0; i < resultLength; i++) {
             uint256 verificationIndex = offset + i;
             PlanterToken planterToken = PlanterToken(userVerifications[verificationIndex].verifierPlanterTokenAddress);
             verifierTokenDetails[i] = VerificationDetails({
                 verifier: userVerifications[verificationIndex].verifier,
                 timestamp: userVerifications[verificationIndex].timestamp,
                 proofHashes: userVerifications[verificationIndex].proofHashes,
                 description: userVerifications[verificationIndex].description,
                 isHidden: userVerifications[verificationIndex].isHidden,
                 numberOfTrees: planterToken.balanceOf(userAddress),
                 verifierPlanterTokenAddress: userVerifications[verificationIndex].verifierPlanterTokenAddress
             });
         }
         return (verifierTokenDetails, totalCount);
     }

Optionally, add a brief NatSpec above the function clarifying the meaning of numberOfTrees to avoid confusion.

test/TreeNft.t.sol (1)

135-139: Strengthen pagination assertions and edge coverage.

Good migration to paginated getters. Two improvements:

  • Also assert totalCount/visibleCount where available to ensure the count semantics remain correct under slicing and after mutations (e.g., after removeVerification).
  • Add tests for overflow and cap behavior: offset near type max and limit > cap should revert with PaginationLimitExceeded once caps are enforced consistently.

Example tweaks:

  • Capture (,,visibleCount) from getTreeNftVerifiersPaginated and assert visibleCount matches expected after hiding.
  • For getUserVerifierTokenDetails, assert totalCount across pages equals the sum of returned lengths, and that offset >= totalCount yields empty arrays.

Would you like me to push a follow-up test commit covering these cases once the caps/overflow guards are added?

Also applies to: 162-172, 215-221, 308-312, 332-336, 429-446, 474-486, 515-535, 537-543, 545-572, 574-599, 600-631

src/OrganisationFactory.sol (11)

38-38: Creation path OK; verify returned organisationId semantics.

Pushing creator into owner index is correct. However, createOrganisation still returns organisationId that is always 0. Either implement an incrementing ID or drop it from the return to avoid misleading callers.


45-65: Pagination slice looks correct.

Bounds checks and slicing are fine; zero limit yields empty set. Consider extracting a small internal helper to DRY with other paginated address getters.


67-73: Avoid external self-call for gas.

return this.getUserOrganisations(...) incurs an external call. Prefer an internal/private helper to slice once and reuse.


75-94: All-organisations pagination LGTM.

Slice math and early-exit are correct. Same note as above about extracting a helper.


102-114: Minor: redundant and missing validations.

  • msg.sender can’t be address(0); that check is redundant after s_isOrganisation[msg.sender].
  • Consider require _member != address(0) as a defensive check.

116-135: promoteToOwner indexing can drift without invariant checks.

Any factory-created org can call this for an arbitrary address. Add guards like:

  • require Organisation(msg.sender).checkMembership(_member)
  • require !Organisation(msg.sender).checkOwnership(_member)
    to keep indexes aligned with on-chain truth.

158-214: Detail fetcher is robust; consider lighter payload option.

try/catch fallback is good. Optionally add a variant that returns only addresses + counts to cut calldata/gas for UIs that don’t need full owners/members arrays.


243-310: Owner-indexed pagination LGTM; note N external calls.

Logic is correct and matches tests (including limit=0 returns empty). Be aware this does N external calls to child orgs; acceptable for view endpoints, but consider a lighter address-only variant for larger pages.


312-379: Member-indexed pagination LGTM; same perf note.

Mirrors owner flow correctly and handles limit=0. Same suggestion about an address-only variant.


381-389: Prefer internal helper over external self-call.

Use an internal/private function to avoid external call overhead.


391-399: Same: internal helper would be cheaper.

Mirror note from getMyOrganisationsAsOwner.

test/OrganisationFactory.t.sol (1)

397-409: Zero limit behavior asserted. Add negative case tests.

Nice guard. Please add tests for:

  • User leaves org → removed from member index.
  • Owner removes member → member index pruned.
    These will fail today (no factory hooks) and will guide the fixes suggested in the factory review.
src/Organisation.sol (3)

178-211: Pagination logic correct; consider input harmonization.

Works as intended. Optional: align with ByStatus variants by rejecting limit == 0 or enforcing paginationLimit for consistency.


333-366: Tree proposals pagination OK; same optional consistency note.

Matches verification requests logic; consider consistent limit validation with paginationLimit.


461-462: Factory callback is the right place; ensure lifecycle symmetry.

Calling organisationFactoryContract.promoteToOwner(newOwner) keeps indexes up-to-date on promotion. Please add similar callbacks for leaveOrganisation/removeMember to keep member/owner indexes in sync (see factory review).

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 41e9f4d and 4af74f6.

📒 Files selected for processing (6)
  • script/DeployAllContracts.s.sol (0 hunks)
  • src/Organisation.sol (5 hunks)
  • src/OrganisationFactory.sol (7 hunks)
  • src/TreeNft.sol (3 hunks)
  • test/OrganisationFactory.t.sol (3 hunks)
  • test/TreeNft.t.sol (9 hunks)
💤 Files with no reviewable changes (1)
  • script/DeployAllContracts.s.sol
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-05-26T10:22:04.651Z
Learnt from: ceilican
PR: StabilityNexus/Treee-Solidity#4
File: src/Organisation.sol:4-4
Timestamp: 2025-05-26T10:22:04.651Z
Learning: The Organisation contract in src/Organisation.sol uses a multi-owner design pattern with `address[] public owners` and custom ownership checking, which is incompatible with OpenZeppelin's single-owner Ownable contract. The multi-owner system is required for voting mechanisms on verification requests.

Applied to files:

  • src/OrganisationFactory.sol
🔇 Additional comments (17)
src/TreeNft.sol (1)

269-279: Good state sync for user verifications during removal.

Mirroring the hidden flag into s_userToVerifications keeps public views consistent after removal. This closes a visibility inconsistency.

src/OrganisationFactory.sol (1)

151-153: Good: explicit revert for unknown organisation.

Clearer failure mode for bad inputs.

test/OrganisationFactory.t.sol (11)

103-114: Tests updated for paginated getMyOrganisations are good.

Covers count and page basics.


126-129: Detail retrieval assertions look solid.

Validates totalCount and basic fields.


151-154: IDs pagination smoke test OK.

Confirms count and list size.


156-225: Owner-indexed tests are comprehensive.

Covers listing and pagination windows.


226-249: Member-indexed happy path covered.

Ensures owner vs member separation.


251-281: Member pagination windows covered.

Looks good.


282-314: Promotion flow tested end-to-end.

Validates counts flip; good coverage.


315-335: getMyOrganisationsAsOwner test OK.

Asserts ordering and count.


336-354: getMyOrganisationsAsMember test OK.

Happy path verified.


356-383: Mixed-role scenario covered.

Confirms separation across orgs.


384-396: Empty results case covered.

Both roles zero.

src/Organisation.sol (4)

327-331: Good: explicit revert on bad proposal ID.

Bounds check is correct.


413-415: Vote bounds check LGTM.

Protects against out-of-range proposal IDs pre-vote.


486-505: Members pagination LGTM.

Slice math and early exit are correct.


507-526: Owners pagination LGTM.

Mirrors members; correct.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (3)
src/OrganisationFactory.sol (2)

102-114: Consider removing defensive ownership check.

Currently, addMemberToOrganisation is only invoked from Organisation.addMember, which already ensures the user is not a member (line 78 in Organisation.sol checks checkMembership). Since owners are also members, checkOwnership(_member) at line 109 will always return false in the current flow, making lines 109-110 unreachable. The else branch (lines 111-113) will always execute.

If this check is intended for future extensibility, consider adding a comment explaining the design intent. Otherwise, simplify:

 function addMemberToOrganisation(address _member) external {
-    // This function adds a member to an organization (called by Organisation contract)
     if (!s_isOrganisation[msg.sender]) revert InvalidOrganisation();
     if (msg.sender == address(0)) revert OrganisationDoesNotExist();
     s_userToOrganisations[_member].push(msg.sender);
-
-    Organisation org = Organisation(msg.sender);
-    if (org.checkOwnership(_member)) {
-        s_userToOrganisationsAsOwner[_member].push(msg.sender);
-    } else {
-        s_userToOrganisationsAsMember[_member].push(msg.sender);
-    }
+    // New members are added to member-only mapping; promotion happens via promoteToOwner
+    s_userToOrganisationsAsMember[_member].push(msg.sender);
 }

289-425: Consider extracting shared logic to reduce duplication.

getUserOrganisationsAsOwner (lines 289-356) and getUserOrganisationsAsMember (lines 358-425) contain nearly identical pagination and detail-fetching logic (~140 lines total). This duplication increases maintenance burden—future changes must be applied to both functions.

Refactor into a shared internal helper:

function _getUserOrganisationDetailsByRole(
    address[] storage roleOrgs,
    uint256 offset,
    uint256 limit
) private view returns (OrganisationDetails[] memory orgs, uint256 totalCount) {
    totalCount = roleOrgs.length;
    if (offset >= totalCount || limit == 0) {
        return (new OrganisationDetails[](0), totalCount);
    }
    
    uint256 end = offset + limit;
    if (end > totalCount) end = totalCount;
    uint256 resultLength = end - offset;
    orgs = new OrganisationDetails[](resultLength);
    
    for (uint256 i; i < resultLength;) {
        address orgAddr = roleOrgs[offset + i];
        Organisation org = Organisation(orgAddr);
        
        try org.getOrganisationInfo() returns (
            address orgAddress,
            string memory name,
            string memory description,
            string memory photoIpfsHash,
            address[] memory owners,
            address[] memory members,
            uint256 timeOfCreation
        ) {
            orgs[i] = OrganisationDetails({
                contractAddress: orgAddress,
                name: name,
                description: description,
                organisationPhoto: photoIpfsHash,
                owners: owners,
                members: members,
                ownerCount: owners.length,
                memberCount: members.length,
                isActive: s_isOrganisation[orgAddress],
                timeOfCreation: timeOfCreation
            });
        } catch {
            orgs[i] = OrganisationDetails({
                contractAddress: orgAddr,
                name: "ERROR: Unable to fetch",
                description: "ERROR: Contract call failed",
                organisationPhoto: "",
                owners: new address[](0),
                members: new address[](0),
                ownerCount: 0,
                memberCount: 0,
                isActive: false,
                timeOfCreation: 0
            });
        }
        unchecked { ++i; }
    }
    return (orgs, totalCount);
}

function getUserOrganisationsAsOwner(address _user, uint256 offset, uint256 limit)
    external view returns (OrganisationDetails[] memory orgs, uint256 totalCount)
{
    return _getUserOrganisationDetailsByRole(s_userToOrganisationsAsOwner[_user], offset, limit);
}

function getUserOrganisationsAsMember(address _user, uint256 offset, uint256 limit)
    external view returns (OrganisationDetails[] memory orgs, uint256 totalCount)
{
    return _getUserOrganisationDetailsByRole(s_userToOrganisationsAsMember[_user], offset, limit);
}
src/Organisation.sol (1)

182-215: Pagination logic is correct; consider gas costs for large datasets.

The two-pass approach (count matches, then collect slice) is correct for filtered pagination. However, for organisations with many verification requests, the full iteration could approach gas limits.

For production with large datasets, consider:

  • Indexing by status to avoid full iteration
  • Off-chain indexing with events for historical queries
  • Setting a maximum verificationCounter that can be scanned
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4af74f6 and f604853.

📒 Files selected for processing (3)
  • .github/workflows/test.yml (1 hunks)
  • src/Organisation.sol (9 hunks)
  • src/OrganisationFactory.sol (7 hunks)
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-05-26T10:22:04.651Z
Learnt from: ceilican
PR: StabilityNexus/Treee-Solidity#4
File: src/Organisation.sol:4-4
Timestamp: 2025-05-26T10:22:04.651Z
Learning: The Organisation contract in src/Organisation.sol uses a multi-owner design pattern with `address[] public owners` and custom ownership checking, which is incompatible with OpenZeppelin's single-owner Ownable contract. The multi-owner system is required for voting mechanisms on verification requests.

Applied to files:

  • src/OrganisationFactory.sol
🔇 Additional comments (14)
src/OrganisationFactory.sol (7)

14-15: Role-indexed mappings with lifecycle support added.

The role-specific mappings introduced here are properly synchronized throughout the lifecycle via promoteToOwner and removeMemberFromOrganisation hooks called from the Organisation contract. This addresses the previous review concern about stale mappings.


116-135: LGTM! Efficient role promotion implementation.

The swap-and-pop pattern is gas-efficient, and the unchecked increment is safe for array iteration. The function correctly updates role-specific mappings when a member becomes an owner.


137-180: LGTM! Complete removal logic with role awareness.

The function correctly removes members from both the general mapping and the appropriate role-specific mapping. The _wasOwner flag is computed by the Organisation contract before removal, ensuring accurate cleanup.


45-94: LGTM! Pagination implemented correctly.

The pagination logic properly handles edge cases:

  • Returns empty array with totalCount when offset is out of bounds
  • Correctly caps the end index at totalCount
  • Provides totalCount for client-side pagination UI

196-198: LGTM! Added validation for organisation existence.

The revert check prevents attempting to fetch information from unrecognized organisation addresses, improving error handling.


203-259: LGTM! Resilient paginated details fetcher.

The try-catch pattern ensures that failures in individual organisation contracts don't break the entire query, returning placeholder error data for failed calls. The pagination logic is consistent with other functions.


427-445: LGTM! Clean delegation pattern for caller convenience.

These wrapper functions provide a convenient API for callers to query their own organisations without explicitly passing their address.

src/Organisation.sol (7)

84-112: LGTM! Proper factory integration for member removal.

The wasOwner flag is correctly computed before modifying the arrays (line 92), ensuring accurate role information is passed to the factory's removeMemberFromOrganisation call (line 110). This maintains consistency between Organisation and OrganisationFactory state.


114-136: LGTM! Consistent factory integration in removeMember.

The implementation mirrors leaveOrganisation—capturing the wasOwner flag (line 119) before state changes and calling the factory (line 134) to maintain synchronized role mappings.


176-178: Critical bug fix: bounds check now reachable.

The previous condition verificationID >= s_verificationCounter && verificationID < 0 was unreachable because uint256 cannot be negative, making the entire check always false. The fix correctly validates only the upper bound, ensuring invalid IDs now properly revert.

This addresses the critical bug flagged in the previous review.


331-333: LGTM! Consistent bounds check fixes for proposal IDs.

The same critical fix applied to verification IDs has been consistently applied to proposal ID validation in getTreePlantingProposal (lines 331-333) and voteOnTreePlantingProposal (lines 417-419).

Also applies to: 417-419


337-370: LGTM! Consistent pagination for tree proposals.

The implementation follows the same correct two-pass pattern as getVerificationRequests. The same gas considerations for large datasets apply here as well.


458-466: LGTM! Proper factory notification on promotion.

After adding the member to the owners array (line 464), the factory is correctly notified (line 465) so it can update the role-specific mappings from member-only to owner.


490-530: LGTM! Clean pagination for members and owners.

Both getMembers and getOwners implement straightforward array slicing with proper bounds checking. Since no filtering is required, these are more gas-efficient than the filtered pagination functions.

@IronJam11 IronJam11 force-pushed the functionPaginations branch 6 times, most recently from a6318f0 to 07f2790 Compare October 21, 2025 12:47
@IronJam11 IronJam11 force-pushed the functionPaginations branch from 07f2790 to a99b560 Compare October 21, 2025 12:49
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
src/TreeNft.sol (3)

168-193: Add limit validation to prevent DoS.

This function lacks validation on the limit parameter, allowing potentially unbounded array allocation and iteration.

Apply this diff:

     returns (Tree[] memory trees, uint256 totalCount)
 {
     // Get the total number of NFTs for this user
+    if (limit > 50) revert PaginationLimitExceeded();

397-421: Add limit validation to prevent DoS.

This function lacks validation on the limit parameter.

Apply this diff:

     returns (Tree[] memory trees, uint256 totalCount)
 {
     // Get the total number of trees verified by this verifier
+    if (limit > 50) revert PaginationLimitExceeded();

423-460: Add limit validation to prevent DoS.

While the filtering logic for hidden verifications is well-implemented, this function lacks validation on the limit parameter.

Apply this diff:

     returns (TreeNftVerification[] memory verifications, uint256 totalCount, uint256 visiblecount)
 {
     // This function retrieves all verifiers for a specific tree with pagination
+    if (limit > 50) revert PaginationLimitExceeded();
🧹 Nitpick comments (5)
src/Organisation.sol (4)

182-215: Add validation for limit parameter.

The pagination logic is sound, but there's no validation that limit is non-zero or reasonable. While view functions don't consume gas when called externally, a limit of 0 would cause unnecessary iteration. Consider adding a check like if (limit == 0) revert InvalidInput(); for consistency with getVerificationRequestsByStatus (line 222).

 function getVerificationRequests(uint256 status, uint256 offset, uint256 limit)
     external
     view
     returns (OrganisationVerificationRequest[] memory requests, uint256 totalCount)
 {
+    if (limit == 0) revert InvalidInput();
     uint256 matchCount = 0;

337-370: Add validation for limit parameter.

Same issue as getVerificationRequests - missing validation that limit is non-zero. Consider adding validation for consistency with getTreePlantingProposalsByStatus (line 379).

 function getTreePlantingProposals(uint256 status, uint256 offset, uint256 limit)
     external
     view
     returns (TreePlantingProposal[] memory proposals, uint256 totalCount)
 {
+    if (limit == 0) revert InvalidInput();
     uint256 matchCount = 0;

490-509: Add validation for limit parameter.

The pagination logic is correct, but consider adding validation that limit > 0 for consistency and to prevent unnecessary processing.

 function getMembers(uint256 offset, uint256 limit)
     external
     view
     returns (address[] memory memberList, uint256 totalCount)
 {
+    if (limit == 0) revert InvalidInput();
     totalCount = members.length;

511-530: Add validation for limit parameter.

Same issue as getMembers - consider adding validation that limit > 0.

 function getOwners(uint256 offset, uint256 limit)
     external
     view
     returns (address[] memory ownerList, uint256 totalCount)
 {
+    if (limit == 0) revert InvalidInput();
     totalCount = owners.length;
src/OrganisationFactory.sol (1)

271-274: Simplify boolean comparison.

The explicit == false comparison is unnecessary.

-    if (s_isOrganisation[_organisationAddress] == false) {
+    if (!s_isOrganisation[_organisationAddress]) {
         revert OrganisationDoesNotExist();
     }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f604853 and 0322077.

📒 Files selected for processing (4)
  • src/Organisation.sol (9 hunks)
  • src/OrganisationFactory.sol (7 hunks)
  • src/TreeNft.sol (6 hunks)
  • test/TreeNft.t.sol (9 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • test/TreeNft.t.sol
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-05-26T10:22:04.651Z
Learnt from: ceilican
PR: StabilityNexus/Treee-Solidity#4
File: src/Organisation.sol:4-4
Timestamp: 2025-05-26T10:22:04.651Z
Learning: The Organisation contract in src/Organisation.sol uses a multi-owner design pattern with `address[] public owners` and custom ownership checking, which is incompatible with OpenZeppelin's single-owner Ownable contract. The multi-owner system is required for voting mechanisms on verification requests.

Applied to files:

  • src/Organisation.sol
  • src/OrganisationFactory.sol
🔇 Additional comments (21)
src/Organisation.sol (6)

92-92: LGTM! Correct role tracking for factory notification.

The wasOwner flag is properly captured before member removal and passed to the factory, enabling the factory to update its role-specific mappings correctly.

Also applies to: 110-110


119-119: LGTM! Consistent role tracking in removeMember.

The same wasOwner pattern is correctly applied here, maintaining consistency with the leaveOrganisation flow.

Also applies to: 134-134


176-178: LGTM! Bounds check correctly simplified.

The previous buggy condition (&& verificationID < 0 on an unsigned int) has been properly removed. The check now correctly validates only the upper bound.


331-333: LGTM! Bounds check correctly simplified.

The same fix as applied to verification requests - correctly validates only the upper bound.


417-419: LGTM! Consistent bounds check.

Correctly validates the upper bound before accessing the mapping.


465-465: LGTM! Correct factory integration for role promotion.

The call to organisationFactoryContract.promoteToOwner(newOwner) after adding to the local owners array ensures the factory's role-specific mappings stay synchronized.

src/OrganisationFactory.sol (14)

14-15: LGTM! Good addition of role-specific mappings.

These private mappings enable efficient querying of organisations by user role. The separation between owner and member-only tracking is a clean design.


38-38: LGTM! Correct initialization of owner mapping.

The creator is properly tracked in the owner-specific mapping at organisation creation. Note that they're not added to s_userToOrganisationsAsMember since that mapping is for non-owner members only.


102-114: LGTM! Defensive role checking in member addition.

The function correctly checks whether the added member is an owner and tracks them in the appropriate role-specific mapping. The check on line 109 is defensive—in normal flow, addMember is only called for new non-owner members, so this branch might rarely execute.


116-135: LGTM! Clean role promotion logic with gas optimization.

The function correctly removes the member from the member-only array and adds them to the owner array. The use of unchecked increment is appropriate for gas savings in a bounded loop. If the member is not found in the array (edge case), the function silently continues, which is acceptable since they'll be added to the owner array regardless.


137-180: LGTM! Comprehensive member removal with role handling.

The function properly handles removal from both the general mapping and the appropriate role-specific mapping based on the _wasOwner flag. The use of unchecked increments is appropriate for gas optimization.


196-198: LGTM! Good defensive validation.

Adding the existence check prevents returning stale data for unregistered organisations.


45-64: LGTM! Clean pagination implementation.

The pagination logic correctly handles edge cases (offset >= totalCount) and returns both the slice and total count. No validation on limit is needed here since view functions don't consume gas when called externally.


67-73: LGTM! Clean delegation pattern.

Correctly delegates to the paginated getUserOrganisations with msg.sender.


75-94: LGTM! Consistent pagination implementation.

Follows the same clean pattern as getUserOrganisations.


203-259: LGTM! Robust error handling in batch operation.

The use of try-catch to handle individual organisation call failures is good defensive programming. Returning placeholder error data prevents one failing organisation from breaking the entire batch query.


289-356: LGTM! Well-implemented role-specific paginated query.

The function correctly retrieves organisations where the user is an owner, with proper pagination, error handling via try-catch, and gas optimization via unchecked increments. The pattern is clean and consistent.


358-425: LGTM! Consistent with owner query implementation.

Follows the same robust pattern as getUserOrganisationsAsOwner but for member-only organisations.


427-435: LGTM! Clean wrapper for caller's owner organisations.

Correctly delegates to the user-specific function with msg.sender.


437-445: LGTM! Clean wrapper for caller's member organisations.

Correctly delegates to the user-specific function with msg.sender.

src/TreeNft.sol (1)

224-226: Approve the verification state tracking.

The verification tracking correctly updates all necessary mappings including the newly added s_userToVerifications mapping.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

♻️ Duplicate comments (3)
src/TreeNft.sol (3)

242-324: Critical: Reset verification mapping to allow re-verification.

s_tokenIDtoUserVerification[_tokenId][verifier] is never reset to false after removal. This permanently prevents the verifier from verifying this tree again, even if the removal was a mistake or new proof becomes available.

Add this line after marking the verification as hidden (around line 287):

             if (treeNftVerification.verifier == verifier && !treeNftVerification.isHidden) {
                 treeNftVerification.isHidden = true;
+                s_tokenIDtoUserVerification[_tokenId][verifier] = false;
 
                 // Also update the verification in the user's array

326-398: Critical: Reset verification mapping to allow re-verification.

Like removeVerification, this function never resets s_tokenIDtoUserVerification[tokenId][verifier] to false. The verifier cannot re-verify the tree after removal.

Add this line after marking verifications as hidden (around line 371):

     verification.isHidden = true;
     userVerifications[_userVerificationIndex].isHidden = true;
+    s_tokenIDtoUserVerification[tokenId][verifier] = false;
 
     User storage user = s_addressToUser[verifier];

513-545: Add limit validation and zero-address check.

This function has two issues:

  1. Missing limit validation — No check against maxLimitForPagination, enabling DoS via unbounded queries.
  2. No zero-address check — Line 533 creates a PlanterToken instance without verifying the address is non-zero. While verify() always initializes a PlanterToken, defensive programming suggests validating.

Apply this diff:

 function getUserVerifierTokenDetails(address userAddress, uint256 offset, uint256 limit)
     public
     view
     returns (VerificationDetails[] memory verifierTokenDetails, uint256 totalCount)
 {
     // This function returns the verifier token details of the user with pagination
+    if (limit > maxLimitForPagination) revert MaximumLimitRequestExceeded();
 
     TreeNftVerification[] memory userVerifications = s_userToVerifications[userAddress];
     totalCount = userVerifications.length;
     if (offset >= totalCount) {
         return (new VerificationDetails[](0), totalCount);
     }
     uint256 end = offset + limit;
     if (end > totalCount) {
         end = totalCount;
     }
     uint256 resultLength = end - offset;
     verifierTokenDetails = new VerificationDetails[](resultLength);
     for (uint256 i = 0; i < resultLength; i++) {
         uint256 verificationIndex = offset + i;
-        PlanterToken planterToken = PlanterToken(userVerifications[verificationIndex].verifierPlanterTokenAddress);
+        address planterTokenAddr = userVerifications[verificationIndex].verifierPlanterTokenAddress;
+        uint256 numberOfTrees = 0;
+        if (planterTokenAddr != address(0)) {
+            PlanterToken planterToken = PlanterToken(planterTokenAddr);
+            numberOfTrees = planterToken.balanceOf(userAddress);
+        }
         verifierTokenDetails[i] = VerificationDetails({
             verifier: userVerifications[verificationIndex].verifier,
             timestamp: userVerifications[verificationIndex].timestamp,
             proofHashes: userVerifications[verificationIndex].proofHashes,
             description: userVerifications[verificationIndex].description,
             isHidden: userVerifications[verificationIndex].isHidden,
-            numberOfTrees: planterToken.balanceOf(userAddress),
+            numberOfTrees: numberOfTrees,
             verifierPlanterTokenAddress: userVerifications[verificationIndex].verifierPlanterTokenAddress
         });
     }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0322077 and ae804b3.

📒 Files selected for processing (6)
  • README.md (1 hunks)
  • src/Organisation.sol (11 hunks)
  • src/OrganisationFactory.sol (6 hunks)
  • src/TreeNft.sol (8 hunks)
  • src/utils/errors.sol (1 hunks)
  • test/TreeNft.t.sol (9 hunks)
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-05-26T10:22:04.651Z
Learnt from: ceilican
PR: StabilityNexus/Treee-Solidity#4
File: src/Organisation.sol:4-4
Timestamp: 2025-05-26T10:22:04.651Z
Learning: The Organisation contract in src/Organisation.sol uses a multi-owner design pattern with `address[] public owners` and custom ownership checking, which is incompatible with OpenZeppelin's single-owner Ownable contract. The multi-owner system is required for voting mechanisms on verification requests.

Applied to files:

  • src/OrganisationFactory.sol
🔇 Additional comments (7)
src/utils/errors.sol (1)

48-48: LGTM!

The new MaximumLimitRequestExceeded error provides clearer semantics for pagination limit violations.

README.md (1)

1-177: LGTM!

The README has been significantly improved with better organization, social badges, and clearer quickstart instructions.

test/TreeNft.t.sol (1)

488-636: LGTM!

Comprehensive test coverage for getUserVerifierTokenDetailsPaginated, including edge cases (empty results, boundaries, hidden verifications).

src/OrganisationFactory.sol (4)

14-15: LGTM!

The role-specific mappings enable efficient filtering of organisations by user role, addressing the past review comment about maintaining role-indexed data.


120-139: LGTM!

promoteToOwner correctly removes the member from the member-only array and adds them to the owner array, ensuring role mappings stay synchronized with Organisation state.


141-184: LGTM!

removeMemberFromOrganisation properly cleans up both general and role-specific mappings using swap-and-pop. The _wasOwner parameter ensures correct array cleanup.


287-355: LGTM!

getUserOrganisationsAsOwner provides paginated access to organisations where the user has owner privileges, with proper error handling via try-catch for failed contract calls.

Comment on lines +492 to 511
function getMembers(uint256 offset, uint256 limit)
external
view
returns (address[] memory memberList, uint256 totalCount)
{
totalCount = members.length;
if (offset >= totalCount) {
return (new address[](0), totalCount);
}
uint256 end = offset + limit;
if (end > totalCount) {
end = totalCount;
}
uint256 resultLength = end - offset;
memberList = new address[](resultLength);
for (uint256 i = 0; i < resultLength; i++) {
memberList[i] = members[offset + i];
}
return (memberList, totalCount);
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Add limit validation to prevent unbounded queries.

getMembers lacks a paginationLimit check, unlike getOwners (line 520). An attacker could pass an extremely large limit, causing excessive gas consumption.

Apply this diff to add validation:

 function getMembers(uint256 offset, uint256 limit)
     external
     view
     returns (address[] memory memberList, uint256 totalCount)
 {
+    if (limit > paginationLimit) revert MaximumLimitRequestExceeded();
     totalCount = members.length;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
function getMembers(uint256 offset, uint256 limit)
external
view
returns (address[] memory memberList, uint256 totalCount)
{
totalCount = members.length;
if (offset >= totalCount) {
return (new address[](0), totalCount);
}
uint256 end = offset + limit;
if (end > totalCount) {
end = totalCount;
}
uint256 resultLength = end - offset;
memberList = new address[](resultLength);
for (uint256 i = 0; i < resultLength; i++) {
memberList[i] = members[offset + i];
}
return (memberList, totalCount);
}
function getMembers(uint256 offset, uint256 limit)
external
view
returns (address[] memory memberList, uint256 totalCount)
{
if (limit > paginationLimit) revert MaximumLimitRequestExceeded();
totalCount = members.length;
if (offset >= totalCount) {
return (new address[](0), totalCount);
}
uint256 end = offset + limit;
if (end > totalCount) {
end = totalCount;
}
uint256 resultLength = end - offset;
memberList = new address[](resultLength);
for (uint256 i = 0; i < resultLength; i++) {
memberList[i] = members[offset + i];
}
return (memberList, totalCount);
}
🤖 Prompt for AI Agents
In src/Organisation.sol around lines 492 to 511, getMembers currently accepts an
unchecked limit which allows callers to request an arbitrarily large page and
may cause excessive gas; add the same pagination limit validation used in
getOwners (around line 520) by requiring the supplied limit is > 0 and <=
paginationLimit (or if zero should be treated as a max, enforce accordingly),
reverting with a clear message like "limit too large" when exceeded; keep the
rest of the pagination logic intact so offset/limit trimming still occurs after
the validation.

treeNft.removeVerification(0, verifier1);
assertFalse(treeNft.isVerified(0, verifier1));
TreeNftVerification[] memory verifications = treeNft.getTreeNftVerifiers(0);
assertTrue(treeNft.isVerified(0, verifier1));
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Test assertion confirms the re-verification bug.

This assertion expects isVerified to return true after removeVerification, which reveals the underlying bug where s_tokenIDtoUserVerification is never reset. Once the contract bug is fixed (resetting the mapping to false on removal), this test will need updating.

After fixing the contract, update this assertion:

-        assertTrue(treeNft.isVerified(0, verifier1));
+        assertFalse(treeNft.isVerified(0, verifier1));

Same issue applies to lines 166, 271, 445, 481, and 652.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
assertTrue(treeNft.isVerified(0, verifier1));
assertFalse(treeNft.isVerified(0, verifier1));
🤖 Prompt for AI Agents
In test/TreeNft.t.sol at line 134 (and similarly update assertions at lines 166,
271, 445, 481, and 652), the test currently asserts that isVerified(tokenId,
verifier) returns true after removeVerification, which matches a known contract
bug; after fixing the contract to reset
s_tokenIDtoUserVerification[tokenId][verifier] = false on removal, change these
tests to assertFalse(isVerified(...)) (or assertEq to false) to reflect the
corrected behavior so the tests validate removal correctly.

@Zahnentferner Zahnentferner merged commit 84300ba into StabilityNexus:main Oct 24, 2025
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants