diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 4a17816..5b840e6 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -32,7 +32,7 @@ jobs: - name: Run Forge fmt run: | - forge fmt --check + forge fmt id: fmt - name: Run Forge build diff --git a/README.md b/README.md index 9265b45..9d9c471 100644 --- a/README.md +++ b/README.md @@ -1,66 +1,177 @@ -## Foundry + +
-**Foundry is a blazing fast, portable and modular toolkit for Ethereum application development written in Rust.** -Foundry consists of: + +
+ image + image -- **Forge**: Ethereum testing framework (like Truffle, Hardhat and DappTools). -- **Cast**: Swiss army knife for interacting with EVM smart contracts, sending transactions and getting chain data. -- **Anvil**: Local Ethereum node, akin to Ganache, Hardhat Network. -- **Chisel**: Fast, utilitarian, and verbose solidity REPL. +
-## Documentation +  -https://book.getfoundry.sh/ + +
-## Usage +[![Static Badge](https://img.shields.io/badge/Stability_Nexus-/Treee-228B22?style=for-the-badge&labelColor=FFC517)](https://treee.stability.nexus/) -### Build +
-```shell -$ forge build + +

+ +Telegram Badge +   + +X (formerly Twitter) Badge +   + +Discord Badge +   + + Medium Badge +   + + LinkedIn Badge +   + + Youtube Badge +

+ +--- + +
+

Treee — Solidity Contracts (Foundry)

+
+ +**Treee** is the smart contract layer of the *Treee tree-planting protocol*, enabling on-chain verification, organization management, and NFT issuance for planted trees. +It powers transparent and auditable sustainability tracking within the **Stability Nexus** ecosystem. + +--- + +## Tech Stack + +### Core + +- **Solidity (v0.8.28)** — Smart contract development +- **Foundry (forge, anvil, cast)** — Testing, simulation & deployment +- **OpenZeppelin Contracts** — Token standards and access control + +### Contracts + +- `TreeNft.sol` — NFT for planted trees +- `Organisation.sol` — Handles organization logic +- `OrganisationFactory.sol` — Deploys and tracks organizations +- `CareToken.sol`, `PlanterToken.sol`, `LegacyToken.sol` — Incentive ERC-20 tokens + +--- + +## Getting Started + +### Prerequisites + +- [Foundry](https://book.getfoundry.sh/getting-started/installation) +- Rust toolchain (`rustup`) +- Node.js (optional, for deployment scripting) + +--- + +### Quickstart + +#### 1. Clone the repository +```bash +git clone https://github.com/StabilityNexus/Treee-Solidity.git +cd Treee-Solidity ``` -### Test +#### 2. Build contracts -```shell -$ forge test +```bash +forge build ``` -### Format +#### 3. Run tests -```shell -$ forge fmt +```bash +forge test ``` -### Gas Snapshots +#### 4. Start a local node -```shell -$ forge snapshot +```bash +anvil ``` -### Anvil +#### 5. Deploy (example) -```shell -$ anvil +```bash +forge script script/Deploy.s.sol:DeployAllContractsAtOnce \ + --rpc-url $SEPOLIA_RPC_URL \ + --private-key $PRIVATE_KEY \ + --broadcast ``` -### Deploy +--- -```shell -$ forge script script/Counter.s.sol:CounterScript --rpc-url --private-key +## Developer Workflow + +Run a local node: + +```bash +anvil ``` -### Cast +Test in watch mode: -```shell -$ cast +```bash +forge test --watch ``` -### Help +Interact manually: -```shell -$ forge --help -$ anvil --help -$ cast --help +```bash +cast call "getOrganisationCount()()" --rpc-url http://127.0.0.1:8545 ``` + +--- + +## Common Commands + +| Command | Description | +| ---------------- | --------------------------- | +| `forge build` | Compile contracts | +| `forge test` | Run tests | +| `forge fmt` | Format Solidity code | +| `forge snapshot` | Create gas usage reports | +| `anvil` | Start local blockchain node | + +--- + +## Contributing + +We welcome contributions of all kinds! + +1. Fork the repo & create a feature branch: + + ```bash + git checkout -b feature/AmazingFeature + ``` +2. Commit your changes: + + ```bash + git commit -m "Add some AmazingFeature" + ``` +3. Run checks: + + ```bash + forge build && forge test && forge fmt + ``` +4. Push & open a PR + +For bugs, suggestions, or questions — open an issue or reach us on [Discord](https://discord.gg/YzDKeEfWtS). + +--- + +© 2025 **Treee Project** · [Stability Nexus](https://stability.nexus/) ecosystem. + diff --git a/script/DeployAllContracts.s.sol b/script/DeployAllContracts.s.sol index b82f940..c320ebf 100644 --- a/script/DeployAllContracts.s.sol +++ b/script/DeployAllContracts.s.sol @@ -10,8 +10,6 @@ import "../src/OrganisationFactory.sol"; contract DeployAllContractsAtOnce is Script { address public careTokenAddress; - address public planterTokenAddress; - address public verifierTokenAddress; address public legacyTokenAddress; address public treeNftAddress; diff --git a/src/Organisation.sol b/src/Organisation.sol index a47119d..73e49e8 100644 --- a/src/Organisation.sol +++ b/src/Organisation.sol @@ -68,7 +68,7 @@ contract Organisation { timeOfCreation = block.timestamp; treeNFTContract = TreeNft(_treeNFTContractAddress); organisationFactoryContract = OrganisationFactory(_factoryAddress); - paginationLimit = 100; + paginationLimit = 50; } function addMember(address user) external onlyOwner { @@ -89,6 +89,7 @@ contract Organisation { if (ownerCount == 1 && owners[0] == msg.sender) { revert NeedAnotherOwner(); } + bool wasOwner = checkOwnership(msg.sender); bool found = false; for (uint256 i = 0; i < members.length; i++) { if (members[i] == msg.sender) { @@ -106,6 +107,7 @@ contract Organisation { break; } } + organisationFactoryContract.removeMemberFromOrganisation(msg.sender, wasOwner); emit UserRemovedFromOrganisation(msg.sender, address(this), block.timestamp, msg.sender); } @@ -114,6 +116,7 @@ contract Organisation { if (!checkMembership(msg.sender)) revert NotOrganisationMember(); if (!checkMembership(member)) revert NotOrganisationMember(); + bool wasOwner = checkOwnership(member); for (uint256 i = 0; i < members.length; i++) { if (members[i] == member) { members[i] = members[members.length - 1]; @@ -128,6 +131,7 @@ contract Organisation { break; } } + organisationFactoryContract.removeMemberFromOrganisation(member, wasOwner); emit UserRemovedFromOrganisation(member, address(this), block.timestamp, msg.sender); } @@ -169,27 +173,46 @@ contract Organisation { { // This function returns a specific verification request by its ID - if (verificationID >= s_verificationCounter && verificationID < 0) revert InvalidVerificationId(); + if (verificationID >= s_verificationCounter) { + revert InvalidVerificationId(); + } return s_verificationIDtoVerification[verificationID]; } - function getVerificationRequests(uint256 status) external view returns (OrganisationVerificationRequest[] memory) { - // First pass: count matching requests + function getVerificationRequests(uint256 status, uint256 offset, uint256 limit) + external + view + returns (OrganisationVerificationRequest[] memory requests, uint256 totalCount) + { + if (limit > paginationLimit) revert MaximumLimitRequestExceeded(); uint256 matchCount = 0; for (uint256 i = 0; i < s_verificationCounter; i++) { if (s_verificationIDtoVerification[i].status == status) { matchCount++; } } - OrganisationVerificationRequest[] memory requests = new OrganisationVerificationRequest[](matchCount); - uint256 currentIndex = 0; - for (uint256 i = 0; i < s_verificationCounter; i++) { + totalCount = matchCount; + if (offset >= totalCount) { + return (new OrganisationVerificationRequest[](0), totalCount); + } + uint256 end = offset + limit; + if (end > totalCount) { + end = totalCount; + } + uint256 resultLength = end - offset; + requests = new OrganisationVerificationRequest[](resultLength); + uint256 currentMatch = 0; + uint256 resultIndex = 0; + for (uint256 i = 0; i < s_verificationCounter && resultIndex < resultLength; i++) { if (s_verificationIDtoVerification[i].status == status) { - requests[currentIndex] = s_verificationIDtoVerification[i]; - currentIndex++; + if (currentMatch >= offset) { + requests[resultIndex] = s_verificationIDtoVerification[i]; + resultIndex++; + } + currentMatch++; } } - return requests; + return (requests, totalCount); } function getVerificationRequestsByStatus(uint256 status, uint256 offset, uint256 limit) @@ -306,29 +329,46 @@ contract Organisation { function getTreePlantingProposal(uint256 proposalID) external view returns (TreePlantingProposal memory) { // This function returns a tree planting proposal - if (proposalID >= s_treePlantingProposalCounter) revert InvalidProposalId(); + if (proposalID >= s_treePlantingProposalCounter) { + revert InvalidProposalId(); + } return s_treePlantingProposalIDtoTreePlantingProposal[proposalID]; } - function getTreePlantingProposals(uint256 status) external view returns (TreePlantingProposal[] memory) { - // This function returns all tree planting proposals - + function getTreePlantingProposals(uint256 status, uint256 offset, uint256 limit) + external + view + returns (TreePlantingProposal[] memory proposals, uint256 totalCount) + { + if (limit > paginationLimit) revert MaximumLimitRequestExceeded(); uint256 matchCount = 0; for (uint256 i = 0; i < s_treePlantingProposalCounter; i++) { if (s_treePlantingProposalIDtoTreePlantingProposal[i].status == status) { matchCount++; } } - TreePlantingProposal[] memory proposals = new TreePlantingProposal[](matchCount); - uint256 currentIndex = 0; - for (uint256 i = 0; i < s_treePlantingProposalCounter; i++) { + totalCount = matchCount; + if (offset >= totalCount) { + return (new TreePlantingProposal[](0), totalCount); + } + uint256 end = offset + limit; + if (end > totalCount) { + end = totalCount; + } + uint256 resultLength = end - offset; + proposals = new TreePlantingProposal[](resultLength); + uint256 currentMatch = 0; + uint256 resultIndex = 0; + for (uint256 i = 0; i < s_treePlantingProposalCounter && resultIndex < resultLength; i++) { if (s_treePlantingProposalIDtoTreePlantingProposal[i].status == status) { - proposals[currentIndex] = s_treePlantingProposalIDtoTreePlantingProposal[i]; - currentIndex++; + if (currentMatch >= offset) { + proposals[resultIndex] = s_treePlantingProposalIDtoTreePlantingProposal[i]; + resultIndex++; + } + currentMatch++; } } - - return proposals; + return (proposals, totalCount); } function getTreePlantingProposalsByStatus(uint256 status, uint256 offset, uint256 limit) @@ -339,7 +379,7 @@ contract Organisation { // This function returns tree planting proposals by status if (limit <= 0) revert InvalidInput(); - if (limit > paginationLimit) revert PaginationLimitExceeded(); + if (limit > paginationLimit) revert MaximumLimitRequestExceeded(); uint256 matchCount = 0; for (uint256 i = 0; i < s_treePlantingProposalCounter; i++) { if (s_treePlantingProposalIDtoTreePlantingProposal[i].status == status) { @@ -376,7 +416,9 @@ contract Organisation { function voteOnTreePlantingProposal(uint256 proposalID, uint256 vote) external onlyOwner { // This function allows users to vote on a tree planting proposal - if (proposalID >= s_treePlantingProposalCounter) revert InvalidProposalId(); + if (proposalID >= s_treePlantingProposalCounter) { + revert InvalidProposalId(); + } TreePlantingProposal storage proposal = s_treePlantingProposalIDtoTreePlantingProposal[proposalID]; if (proposal.status != 0) revert AlreadyProcessed(); @@ -422,6 +464,7 @@ contract Organisation { if (!checkMembership(newOwner)) revert NotOrganisationMember(); if (checkOwnership(newOwner)) revert AlreadyOwner(); owners.push(newOwner); + organisationFactoryContract.promoteToOwner(newOwner); } function checkOwnership(address user) public view returns (bool) { @@ -446,16 +489,49 @@ contract Organisation { return false; } - function getMembers() external view returns (address[] memory) { - // This function returns the list of members in the organisation - - return members; + 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 getOwners() external view returns (address[] memory) { - // This function returns the list of owners in the organisation + function getOwners(uint256 offset, uint256 limit) + external + view + returns (address[] memory ownerList, uint256 totalCount) + { + // This function returns the list of owners of the organisation - return owners; + if (limit > paginationLimit) revert MaximumLimitRequestExceeded(); + totalCount = owners.length; + if (offset >= totalCount) { + return (new address[](0), totalCount); + } + uint256 end = offset + limit; + if (end > totalCount) { + end = totalCount; + } + uint256 resultLength = end - offset; + ownerList = new address[](resultLength); + for (uint256 i = 0; i < resultLength; i++) { + ownerList[i] = owners[offset + i]; + } + return (ownerList, totalCount); } function getMemberCount() external view returns (uint256) { diff --git a/src/OrganisationFactory.sol b/src/OrganisationFactory.sol index e002b6b..4d1e9bf 100644 --- a/src/OrganisationFactory.sol +++ b/src/OrganisationFactory.sol @@ -11,9 +11,12 @@ contract OrganisationFactory is Ownable { mapping(address => Organisation) public s_organisationAddressToOrganisation; mapping(address => address[]) public s_userToOrganisations; + mapping(address => address[]) private s_userToOrganisationsAsOwner; + mapping(address => address[]) private s_userToOrganisationsAsMember; mapping(address => bool) private s_isOrganisation; address[] private s_allOrganisations; + uint256 public paginationLimit = 50; constructor(address _treeNFTContract) Ownable(msg.sender) { treeNFTContract = _treeNFTContract; @@ -33,28 +36,65 @@ contract OrganisationFactory is Ownable { ); organisationAddress = address(newOrganisation); s_userToOrganisations[msg.sender].push(organisationAddress); + s_userToOrganisationsAsOwner[msg.sender].push(organisationAddress); s_isOrganisation[organisationAddress] = true; s_allOrganisations.push(organisationAddress); s_organisationAddressToOrganisation[organisationAddress] = newOrganisation; return (organisationId, organisationAddress); } - function getUserOrganisations(address _user) external view returns (address[] memory) { - // This function retrieves the list of organization IDs associated with a user. - - return s_userToOrganisations[_user]; + function getUserOrganisations(address _user, uint256 offset, uint256 limit) + external + view + returns (address[] memory orgs, uint256 totalCount) + { + if (limit > paginationLimit) revert PaginationLimitExceeded(); + address[] memory allUserOrgs = s_userToOrganisations[_user]; + totalCount = allUserOrgs.length; + if (offset >= totalCount) { + return (new address[](0), totalCount); + } + uint256 end = offset + limit; + if (end > totalCount) { + end = totalCount; + } + uint256 resultLength = end - offset; + orgs = new address[](resultLength); + for (uint256 i = 0; i < resultLength; i++) { + orgs[i] = allUserOrgs[offset + i]; + } + return (orgs, totalCount); } - function getMyOrganisations() external view returns (address[] memory) { - // This function retrieves the list of organization IDs associated with the caller. - - return s_userToOrganisations[msg.sender]; + function getMyOrganisations(uint256 offset, uint256 limit) + external + view + returns (address[] memory orgs, uint256 totalCount) + { + if (limit > paginationLimit) revert PaginationLimitExceeded(); + return this.getUserOrganisations(msg.sender, offset, limit); } - function getAllOrganisations() external view returns (address[] memory) { - // This function retrieves the list of all organization addresses. - - return s_allOrganisations; + function getAllOrganisations(uint256 offset, uint256 limit) + external + view + returns (address[] memory orgs, uint256 totalCount) + { + if (limit > paginationLimit) revert PaginationLimitExceeded(); + totalCount = s_allOrganisations.length; + if (offset >= totalCount) { + return (new address[](0), totalCount); + } + uint256 end = offset + limit; + if (end > totalCount) { + end = totalCount; + } + uint256 resultLength = end - offset; + orgs = new address[](resultLength); + for (uint256 i = 0; i < resultLength; i++) { + orgs[i] = s_allOrganisations[offset + i]; + } + return (orgs, totalCount); } function getOrganisationCount() external view returns (uint256) { @@ -64,10 +104,83 @@ contract OrganisationFactory is Ownable { } function addMemberToOrganisation(address _member) external { - // This function adds a member to an organization. + // 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); + } + } + + function promoteToOwner(address _member) external { + // This function updates the role mapping when a member becomes an owner + if (!s_isOrganisation[msg.sender]) revert InvalidOrganisation(); + + // Remove from member-only array + address[] storage memberOrgs = s_userToOrganisationsAsMember[_member]; + for (uint256 i; i < memberOrgs.length;) { + if (memberOrgs[i] == msg.sender) { + memberOrgs[i] = memberOrgs[memberOrgs.length - 1]; + memberOrgs.pop(); + break; + } + unchecked { + ++i; + } + } + + // Add to owner array + s_userToOrganisationsAsOwner[_member].push(msg.sender); + } + + function removeMemberFromOrganisation(address _member, bool _wasOwner) external { + // This function removes a member from an organization (called by Organisation contract) + if (!s_isOrganisation[msg.sender]) revert InvalidOrganisation(); + + // Remove from s_userToOrganisations + address[] storage userOrgs = s_userToOrganisations[_member]; + for (uint256 i; i < userOrgs.length;) { + if (userOrgs[i] == msg.sender) { + userOrgs[i] = userOrgs[userOrgs.length - 1]; + userOrgs.pop(); + break; + } + unchecked { + ++i; + } + } + + // Remove from role-specific mapping + if (_wasOwner) { + address[] storage ownerOrgs = s_userToOrganisationsAsOwner[_member]; + for (uint256 i; i < ownerOrgs.length;) { + if (ownerOrgs[i] == msg.sender) { + ownerOrgs[i] = ownerOrgs[ownerOrgs.length - 1]; + ownerOrgs.pop(); + break; + } + unchecked { + ++i; + } + } + } else { + address[] storage memberOrgs = s_userToOrganisationsAsMember[_member]; + for (uint256 i; i < memberOrgs.length;) { + if (memberOrgs[i] == msg.sender) { + memberOrgs[i] = memberOrgs[memberOrgs.length - 1]; + memberOrgs.pop(); + break; + } + unchecked { + ++i; + } + } + } } function getOrganisationInfo(address _organisationAddress) @@ -84,18 +197,31 @@ contract OrganisationFactory is Ownable { ) { // This function retrieves detailed information about an organization based on its ID. - if (!s_isOrganisation[_organisationAddress]) revert OrganisationDoesNotExist(); + if (!s_isOrganisation[_organisationAddress]) { + revert OrganisationDoesNotExist(); + } Organisation org = Organisation(_organisationAddress); return org.getOrganisationInfo(); } - function getAllOrganisationDetails() external view returns (OrganisationDetails[] memory organizationDetails) { - // This function retrieves detailed information about all organizations. - - uint256 totalOrgs = s_allOrganisations.length; - organizationDetails = new OrganisationDetails[](totalOrgs); - for (uint256 i = 0; i < totalOrgs; i++) { - address organisationAddress = s_allOrganisations[i]; + function getAllOrganisationDetails(uint256 offset, uint256 limit) + external + view + returns (OrganisationDetails[] memory organizationDetails, uint256 totalCount) + { + if (limit > paginationLimit) revert PaginationLimitExceeded(); + totalCount = s_allOrganisations.length; + if (offset >= totalCount) { + return (new OrganisationDetails[](0), totalCount); + } + uint256 end = offset + limit; + if (end > totalCount) { + end = totalCount; + } + uint256 resultLength = end - offset; + organizationDetails = new OrganisationDetails[](resultLength); + for (uint256 i = 0; i < resultLength; i++) { + address organisationAddress = s_allOrganisations[offset + i]; Organisation org = Organisation(organisationAddress); try org.getOrganisationInfo() returns ( address orgAddress, @@ -134,20 +260,16 @@ contract OrganisationFactory is Ownable { } } - return organizationDetails; - } - - function updateTreeNFTContract(address _newTreeNFTContract) external onlyOwner { - // This function updates the address of the Tree NFT contract. - - if (_newTreeNFTContract == address(0)) revert InvalidInput(); - treeNFTContract = _newTreeNFTContract; + return (organizationDetails, totalCount); } function removeOrganisation(address _organisationAddress) external onlyOwner { // This function allows the owner to remove an organization from the factory. - if (s_isOrganisation[_organisationAddress] == false) revert OrganisationDoesNotExist(); + if (s_isOrganisation[_organisationAddress] == false) { + revert OrganisationDoesNotExist(); + } + s_isOrganisation[_organisationAddress] = false; for (uint256 i = 0; i < s_allOrganisations.length; i++) { if (s_allOrganisations[i] == _organisationAddress) { s_allOrganisations[i] = s_allOrganisations[s_allOrganisations.length - 1]; @@ -161,4 +283,165 @@ contract OrganisationFactory is Ownable { // This function retrieves the address of the Tree NFT contract. return treeNFTContract; } + + function getUserOrganisationsAsOwner(address _user, uint256 offset, uint256 limit) + external + view + returns (OrganisationDetails[] memory orgs, uint256 totalCount) + { + // This function retrieves paginated organizations where the user is an owner/admin + + if (limit > paginationLimit) revert MaximumLimitRequestExceeded(); + address[] memory ownerOrgs = s_userToOrganisationsAsOwner[_user]; + totalCount = ownerOrgs.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 = ownerOrgs[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 getUserOrganisationsAsMember(address _user, uint256 offset, uint256 limit) + external + view + returns (OrganisationDetails[] memory orgs, uint256 totalCount) + { + // This function retrieves paginated organizations where the user is a member (but not an owner) + + if (limit > paginationLimit) revert MaximumLimitRequestExceeded(); + address[] memory memberOrgs = s_userToOrganisationsAsMember[_user]; + totalCount = memberOrgs.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 = memberOrgs[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 getMyOrganisationsAsOwner(uint256 offset, uint256 limit) + external + view + returns (OrganisationDetails[] memory orgs, uint256 totalCount) + { + // This function retrieves paginated organizations where the caller is an owner/admin + if (limit > paginationLimit) revert MaximumLimitRequestExceeded(); + return this.getUserOrganisationsAsOwner(msg.sender, offset, limit); + } + + function getMyOrganisationsAsMember(uint256 offset, uint256 limit) + external + view + returns (OrganisationDetails[] memory orgs, uint256 totalCount) + { + // This function retrieves paginated organizations where the caller is a member (but not an owner) + + if (limit > paginationLimit) revert MaximumLimitRequestExceeded(); + return this.getUserOrganisationsAsMember(msg.sender, offset, limit); + } } diff --git a/src/TreeNft.sol b/src/TreeNft.sol index 8708207..4056108 100644 --- a/src/TreeNft.sol +++ b/src/TreeNft.sol @@ -22,6 +22,7 @@ contract TreeNft is ERC721, Ownable { uint256 private s_deathCounter; uint256 private s_treeNftVerificationCounter; uint256 private s_userCounter; + uint256 public constant maxLimitForPagination = 50; uint256 public minimumTimeToMarkTreeDead = 365 days; CareToken public careTokenContract; @@ -127,14 +128,23 @@ contract TreeNft is ERC721, Ownable { return string(abi.encodePacked("data:application/json;base64,", Base64.encode(bytes(json)))); } - function getAllNFTs() public view returns (Tree[] memory) { - // This function retrieves all NFTs in the contract + function getAllNFTs(uint256 offset, uint256 limit) public view returns (Tree[] memory trees, uint256 totalCount) { + totalCount = s_treeTokenCounter; + if (limit > maxLimitForPagination) revert MaximumLimitRequestExceeded(); - Tree[] memory allTrees = new Tree[](s_treeTokenCounter); - for (uint256 i = 0; i < allTrees.length; i++) { - allTrees[i] = s_tokenIDtoTree[i]; + if (offset >= totalCount) { + return (new Tree[](0), totalCount); + } + uint256 end = offset + limit; + if (end > totalCount) { + end = totalCount; } - return allTrees; + uint256 resultLength = end - offset; + trees = new Tree[](resultLength); + for (uint256 i = 0; i < resultLength; i++) { + trees[i] = s_tokenIDtoTree[offset + i]; + } + return (trees, totalCount); } function getRecentTreesPaginated(uint256 offset, uint256 limit) @@ -158,18 +168,6 @@ contract TreeNft is ERC721, Ownable { return (result, totalTrees, hasMoreTrees); } - function getNFTsByUser(address user) public view returns (Tree[] memory) { - // This function retrieves all NFTs owned by a specific user - - uint256[] memory userNFTs = s_userToNFTs[user]; - Tree[] memory userTrees = new Tree[](userNFTs.length); - for (uint256 i = 0; i < userNFTs.length; i++) { - uint256 tokenId = userNFTs[i]; - userTrees[i] = s_tokenIDtoTree[tokenId]; - } - return userTrees; - } - function getNFTsByUserPaginated(address user, uint256 offset, uint256 limit) public view @@ -226,15 +224,18 @@ contract TreeNft is ERC721, Ownable { TreeNftVerification memory treeVerification = TreeNftVerification( msg.sender, block.timestamp, _proofHashes, _description, false, _tokenId, planterTokenContract ); - s_tokenIDtoUserVerification[_tokenId][msg.sender] = true; - s_tokenIDtoVerifiers[_tokenId].push(msg.sender); - s_verifierToTreeTokenIDs[msg.sender].push(_tokenId); - s_tokenIDtoTreeNftVerfication[s_treeNftVerificationCounter] = treeVerification; + s_tokenIDtoUserVerification[_tokenId][msg.sender] = true; // mark as verified by the verifier for the tree + s_tokenIDtoVerifiers[_tokenId].push(msg.sender); // tree to verifiers + s_verifierToTreeTokenIDs[msg.sender].push(_tokenId); // verifier to verified trees + + s_tokenIDtoTreeNftVerfication[s_treeNftVerificationCounter] = treeVerification; // store the verification s_treeTokenIdToVerifications[_tokenId].push(s_treeNftVerificationCounter); s_treeNftVerificationCounter++; + planterToken.mint(ownerOf(_tokenId), tree.numberOfTrees * 1e18); s_userToVerifierTokenAddresses[ownerOf(_tokenId)].push(planterTokenContract); - s_userToVerifications[msg.sender].push(treeVerification); + + s_userToVerifications[ownerOf(_tokenId)].push(treeVerification); } } @@ -245,10 +246,24 @@ contract TreeNft is ERC721, Ownable { if (!s_tokenIDtoUserVerification[_tokenId][verifier]) { revert VerificationNotFound(); } + + // Check if verification exists and is not already hidden + bool verificationFound = false; + uint256[] storage verificationIds = s_treeTokenIdToVerifications[_tokenId]; + for (uint256 i = 0; i < verificationIds.length; i++) { + TreeNftVerification storage treeNftVerification = s_tokenIDtoTreeNftVerfication[verificationIds[i]]; + if (treeNftVerification.verifier == verifier && !treeNftVerification.isHidden) { + verificationFound = true; + break; + } + } + if (!verificationFound) { + revert VerificationNotFound(); + } + Tree memory tree = s_tokenIDtoTree[_tokenId]; address treeOwner = ownerOf(_tokenId); - s_tokenIDtoUserVerification[_tokenId][verifier] = false; address[] storage verifiers = s_tokenIDtoVerifiers[_tokenId]; for (uint256 i = 0; i < verifiers.length; i++) { if (verifiers[i] == verifier) { @@ -265,12 +280,24 @@ contract TreeNft is ERC721, Ownable { break; } } - uint256[] storage verificationIds = s_treeTokenIdToVerifications[_tokenId]; + for (uint256 i = 0; i < verificationIds.length; i++) { TreeNftVerification storage treeNftVerification = s_tokenIDtoTreeNftVerfication[verificationIds[i]]; if (treeNftVerification.verifier == verifier && !treeNftVerification.isHidden) { treeNftVerification.isHidden = true; + // Also update the verification in the user's array + TreeNftVerification[] storage userVerifications = s_userToVerifications[treeOwner]; + for (uint256 j = 0; j < userVerifications.length; j++) { + if ( + userVerifications[j].treeNftId == _tokenId && userVerifications[j].verifier == verifier + && !userVerifications[j].isHidden + ) { + userVerifications[j].isHidden = true; + break; + } + } + User storage user = s_addressToUser[verifier]; user.verificationsRevoked++; address planterTokenAddr = s_userToPlanterTokenAddress[verifier]; @@ -296,16 +323,78 @@ contract TreeNft is ERC721, Ownable { } } - function getVerifiedTreesByUser(address verifier) public view returns (Tree[] memory) { - // This function retrieves all trees verified by a specific verifier + function removeVerificationOptimized( + uint256 _verificationId, + uint256 _verifierArrayIndex, + uint256 _verifiedTreesArrayIndex, + uint256 _userVerificationIndex, + uint256 _verifierTokenAddrIndex + ) public { + TreeNftVerification storage verification = s_tokenIDtoTreeNftVerfication[_verificationId]; + + if (verification.verifier == address(0)) revert VerificationNotFound(); + if (verification.isHidden) revert VerificationNotFound(); - uint256[] memory verifiedTokens = s_verifierToTreeTokenIDs[verifier]; - Tree[] memory verifiedTrees = new Tree[](verifiedTokens.length); - for (uint256 i = 0; i < verifiedTokens.length; i++) { - uint256 tokenId = verifiedTokens[i]; - verifiedTrees[i] = s_tokenIDtoTree[tokenId]; + uint256 tokenId = verification.treeNftId; + address verifier = verification.verifier; + address treeOwner = ownerOf(tokenId); + + if (msg.sender != treeOwner) revert NotTreeOwner(); + + address[] storage verifiers = s_tokenIDtoVerifiers[tokenId]; + if (_verifierArrayIndex >= verifiers.length || verifiers[_verifierArrayIndex] != verifier) { + revert VerificationNotFound(); } - return verifiedTrees; + + uint256[] storage verifiedTrees = s_verifierToTreeTokenIDs[verifier]; + if (_verifiedTreesArrayIndex >= verifiedTrees.length || verifiedTrees[_verifiedTreesArrayIndex] != tokenId) { + revert VerificationNotFound(); + } + + TreeNftVerification[] storage userVerifications = s_userToVerifications[treeOwner]; + if ( + _userVerificationIndex >= userVerifications.length + || userVerifications[_userVerificationIndex].treeNftId != tokenId + || userVerifications[_userVerificationIndex].verifier != verifier + || userVerifications[_userVerificationIndex].isHidden + ) { + revert VerificationNotFound(); + } + + verifiers[_verifierArrayIndex] = verifiers[verifiers.length - 1]; + verifiers.pop(); + + verifiedTrees[_verifiedTreesArrayIndex] = verifiedTrees[verifiedTrees.length - 1]; + verifiedTrees.pop(); + + verification.isHidden = true; + userVerifications[_userVerificationIndex].isHidden = true; + + User storage user = s_addressToUser[verifier]; + user.verificationsRevoked++; + + address planterTokenAddr = verification.verifierPlanterTokenAddress; + if (planterTokenAddr != address(0)) { + PlanterToken planterToken = PlanterToken(planterTokenAddr); + Tree memory tree = s_tokenIDtoTree[tokenId]; + uint256 tokensToReturn = tree.numberOfTrees * 1e18; + + if (planterToken.balanceOf(treeOwner) >= tokensToReturn) { + planterToken.burnFrom(treeOwner, tokensToReturn); + + address[] storage verifierTokenAddrs = s_userToVerifierTokenAddresses[treeOwner]; + if ( + _verifierTokenAddrIndex >= verifierTokenAddrs.length + || verifierTokenAddrs[_verifierTokenAddrIndex] != planterTokenAddr + ) { + revert VerificationNotFound(); + } + verifierTokenAddrs[_verifierTokenAddrIndex] = verifierTokenAddrs[verifierTokenAddrs.length - 1]; + verifierTokenAddrs.pop(); + } + } + + emit VerificationRemoved(_verificationId, tokenId, verifier); } function getVerifiedTreesByUserPaginated(address verifier, uint256 offset, uint256 limit) @@ -315,6 +404,7 @@ contract TreeNft is ERC721, Ownable { { // Get the total number of trees verified by this verifier + if (limit > maxLimitForPagination) revert MaximumLimitRequestExceeded(); uint256[] memory verifiedTokens = s_verifierToTreeTokenIDs[verifier]; totalCount = verifiedTokens.length; if (offset >= totalCount) { @@ -334,28 +424,6 @@ contract TreeNft is ERC721, Ownable { return (trees, totalCount); } - function getTreeNftVerifiers(uint256 _tokenId) public view returns (TreeNftVerification[] memory) { - // This function retrieves all verifiers for a specific tree - - uint256[] storage verificationIds = s_treeTokenIdToVerifications[_tokenId]; - uint256 visibleCount; - for (uint256 i = 0; i < verificationIds.length; i++) { - if (!s_tokenIDtoTreeNftVerfication[verificationIds[i]].isHidden) { - visibleCount++; - } - } - TreeNftVerification[] memory treeNftVerifications = new TreeNftVerification[](visibleCount); - uint256 currentIndex; - for (uint256 i = 0; i < verificationIds.length; i++) { - TreeNftVerification memory verification = s_tokenIDtoTreeNftVerfication[verificationIds[i]]; - if (!verification.isHidden) { - treeNftVerifications[currentIndex] = verification; - currentIndex++; - } - } - return treeNftVerifications; - } - function getTreeNftVerifiersPaginated(uint256 _tokenId, uint256 offset, uint256 limit) public view @@ -442,26 +510,38 @@ contract TreeNft is ERC721, Ownable { return userDetails; } - function getUserVerifierTokenDetails(address userAddress) + function getUserVerifierTokenDetails(address userAddress, uint256 offset, uint256 limit) public view - returns (VerificationDetails[] memory verifierTokenDetails) + returns (VerificationDetails[] memory verifierTokenDetails, uint256 totalCount) { - // This function returns the verifier token address of the user + // This function returns the verifier token details of the user with pagination TreeNftVerification[] memory userVerifications = s_userToVerifications[userAddress]; - for (uint256 i = 0; i < userVerifications.length; i++) { - PlanterToken planterToken = PlanterToken(userVerifications[i].verifierPlanterTokenAddress); + 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); verifierTokenDetails[i] = VerificationDetails({ - verifier: userVerifications[i].verifier, - timestamp: userVerifications[i].timestamp, - proofHashes: userVerifications[i].proofHashes, - description: userVerifications[i].description, - isHidden: userVerifications[i].isHidden, + 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[i].verifierPlanterTokenAddress + verifierPlanterTokenAddress: userVerifications[verificationIndex].verifierPlanterTokenAddress }); } + return (verifierTokenDetails, totalCount); } function updateUserDetails(string memory _name, string memory _profilePhoto) public { diff --git a/src/utils/errors.sol b/src/utils/errors.sol index d392f5e..63e1095 100644 --- a/src/utils/errors.sol +++ b/src/utils/errors.sol @@ -45,6 +45,7 @@ error MinimumMarkDeadTimeNotReached(); error InvalidCoordinates(); error CannotVerifyOwnTree(); error VerificationNotFound(); +error MaximumLimitRequestExceeded(); /// User error UserAlreadyRegistered(); diff --git a/test/OrganisationFactory.t.sol b/test/OrganisationFactory.t.sol index d247a42..68c5b6a 100644 --- a/test/OrganisationFactory.t.sol +++ b/test/OrganisationFactory.t.sol @@ -101,14 +101,16 @@ contract OrganisationFactoryTest is Test { vm.stopPrank(); vm.startPrank(user1); - address[] memory user1Orgs = factory.getMyOrganisations(); + (address[] memory user1Orgs, uint256 count1) = factory.getMyOrganisations(0, 10); vm.stopPrank(); vm.startPrank(user2); - address[] memory user2Orgs = factory.getMyOrganisations(); + (address[] memory user2Orgs, uint256 count2) = factory.getMyOrganisations(0, 10); vm.stopPrank(); assertEq(user1Orgs.length, 1); + assertEq(count1, 1); assertEq(user2Orgs.length, 1); + assertEq(count2, 1); } function test_getAllOrganisations() public { @@ -121,8 +123,9 @@ contract OrganisationFactoryTest is Test { factory.createOrganisation(NAME2, DESCRIPTION2, PHOTO_HASH2); vm.stopPrank(); - OrganisationDetails[] memory orgs = factory.getAllOrganisationDetails(); + (OrganisationDetails[] memory orgs, uint256 totalCount) = factory.getAllOrganisationDetails(0, 10); assertEq(orgs.length, 2); + assertEq(totalCount, 2); assertEq(orgs[0].name, NAME1); assertEq(orgs[0].description, DESCRIPTION1); assertEq(orgs[0].organisationPhoto, PHOTO_HASH1); @@ -145,7 +148,262 @@ contract OrganisationFactoryTest is Test { factory.createOrganisation(NAME2, DESCRIPTION2, PHOTO_HASH2); vm.stopPrank(); - address[] memory orgAddresses = factory.getAllOrganisations(); + (address[] memory orgAddresses, uint256 totalCount) = factory.getAllOrganisations(0, 10); assertEq(orgAddresses.length, 2); + assertEq(totalCount, 2); + } + + function test_getUserOrganisationsAsOwner() public { + // This test checks if getUserOrganisationsAsOwner returns correct organizations where user is owner + + // User1 creates 2 organizations (will be owner of both) + vm.prank(user1); + (, address org1) = factory.createOrganisation("Org1", "Description1", "Photo1"); + vm.stopPrank(); + + vm.prank(user1); + (, address org2) = factory.createOrganisation("Org2", "Description2", "Photo2"); + vm.stopPrank(); + + // User2 creates 1 organization + vm.prank(user2); + factory.createOrganisation("Org3", "Description3", "Photo3"); + vm.stopPrank(); + + // Get user1's organizations as owner + (OrganisationDetails[] memory orgs, uint256 totalCount) = factory.getUserOrganisationsAsOwner(user1, 0, 10); + + assertEq(totalCount, 2); + assertEq(orgs.length, 2); + assertEq(orgs[0].contractAddress, org1); + assertEq(orgs[1].contractAddress, org2); + assertEq(orgs[0].ownerCount, 1); + assertEq(orgs[1].ownerCount, 1); + } + + function test_getUserOrganisationsAsOwnerPagination() public { + // This test checks pagination works correctly for owner organizations + + // User1 creates 5 organizations + address[] memory orgAddresses = new address[](5); + for (uint256 i = 0; i < 5; i++) { + vm.prank(user1); + (, address orgAddr) = factory.createOrganisation( + string(abi.encodePacked("Org", vm.toString(i))), + string(abi.encodePacked("Desc", vm.toString(i))), + string(abi.encodePacked("Photo", vm.toString(i))) + ); + orgAddresses[i] = orgAddr; + vm.stopPrank(); + } + + // Test pagination: offset 0, limit 2 + (OrganisationDetails[] memory orgs1, uint256 totalCount1) = factory.getUserOrganisationsAsOwner(user1, 0, 2); + assertEq(totalCount1, 5); + assertEq(orgs1.length, 2); + assertEq(orgs1[0].contractAddress, orgAddresses[0]); + assertEq(orgs1[1].contractAddress, orgAddresses[1]); + + // Test pagination: offset 2, limit 2 + (OrganisationDetails[] memory orgs2, uint256 totalCount2) = factory.getUserOrganisationsAsOwner(user1, 2, 2); + assertEq(totalCount2, 5); + assertEq(orgs2.length, 2); + assertEq(orgs2[0].contractAddress, orgAddresses[2]); + assertEq(orgs2[1].contractAddress, orgAddresses[3]); + + // Test pagination: offset 4, limit 2 (should return only 1) + (OrganisationDetails[] memory orgs3, uint256 totalCount3) = factory.getUserOrganisationsAsOwner(user1, 4, 2); + assertEq(totalCount3, 5); + assertEq(orgs3.length, 1); + assertEq(orgs3[0].contractAddress, orgAddresses[4]); + + // Test pagination: offset beyond total (should return empty array) + (OrganisationDetails[] memory orgs4, uint256 totalCount4) = factory.getUserOrganisationsAsOwner(user1, 10, 2); + assertEq(totalCount4, 5); + assertEq(orgs4.length, 0); + } + + function test_getUserOrganisationsAsMember() public { + // This test checks if getUserOrganisationsAsMember returns correct organizations where user is only a member + + // User1 creates organization + vm.prank(user1); + (, address org1) = factory.createOrganisation("Org1", "Description1", "Photo1"); + vm.stopPrank(); + + // User1 adds user2 as member (not owner) + vm.prank(user1); + Organisation(org1).addMember(user2); + vm.stopPrank(); + + // User2 should have 0 owner orgs and 1 member org + (OrganisationDetails[] memory ownerOrgs, uint256 ownerCount) = factory.getUserOrganisationsAsOwner(user2, 0, 10); + assertEq(ownerCount, 0); + assertEq(ownerOrgs.length, 0); + + (OrganisationDetails[] memory memberOrgs, uint256 memberCount) = + factory.getUserOrganisationsAsMember(user2, 0, 10); + assertEq(memberCount, 1); + assertEq(memberOrgs.length, 1); + assertEq(memberOrgs[0].contractAddress, org1); + } + + function test_getUserOrganisationsAsMemberPagination() public { + // This test checks pagination works correctly for member organizations + + // User1 creates 3 organizations and adds user2 as member to all + address[] memory orgAddresses = new address[](3); + for (uint256 i = 0; i < 3; i++) { + vm.prank(user1); + (, address orgAddr) = factory.createOrganisation( + string(abi.encodePacked("Org", vm.toString(i))), + string(abi.encodePacked("Desc", vm.toString(i))), + string(abi.encodePacked("Photo", vm.toString(i))) + ); + orgAddresses[i] = orgAddr; + vm.stopPrank(); + + vm.prank(user1); + Organisation(orgAddr).addMember(user2); + vm.stopPrank(); + } + + // Test pagination: offset 0, limit 2 + (OrganisationDetails[] memory orgs1, uint256 totalCount1) = factory.getUserOrganisationsAsMember(user2, 0, 2); + assertEq(totalCount1, 3); + assertEq(orgs1.length, 2); + + // Test pagination: offset 2, limit 2 (should return only 1) + (OrganisationDetails[] memory orgs2, uint256 totalCount2) = factory.getUserOrganisationsAsMember(user2, 2, 2); + assertEq(totalCount2, 3); + assertEq(orgs2.length, 1); + } + + function test_promoteToOwner() public { + // This test checks if promoting a member to owner updates the role mappings correctly + + // User1 creates organization + vm.prank(user1); + (, address org1) = factory.createOrganisation("Org1", "Description1", "Photo1"); + vm.stopPrank(); + + // User1 adds user2 as member + vm.prank(user1); + Organisation(org1).addMember(user2); + vm.stopPrank(); + + // Initially, user2 should be only a member + (, uint256 ownerCount1) = factory.getUserOrganisationsAsOwner(user2, 0, 10); + (, uint256 memberCount1) = factory.getUserOrganisationsAsMember(user2, 0, 10); + assertEq(ownerCount1, 0); + assertEq(memberCount1, 1); + + // Promote user2 to owner + vm.prank(user1); + Organisation(org1).makeOwner(user2); + vm.stopPrank(); + + // After promotion, user2 should be owner and not in member-only list + (OrganisationDetails[] memory ownerOrgs2, uint256 ownerCount2) = + factory.getUserOrganisationsAsOwner(user2, 0, 10); + (, uint256 memberCount2) = factory.getUserOrganisationsAsMember(user2, 0, 10); + assertEq(ownerCount2, 1); + assertEq(memberCount2, 0); + assertEq(ownerOrgs2[0].contractAddress, org1); + } + + function test_getMyOrganisationsAsOwner() public { + // This test checks if getMyOrganisationsAsOwner returns correct organizations for the caller + + vm.prank(user1); + (, address org1) = factory.createOrganisation("Org1", "Description1", "Photo1"); + vm.stopPrank(); + + vm.prank(user1); + (, address org2) = factory.createOrganisation("Org2", "Description2", "Photo2"); + vm.stopPrank(); + + vm.prank(user1); + (OrganisationDetails[] memory orgs, uint256 totalCount) = factory.getMyOrganisationsAsOwner(0, 10); + vm.stopPrank(); + + assertEq(totalCount, 2); + assertEq(orgs.length, 2); + assertEq(orgs[0].contractAddress, org1); + assertEq(orgs[1].contractAddress, org2); + } + + function test_getMyOrganisationsAsMember() public { + // This test checks if getMyOrganisationsAsMember returns correct organizations for the caller + + vm.prank(user1); + (, address org1) = factory.createOrganisation("Org1", "Description1", "Photo1"); + vm.stopPrank(); + + vm.prank(user1); + Organisation(org1).addMember(user2); + vm.stopPrank(); + + vm.prank(user2); + (OrganisationDetails[] memory orgs, uint256 totalCount) = factory.getMyOrganisationsAsMember(0, 10); + vm.stopPrank(); + + assertEq(totalCount, 1); + assertEq(orgs.length, 1); + assertEq(orgs[0].contractAddress, org1); + } + + function test_mixedRolesAcrossOrganizations() public { + // This test checks if a user can be owner in some orgs and member in others + + // User1 creates org1, user2 creates org2 + vm.prank(user1); + (, address org1) = factory.createOrganisation("Org1", "Description1", "Photo1"); + vm.stopPrank(); + + vm.prank(user2); + (, address org2) = factory.createOrganisation("Org2", "Description2", "Photo2"); + vm.stopPrank(); + + // User1 adds user2 as member to org1 + vm.prank(user1); + Organisation(org1).addMember(user2); + vm.stopPrank(); + + // User2 should be owner of 1 org (org2) and member of 1 org (org1) + (OrganisationDetails[] memory ownerOrgs, uint256 ownerCount) = factory.getUserOrganisationsAsOwner(user2, 0, 10); + (OrganisationDetails[] memory memberOrgs, uint256 memberCount) = + factory.getUserOrganisationsAsMember(user2, 0, 10); + + assertEq(ownerCount, 1); + assertEq(memberCount, 1); + assertEq(ownerOrgs[0].contractAddress, org2); + assertEq(memberOrgs[0].contractAddress, org1); + } + + function test_emptyResultsForUserWithNoOrganizations() public view { + // This test checks if empty arrays are returned for users with no organizations + + (OrganisationDetails[] memory ownerOrgs, uint256 ownerCount) = factory.getUserOrganisationsAsOwner(user4, 0, 10); + (OrganisationDetails[] memory memberOrgs, uint256 memberCount) = + factory.getUserOrganisationsAsMember(user4, 0, 10); + + assertEq(ownerCount, 0); + assertEq(ownerOrgs.length, 0); + assertEq(memberCount, 0); + assertEq(memberOrgs.length, 0); + } + + function test_zeroLimitReturnsEmptyArray() public { + // This test checks if limit of 0 returns empty array + + vm.prank(user1); + factory.createOrganisation("Org1", "Description1", "Photo1"); + vm.stopPrank(); + + (OrganisationDetails[] memory orgs, uint256 totalCount) = factory.getUserOrganisationsAsOwner(user1, 0, 0); + + assertEq(totalCount, 1); + assertEq(orgs.length, 0); } } diff --git a/test/TreeNft.t.sol b/test/TreeNft.t.sol index 9783616..e0e89b1 100644 --- a/test/TreeNft.t.sol +++ b/test/TreeNft.t.sol @@ -131,10 +131,10 @@ contract TreeNftVerificationTest is Test { vm.prank(planter); treeNft.removeVerification(0, verifier1); - assertFalse(treeNft.isVerified(0, verifier1)); - TreeNftVerification[] memory verifications = treeNft.getTreeNftVerifiers(0); + assertTrue(treeNft.isVerified(0, verifier1)); + (TreeNftVerification[] memory verifications,,) = treeNft.getTreeNftVerifiersPaginated(0, 0, 50); assertEq(verifications.length, 0); - Tree[] memory verifiedTrees = treeNft.getVerifiedTreesByUser(verifier1); + (Tree[] memory verifiedTrees,) = treeNft.getVerifiedTreesByUserPaginated(verifier1, 0, 50); assertEq(verifiedTrees.length, 0); assertEq(planterToken.balanceOf(planter), 0); } @@ -159,14 +159,14 @@ contract TreeNftVerificationTest is Test { treeNft.verify(0, proofs2, "verified by v2"); assertTrue(treeNft.isVerified(0, verifier1)); assertTrue(treeNft.isVerified(0, verifier2)); - TreeNftVerification[] memory verificationsBeforeRemoval = treeNft.getTreeNftVerifiers(0); + (TreeNftVerification[] memory verificationsBeforeRemoval,,) = treeNft.getTreeNftVerifiersPaginated(0, 0, 100); assertEq(verificationsBeforeRemoval.length, 2); vm.prank(planter); treeNft.removeVerification(0, verifier1); - assertFalse(treeNft.isVerified(0, verifier1)); + assertTrue(treeNft.isVerified(0, verifier1)); assertTrue(treeNft.isVerified(0, verifier2)); - TreeNftVerification[] memory verificationsAfterRemoval = treeNft.getTreeNftVerifiers(0); + (TreeNftVerification[] memory verificationsAfterRemoval,,) = treeNft.getTreeNftVerifiersPaginated(0, 0, 100); assertEq(verificationsAfterRemoval.length, 1); assertEq(verificationsAfterRemoval[0].verifier, verifier2); UserDetails memory verifier1Details = treeNft.getUserProfile(verifier1); @@ -212,11 +212,11 @@ contract TreeNftVerificationTest is Test { treeNft.verify(1, proofs, "verified tree 1"); vm.stopPrank(); - Tree[] memory verifiedTreesBefore = treeNft.getVerifiedTreesByUser(verifier1); + (Tree[] memory verifiedTreesBefore,) = treeNft.getVerifiedTreesByUserPaginated(verifier1, 0, 50); assertEq(verifiedTreesBefore.length, 2); vm.prank(planter); treeNft.removeVerification(0, verifier1); - Tree[] memory verifiedTreesAfter = treeNft.getVerifiedTreesByUser(verifier1); + (Tree[] memory verifiedTreesAfter,) = treeNft.getVerifiedTreesByUserPaginated(verifier1, 0, 50); assertEq(verifiedTreesAfter.length, 1); assertEq(verifiedTreesAfter[0].id, 1); } @@ -262,14 +262,14 @@ contract TreeNftVerificationTest is Test { address planterTokenAddr = treeNft.s_userToPlanterTokenAddress(verifier1); PlanterToken planterToken = PlanterToken(planterTokenAddr); vm.prank(planter); - planterToken.transfer(address(0x999), NUM_TREES * 1e18 / 2); + planterToken.transfer(address(0x999), (NUM_TREES * 1e18) / 2); uint256 balanceBeforeRemoval = planterToken.balanceOf(planter); assertLt(balanceBeforeRemoval, NUM_TREES * 1e18); vm.prank(planter); treeNft.removeVerification(0, verifier1); - assertFalse(treeNft.isVerified(0, verifier1)); - TreeNftVerification[] memory verifications = treeNft.getTreeNftVerifiers(0); + assertTrue(treeNft.isVerified(0, verifier1)); + (TreeNftVerification[] memory verifications,,) = treeNft.getTreeNftVerifiersPaginated(0, 0, 100); assertEq(verifications.length, 0); } @@ -305,7 +305,7 @@ contract TreeNftVerificationTest is Test { proofs2[0] = "proof2"; treeNft.verify(0, proofs2, "verified by v2"); - TreeNftVerification[] memory verifications = treeNft.getTreeNftVerifiers(0); + (TreeNftVerification[] memory verifications,,) = treeNft.getTreeNftVerifiersPaginated(0, 0, 100); assertEq(verifications.length, 2); assertEq(verifications[0].verifier, verifier1); assertEq(verifications[1].verifier, verifier2); @@ -329,7 +329,7 @@ contract TreeNftVerificationTest is Test { vm.prank(verifier1); treeNft.verify(1, proofs, "verified tree 1"); - Tree[] memory verifiedTrees = treeNft.getVerifiedTreesByUser(verifier1); + (Tree[] memory verifiedTrees,) = treeNft.getVerifiedTreesByUserPaginated(verifier1, 0, 50); assertEq(verifiedTrees.length, 2); assertEq(verifiedTrees[0].id, 0); @@ -426,12 +426,12 @@ contract TreeNftVerificationTest is Test { treeNft.verify(2, proofs, "verified tree 2"); vm.stopPrank(); - Tree[] memory verifiedTrees = treeNft.getVerifiedTreesByUser(verifier1); + (Tree[] memory verifiedTrees,) = treeNft.getVerifiedTreesByUserPaginated(verifier1, 0, 50); assertEq(verifiedTrees.length, 3); vm.prank(planter); treeNft.removeVerification(1, verifier1); - Tree[] memory remainingTrees = treeNft.getVerifiedTreesByUser(verifier1); + (Tree[] memory remainingTrees,) = treeNft.getVerifiedTreesByUserPaginated(verifier1, 0, 50); assertEq(remainingTrees.length, 2); bool hasTree0 = false; @@ -442,7 +442,7 @@ contract TreeNftVerificationTest is Test { } assertTrue(hasTree0, "Tree 0 should still be verified"); assertTrue(hasTree2, "Tree 2 should still be verified"); - assertFalse(treeNft.isVerified(1, verifier1)); + assertTrue(treeNft.isVerified(1, verifier1)); } function test_removeVerificationPreservesOtherVerifiers() public { @@ -471,17 +471,412 @@ contract TreeNftVerificationTest is Test { assertTrue(treeNft.isVerified(0, verifier2)); assertTrue(treeNft.isVerified(0, thirdVerifier)); - TreeNftVerification[] memory allVerifications = treeNft.getTreeNftVerifiers(0); + (TreeNftVerification[] memory allVerifications,,) = treeNft.getTreeNftVerifiersPaginated(0, 0, 100); assertEq(allVerifications.length, 3); vm.prank(planter); treeNft.removeVerification(0, verifier2); assertTrue(treeNft.isVerified(0, verifier1)); - assertFalse(treeNft.isVerified(0, verifier2)); + assertTrue(treeNft.isVerified(0, verifier2)); assertTrue(treeNft.isVerified(0, thirdVerifier)); - TreeNftVerification[] memory remainingVerifications = treeNft.getTreeNftVerifiers(0); + (TreeNftVerification[] memory remainingVerifications,,) = treeNft.getTreeNftVerifiersPaginated(0, 0, 100); assertEq(remainingVerifications.length, 2); } + + function test_getUserVerifierTokenDetailsPaginated() public { + // Register user and mint trees + + vm.prank(verifier1); + treeNft.registerUserProfile("Verifier1", "ipfs://profile1"); + + string[] memory photos = new string[](1); + photos[0] = "photo1"; + + vm.prank(planter); + treeNft.mintNft(LATITUDE, LONGITUDE, "Tree1", IMAGE_URI, QR_HASH, METADATA, GEOHASH, photos, NUM_TREES); + + vm.prank(planter); + treeNft.mintNft( + LATITUDE + 1000, LONGITUDE + 1000, "Tree2", IMAGE_URI, QR_HASH, METADATA, GEOHASH, photos, NUM_TREES + ); + + vm.prank(planter); + treeNft.mintNft( + LATITUDE + 2000, LONGITUDE + 2000, "Tree3", IMAGE_URI, QR_HASH, METADATA, GEOHASH, photos, NUM_TREES + ); + + // Verifier1 verifies all trees + vm.prank(verifier1); + string[] memory proofs = new string[](1); + proofs[0] = "proof1"; + treeNft.verify(0, proofs, "verified tree 0"); + + vm.prank(verifier1); + treeNft.verify(1, proofs, "verified tree 1"); + + vm.prank(verifier1); + treeNft.verify(2, proofs, "verified tree 2"); + + (VerificationDetails[] memory details1, uint256 totalCount1) = + treeNft.getUserVerifierTokenDetails(planter, 0, 2); + assertEq(totalCount1, 3, "Total count should be 3"); + assertEq(details1.length, 2, "Should return 2 items with limit=2"); + assertEq(details1[0].verifier, verifier1); + assertEq(details1[1].verifier, verifier1); + assertEq(details1[0].numberOfTrees, 3 * NUM_TREES * 1e18); + assertEq(details1[1].numberOfTrees, 3 * NUM_TREES * 1e18); + + (VerificationDetails[] memory details2, uint256 totalCount2) = + treeNft.getUserVerifierTokenDetails(planter, 2, 2); + assertEq(details2.length, 1); + assertEq(totalCount2, 3); + assertEq(details2[0].verifier, verifier1); + assertEq(details2[0].numberOfTrees, 3 * NUM_TREES * 1e18); + + (VerificationDetails[] memory details3, uint256 totalCount3) = + treeNft.getUserVerifierTokenDetails(planter, 5, 2); + assertEq(details3.length, 0); + assertEq(totalCount3, 3); + } + + function test_getUserVerifierTokenDetailsEmptyUser() public view { + // Test with user who has no verifications + (VerificationDetails[] memory details, uint256 totalCount) = + treeNft.getUserVerifierTokenDetails(verifier1, 0, 10); + assertEq(details.length, 0); + assertEq(totalCount, 0); + } + + function test_getUserVerifierTokenDetailsWithTokenBalances() public { + // Register user and mint tree + vm.prank(verifier1); + treeNft.registerUserProfile("Verifier1", "ipfs://profile1"); + + vm.prank(planter); + string[] memory photos = new string[](1); + photos[0] = "photo1"; + treeNft.mintNft(LATITUDE, LONGITUDE, SPECIES, IMAGE_URI, QR_HASH, METADATA, GEOHASH, photos, NUM_TREES); + + vm.prank(verifier1); + string[] memory proofs = new string[](1); + proofs[0] = "proof1"; + treeNft.verify(0, proofs, "verified"); + + (VerificationDetails[] memory details, uint256 totalCount) = treeNft.getUserVerifierTokenDetails(planter, 0, 10); + assertEq(details.length, 1); + assertEq(totalCount, 1); + + address planterTokenAddr = treeNft.s_userToPlanterTokenAddress(verifier1); + PlanterToken planterToken = PlanterToken(planterTokenAddr); + assertEq(details[0].numberOfTrees, planterToken.balanceOf(planter)); + assertEq(details[0].numberOfTrees, NUM_TREES * 1e18); + assertEq(details[0].verifierPlanterTokenAddress, planterTokenAddr); + assertEq(details[0].description, "verified"); + assertFalse(details[0].isHidden); + } + + function test_getUserVerifierTokenDetailsWithHiddenVerifications() public { + // Register users and mint tree + vm.prank(verifier1); + treeNft.registerUserProfile("Verifier1", "ipfs://profile1"); + + vm.prank(planter); + string[] memory photos = new string[](1); + photos[0] = "photo1"; + treeNft.mintNft(LATITUDE, LONGITUDE, SPECIES, IMAGE_URI, QR_HASH, METADATA, GEOHASH, photos, NUM_TREES); + + vm.prank(verifier1); + string[] memory proofs = new string[](1); + proofs[0] = "proof1"; + treeNft.verify(0, proofs, "verified"); + + vm.prank(planter); + treeNft.removeVerification(0, verifier1); + + (VerificationDetails[] memory details, uint256 totalCount) = treeNft.getUserVerifierTokenDetails(planter, 0, 10); + assertEq(details.length, 1); + assertEq(totalCount, 1); + assertTrue(details[0].isHidden); // Should be marked as hidden + assertEq(details[0].verifier, verifier1); + } + + function test_getUserVerifierTokenDetailsPaginationBoundaries() public { + // Register user and mint tree + + vm.prank(verifier1); + treeNft.registerUserProfile("Verifier1", "ipfs://profile1"); + + vm.prank(planter); + string[] memory photos = new string[](1); + photos[0] = "photo1"; + treeNft.mintNft(LATITUDE, LONGITUDE, SPECIES, IMAGE_URI, QR_HASH, METADATA, GEOHASH, photos, NUM_TREES); + + // Verifier1 verifies the tree + vm.prank(verifier1); + string[] memory proofs = new string[](1); + proofs[0] = "proof1"; + treeNft.verify(0, proofs, "verified"); + + (VerificationDetails[] memory details1, uint256 totalCount1) = + treeNft.getUserVerifierTokenDetails(planter, 0, 100); + assertEq(details1.length, 1); + assertEq(totalCount1, 1); + + (VerificationDetails[] memory details2, uint256 totalCount2) = + treeNft.getUserVerifierTokenDetails(planter, 1, 10); + assertEq(details2.length, 0); + assertEq(totalCount2, 1); + + (VerificationDetails[] memory details3, uint256 totalCount3) = + treeNft.getUserVerifierTokenDetails(planter, 0, 0); + assertEq(details3.length, 0); + assertEq(totalCount3, 1); + } + + function test_cannotReverifyAfterRemoval() public { + vm.prank(planter); + string[] memory photos = new string[](1); + photos[0] = "photo1"; + treeNft.mintNft(LATITUDE, LONGITUDE, SPECIES, IMAGE_URI, QR_HASH, METADATA, GEOHASH, photos, NUM_TREES); + + vm.prank(verifier1); + string[] memory proofs = new string[](1); + proofs[0] = "proof1"; + treeNft.verify(0, proofs, "verified"); + assertTrue(treeNft.isVerified(0, verifier1)); + + vm.prank(planter); + treeNft.removeVerification(0, verifier1); + assertTrue(treeNft.isVerified(0, verifier1)); + + vm.prank(verifier1); + vm.expectRevert(AlreadyVerified.selector); + treeNft.verify(0, proofs, "try to verify again"); + } + + function test_removeVerificationOptimized() public { + vm.prank(planter); + string[] memory photos = new string[](1); + photos[0] = "photo1"; + treeNft.mintNft(LATITUDE, LONGITUDE, SPECIES, IMAGE_URI, QR_HASH, METADATA, GEOHASH, photos, NUM_TREES); + + vm.prank(verifier1); + string[] memory proofs = new string[](1); + proofs[0] = "proof1"; + treeNft.verify(0, proofs, "verified"); + + address planterTokenAddr = treeNft.s_userToPlanterTokenAddress(verifier1); + PlanterToken planterToken = PlanterToken(planterTokenAddr); + assertEq(planterToken.balanceOf(planter), NUM_TREES * 1e18); + + uint256 verificationId = 0; + uint256 verifierArrayIndex = 0; + uint256 verifiedTreesArrayIndex = 0; + uint256 userVerificationIndex = 0; + uint256 verifierTokenAddrIndex = 0; + + vm.prank(planter); + treeNft.removeVerificationOptimized( + verificationId, verifierArrayIndex, verifiedTreesArrayIndex, userVerificationIndex, verifierTokenAddrIndex + ); + + (TreeNftVerification[] memory verifications,,) = treeNft.getTreeNftVerifiersPaginated(0, 0, 100); + assertEq(verifications.length, 0); + assertEq(planterToken.balanceOf(planter), 0); + } + + function test_removeVerificationOptimizedWithMultipleVerifiers() public { + vm.prank(planter); + string[] memory photos = new string[](1); + photos[0] = "photo1"; + treeNft.mintNft(LATITUDE, LONGITUDE, SPECIES, IMAGE_URI, QR_HASH, METADATA, GEOHASH, photos, NUM_TREES); + + vm.prank(verifier1); + string[] memory proofs1 = new string[](1); + proofs1[0] = "proof1"; + treeNft.verify(0, proofs1, "verified by v1"); + + vm.prank(verifier2); + string[] memory proofs2 = new string[](1); + proofs2[0] = "proof2"; + treeNft.verify(0, proofs2, "verified by v2"); + + uint256 verificationId = 1; + uint256 verifierArrayIndex = 1; + uint256 verifiedTreesArrayIndex = 0; + uint256 userVerificationIndex = 1; + uint256 verifierTokenAddrIndex = 1; + + vm.prank(planter); + treeNft.removeVerificationOptimized( + verificationId, verifierArrayIndex, verifiedTreesArrayIndex, userVerificationIndex, verifierTokenAddrIndex + ); + + (TreeNftVerification[] memory verifications,,) = treeNft.getTreeNftVerifiersPaginated(0, 0, 100); + assertEq(verifications.length, 1); + assertEq(verifications[0].verifier, verifier1); + } + + function test_removeVerificationOptimizedInvalidVerificationId() public { + vm.prank(planter); + string[] memory photos = new string[](1); + photos[0] = "photo1"; + treeNft.mintNft(LATITUDE, LONGITUDE, SPECIES, IMAGE_URI, QR_HASH, METADATA, GEOHASH, photos, NUM_TREES); + + vm.prank(verifier1); + string[] memory proofs = new string[](1); + proofs[0] = "proof1"; + treeNft.verify(0, proofs, "verified"); + + vm.prank(planter); + vm.expectRevert(VerificationNotFound.selector); + treeNft.removeVerificationOptimized(999, 0, 0, 0, 0); + } + + function test_removeVerificationOptimizedInvalidVerifierIndex() public { + vm.prank(planter); + string[] memory photos = new string[](1); + photos[0] = "photo1"; + treeNft.mintNft(LATITUDE, LONGITUDE, SPECIES, IMAGE_URI, QR_HASH, METADATA, GEOHASH, photos, NUM_TREES); + + vm.prank(verifier1); + string[] memory proofs = new string[](1); + proofs[0] = "proof1"; + treeNft.verify(0, proofs, "verified"); + + vm.prank(planter); + vm.expectRevert(VerificationNotFound.selector); + treeNft.removeVerificationOptimized(0, 999, 0, 0, 0); + } + + function test_removeVerificationOptimizedInvalidTreeIndex() public { + vm.prank(planter); + string[] memory photos = new string[](1); + photos[0] = "photo1"; + treeNft.mintNft(LATITUDE, LONGITUDE, SPECIES, IMAGE_URI, QR_HASH, METADATA, GEOHASH, photos, NUM_TREES); + + vm.prank(verifier1); + string[] memory proofs = new string[](1); + proofs[0] = "proof1"; + treeNft.verify(0, proofs, "verified"); + + vm.prank(planter); + vm.expectRevert(VerificationNotFound.selector); + treeNft.removeVerificationOptimized(0, 0, 999, 0, 0); + } + + function test_removeVerificationOptimizedInvalidUserVerificationIndex() public { + vm.prank(planter); + string[] memory photos = new string[](1); + photos[0] = "photo1"; + treeNft.mintNft(LATITUDE, LONGITUDE, SPECIES, IMAGE_URI, QR_HASH, METADATA, GEOHASH, photos, NUM_TREES); + + vm.prank(verifier1); + string[] memory proofs = new string[](1); + proofs[0] = "proof1"; + treeNft.verify(0, proofs, "verified"); + + vm.prank(planter); + vm.expectRevert(VerificationNotFound.selector); + treeNft.removeVerificationOptimized(0, 0, 0, 999, 0); + } + + function test_removeVerificationOptimizedOnlyOwner() public { + vm.prank(planter); + string[] memory photos = new string[](1); + photos[0] = "photo1"; + treeNft.mintNft(LATITUDE, LONGITUDE, SPECIES, IMAGE_URI, QR_HASH, METADATA, GEOHASH, photos, NUM_TREES); + + vm.prank(verifier1); + string[] memory proofs = new string[](1); + proofs[0] = "proof1"; + treeNft.verify(0, proofs, "verified"); + + vm.prank(verifier2); + vm.expectRevert(NotTreeOwner.selector); + treeNft.removeVerificationOptimized(0, 0, 0, 0, 0); + } + + function test_removeVerificationOptimizedAlreadyHidden() public { + vm.prank(planter); + string[] memory photos = new string[](1); + photos[0] = "photo1"; + treeNft.mintNft(LATITUDE, LONGITUDE, SPECIES, IMAGE_URI, QR_HASH, METADATA, GEOHASH, photos, NUM_TREES); + + vm.prank(verifier1); + string[] memory proofs = new string[](1); + proofs[0] = "proof1"; + treeNft.verify(0, proofs, "verified"); + + vm.prank(planter); + treeNft.removeVerificationOptimized(0, 0, 0, 0, 0); + + vm.prank(planter); + vm.expectRevert(VerificationNotFound.selector); + treeNft.removeVerificationOptimized(0, 0, 0, 0, 0); + } + + function test_removeVerificationOptimizedEmitsEvent() public { + vm.prank(planter); + string[] memory photos = new string[](1); + photos[0] = "photo1"; + treeNft.mintNft(LATITUDE, LONGITUDE, SPECIES, IMAGE_URI, QR_HASH, METADATA, GEOHASH, photos, NUM_TREES); + + vm.prank(verifier1); + string[] memory proofs = new string[](1); + proofs[0] = "proof1"; + treeNft.verify(0, proofs, "verified"); + + vm.prank(planter); + vm.expectEmit(true, true, true, false); + emit VerificationRemoved(0, 0, verifier1); + treeNft.removeVerificationOptimized(0, 0, 0, 0, 0); + } + + function test_removeVerificationOptimizedWithInsufficientTokens() public { + vm.prank(planter); + string[] memory photos = new string[](1); + photos[0] = "photo1"; + treeNft.mintNft(LATITUDE, LONGITUDE, SPECIES, IMAGE_URI, QR_HASH, METADATA, GEOHASH, photos, NUM_TREES); + + vm.prank(verifier1); + string[] memory proofs = new string[](1); + proofs[0] = "proof1"; + treeNft.verify(0, proofs, "verified"); + + address planterTokenAddr = treeNft.s_userToPlanterTokenAddress(verifier1); + PlanterToken planterToken = PlanterToken(planterTokenAddr); + + vm.prank(planter); + planterToken.transfer(address(0x999), (NUM_TREES * 1e18) / 2); + + vm.prank(planter); + treeNft.removeVerificationOptimized(0, 0, 0, 0, 0); + + (TreeNftVerification[] memory verifications,,) = treeNft.getTreeNftVerifiersPaginated(0, 0, 100); + assertEq(verifications.length, 0); + } + + function test_removeVerificationOptimizedRevocationCounter() public { + vm.prank(verifier1); + treeNft.registerUserProfile("Verifier1", "ipfs://profile"); + + vm.prank(planter); + string[] memory photos = new string[](1); + photos[0] = "photo1"; + treeNft.mintNft(LATITUDE, LONGITUDE, SPECIES, IMAGE_URI, QR_HASH, METADATA, GEOHASH, photos, NUM_TREES); + + vm.prank(verifier1); + string[] memory proofs = new string[](1); + proofs[0] = "proof1"; + treeNft.verify(0, proofs, "verified"); + + vm.prank(planter); + treeNft.removeVerificationOptimized(0, 0, 0, 0, 0); + + UserDetails memory userDetails = treeNft.getUserProfile(verifier1); + assertEq(userDetails.verificationsRevoked, 1); + } }