diff --git a/.DS_Store b/.DS_Store index 636aa53..bf772c7 100644 Binary files a/.DS_Store and b/.DS_Store differ diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 762a296..4a17816 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -7,6 +7,7 @@ on: env: FOUNDRY_PROFILE: ci + FOUNDRY_DISABLE_NIGHTLY_WARNING: true jobs: check: @@ -23,7 +24,7 @@ jobs: - name: Install Foundry uses: foundry-rs/foundry-toolchain@v1 with: - version: nightly + version: stable - name: Show Forge version run: | diff --git a/script/DeployAllContracts.s.sol b/script/DeployAllContracts.s.sol new file mode 100644 index 0000000..b82f940 --- /dev/null +++ b/script/DeployAllContracts.s.sol @@ -0,0 +1,83 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.28; + +import "forge-std/Script.sol"; +import "../src/TreeNft.sol"; +import "../src/token-contracts/CareToken.sol"; +import "../src/token-contracts/PlanterToken.sol"; +import "../src/token-contracts/LegacyToken.sol"; +import "../src/OrganisationFactory.sol"; + +contract DeployAllContractsAtOnce is Script { + address public careTokenAddress; + address public planterTokenAddress; + address public verifierTokenAddress; + address public legacyTokenAddress; + address public treeNftAddress; + + function run() external { + uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY"); + address deployer = vm.addr(deployerPrivateKey); + + console.log("\n========== DEPLOYMENT INITIALIZED =========="); + console.log("Deployer Address: ", deployer); + console.log("Deployer ETH Balance: ", deployer.balance); + console.log("============================================\n"); + + vm.startBroadcast(deployerPrivateKey); + console.log(">> Step 1: Deploying ERC20 Token Contracts..."); + + CareToken careToken = new CareToken(deployer); + careTokenAddress = address(careToken); + console.log(" - CareToken deployed at: ", careTokenAddress); + + LegacyToken legacyToken = new LegacyToken(deployer); + legacyTokenAddress = address(legacyToken); + console.log(" - LegacyToken deployed at: ", legacyTokenAddress); + + console.log("\n>> Step 2: Deploying TreeNft Contract..."); + TreeNft treeNft = new TreeNft(careTokenAddress, legacyTokenAddress); + treeNftAddress = address(treeNft); + console.log(" - TreeNft deployed at: ", treeNftAddress); + + console.log("\n>> Step 3: Transferring Token Ownership to TreeNft..."); + careToken.transferOwnership(treeNftAddress); + console.log(" - CareToken ownership transferred."); + legacyToken.transferOwnership(treeNftAddress); + console.log(" - LegacyToken ownership transferred."); + + console.log("\n>> Step 4: Deploying OrganisationFactory..."); + OrganisationFactory orgFactory = new OrganisationFactory(treeNftAddress); + address orgFactoryAddress = address(orgFactory); + console.log(" - OrganisationFactory deployed at:", orgFactoryAddress); + + vm.stopBroadcast(); + + console.log("\n========== DEPLOYMENT SUMMARY =========="); + console.log("CareToken Address: ", careTokenAddress); + console.log("LegacyToken Address: ", legacyTokenAddress); + console.log("TreeNft Address: ", treeNftAddress); + console.log("OrganisationFactory: ", orgFactoryAddress); + console.log("All token ownerships successfully transferred to TreeNft."); + console.log("=========================================\n"); + + verifyDeployment(); + } + + function verifyDeployment() internal view { + console.log(">> Verifying Deployment Integrity..."); + + TreeNft treeNft = TreeNft(treeNftAddress); + + require(address(treeNft.careTokenContract()) == careTokenAddress, "CareToken address mismatch"); + require(address(treeNft.legacyToken()) == legacyTokenAddress, "LegacyToken address mismatch"); + + CareToken careToken = CareToken(careTokenAddress); + require(careToken.owner() == treeNftAddress, "CareToken ownership not transferred"); + + CareToken legacyToken = CareToken(legacyTokenAddress); + require(legacyToken.owner() == treeNftAddress, "LegacyToken ownership not transferred"); + + console.log("Deployment verification passed.\n"); + } +} diff --git a/script/DeployOrganisationFactory.s.sol b/script/DeployOrganisationFactory.s.sol new file mode 100644 index 0000000..ddfacd5 --- /dev/null +++ b/script/DeployOrganisationFactory.s.sol @@ -0,0 +1,37 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.28; + +import "forge-std/Script.sol"; +import "../src/TreeNft.sol"; +import "../src/OrganisationFactory.sol"; + +contract DeployOrganisationFactory is Script { + function run() external { + uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY"); + address deployer = vm.addr(deployerPrivateKey); + address treeNftAddress = vm.envAddress("TREE_NFT_ADDRESS"); + + if (treeNftAddress.code.length <= 0) revert InvalidContractAddress(); + + console.log("\n========== DEPLOYMENT STARTED =========="); + console.log(">> Deployer Address: ", deployer); + console.log(">> Deployer Balance (wei): ", deployer.balance); + console.log(">> Linked TreeNFT Address: ", treeNftAddress); + console.log("========================================\n"); + + vm.startBroadcast(deployerPrivateKey); + + console.log("Deploying OrganisationFactory..."); + OrganisationFactory orgFactory = new OrganisationFactory(treeNftAddress); + address orgFactoryAddress = address(orgFactory); + console.log("OrganisationFactory deployed at:", orgFactoryAddress); + + vm.stopBroadcast(); + + console.log("\n========== DEPLOYMENT SUMMARY =========="); + console.log("OrganisationFactory Address: ", orgFactoryAddress); + console.log("Linked TreeNFT Address: ", treeNftAddress); + console.log("Deployment completed successfully."); + console.log("========================================\n"); + } +} diff --git a/script/DeployTreeNftContract.s.sol b/script/DeployTreeNftContract.s.sol index da161bb..63bf833 100644 --- a/script/DeployTreeNftContract.s.sol +++ b/script/DeployTreeNftContract.s.sol @@ -5,13 +5,10 @@ import "forge-std/Script.sol"; import "../src/TreeNft.sol"; import "../src/token-contracts/CareToken.sol"; import "../src/token-contracts/PlanterToken.sol"; -import "../src/token-contracts/VerifierToken.sol"; import "../src/token-contracts/LegacyToken.sol"; contract DeployTreeNft is Script { address public careTokenAddress; - address public planterTokenAddress; - address public verifierTokenAddress; address public legacyTokenAddress; address public treeNftAddress; @@ -29,37 +26,22 @@ contract DeployTreeNft is Script { careTokenAddress = address(careToken); console.log("CareToken deployed at:", careTokenAddress); - PlanterToken planterToken = new PlanterToken(deployer); - planterTokenAddress = address(planterToken); - console.log("PlanterToken deployed at:", planterTokenAddress); - - VerifierToken verifierToken = new VerifierToken(deployer); - verifierTokenAddress = address(verifierToken); - console.log("VerifierToken deployed at:", verifierTokenAddress); - LegacyToken legacyToken = new LegacyToken(deployer); legacyTokenAddress = address(legacyToken); console.log("LegacyToken deployed at:", legacyTokenAddress); console.log("Step 2: Deploy TreeNft contract..."); - TreeNft treeNft = new TreeNft(careTokenAddress, planterTokenAddress, verifierTokenAddress, legacyTokenAddress); + TreeNft treeNft = new TreeNft(careTokenAddress, legacyTokenAddress); treeNftAddress = address(treeNft); console.log("TreeNft deployed at:", treeNftAddress); console.log("Step 3: Transfer ownership to TreeNft contract..."); careToken.transferOwnership(treeNftAddress); console.log("CareToken ownership transferred to TreeNft"); - planterToken.transferOwnership(treeNftAddress); - console.log("PlanterToken ownership transferred to TreeNft"); - verifierToken.transferOwnership(treeNftAddress); - console.log("VerifierToken ownership transferred to TreeNft"); legacyToken.transferOwnership(treeNftAddress); console.log("LegacyToken ownership transferred to TreeNft"); - vm.stopBroadcast(); console.log("\n=== DEPLOYMENT SUMMARY ==="); console.log("CareToken:", careTokenAddress); - console.log("PlanterToken:", planterTokenAddress); - console.log("VerifierToken:", verifierTokenAddress); console.log("LegacyToken:", legacyTokenAddress); console.log("TreeNft:", treeNftAddress); console.log("All token ownerships transferred to TreeNft!"); @@ -72,12 +54,14 @@ contract DeployTreeNft is Script { TreeNft treeNft = TreeNft(treeNftAddress); require(address(treeNft.careTokenContract()) == careTokenAddress, "CareToken address mismatch"); - require(address(treeNft.planterTokenContract()) == planterTokenAddress, "PlanterToken address mismatch"); - require(address(treeNft.verifierTokenContract()) == verifierTokenAddress, "VerifierToken address mismatch"); require(address(treeNft.legacyToken()) == legacyTokenAddress, "LegacyToken address mismatch"); CareToken careToken = CareToken(careTokenAddress); - require(careToken.owner() == treeNftAddress, "CareToken ownership not transferred"); + if (careToken.owner() != treeNftAddress) revert OwnershipNotTransferred(); + + LegacyToken legacyToken = LegacyToken(legacyTokenAddress); + if (legacyToken.owner() != treeNftAddress) revert OwnershipNotTransferred(); + console.log("Deployment verification successful!"); } } diff --git a/src/Organisation.sol b/src/Organisation.sol index a29092c..a47119d 100644 --- a/src/Organisation.sol +++ b/src/Organisation.sol @@ -257,9 +257,11 @@ contract Organisation { uint256 _longitude, string memory _species, string memory _imageURI, - string memory _qrIpfshash, + string memory _qrPhoto, + string memory _metadata, string[] memory photos, - string memory geoHash + string memory geoHash, + uint256 numberOfTrees ) public { if (_latitude > 180 * 1e6) revert InvalidCoordinates(); if (_longitude > 360 * 1e6) revert InvalidCoordinates(); @@ -271,10 +273,13 @@ contract Organisation { longitude: _longitude, species: _species, imageUri: _imageURI, - qrIpfsHash: _qrIpfshash, + qrPhoto: _qrPhoto, photos: photos, geoHash: geoHash, - status: 0 + metadata: _metadata, + status: 0, + numberOfTrees: numberOfTrees, + initiator: msg.sender }); if (checkOwnership(msg.sender)) { s_treeProposalYesVoters[s_treePlantingProposalCounter].push(msg.sender); @@ -285,9 +290,11 @@ contract Organisation { proposal.longitude, proposal.species, proposal.imageUri, - proposal.qrIpfsHash, + proposal.qrPhoto, + proposal.metadata, proposal.geoHash, - proposal.photos + proposal.photos, + proposal.numberOfTrees ); } } @@ -397,9 +404,11 @@ contract Organisation { proposal.longitude, proposal.species, proposal.imageUri, - proposal.qrIpfsHash, + proposal.qrPhoto, + proposal.metadata, proposal.geoHash, - proposal.photos + proposal.photos, + proposal.numberOfTrees ); } else if (s_treeProposalNoVoters[proposalID].length >= requiredVotes) { proposal.status = 2; diff --git a/src/OrganisationFactory.sol b/src/OrganisationFactory.sol index 987b7d8..e002b6b 100644 --- a/src/OrganisationFactory.sol +++ b/src/OrganisationFactory.sol @@ -110,7 +110,7 @@ contract OrganisationFactory is Ownable { contractAddress: orgAddress, name: name, description: description, - photoIpfsHash: photoIpfsHash, + organisationPhoto: photoIpfsHash, owners: owners, members: members, ownerCount: owners.length, @@ -123,7 +123,7 @@ contract OrganisationFactory is Ownable { contractAddress: organisationAddress, name: "ERROR: Unable to fetch", description: "ERROR: Contract call failed", - photoIpfsHash: "", + organisationPhoto: "", owners: new address[](0), members: new address[](0), ownerCount: 0, diff --git a/src/TreeNft.sol b/src/TreeNft.sol index 9500398..8708207 100644 --- a/src/TreeNft.sol +++ b/src/TreeNft.sol @@ -15,52 +15,44 @@ import "./OrganisationFactory.sol"; import "./token-contracts/CareToken.sol"; import "./token-contracts/LegacyToken.sol"; import "./token-contracts/PlanterToken.sol"; -import "./token-contracts/VerifierToken.sol"; contract TreeNft is ERC721, Ownable { - uint256 private s_tokenCounter; + uint256 private s_treeTokenCounter; uint256 private s_organisationCounter; uint256 private s_deathCounter; - uint256 private s_treeNftVerification; + uint256 private s_treeNftVerificationCounter; uint256 private s_userCounter; uint256 public minimumTimeToMarkTreeDead = 365 days; CareToken public careTokenContract; - PlanterToken public planterTokenContract; - VerifierToken public verifierTokenContract; LegacyToken public legacyToken; mapping(uint256 => Tree) private s_tokenIDtoTree; + mapping(uint256 => address) private s_tokenIDtoVerificationContracts; mapping(uint256 => address[]) private s_tokenIDtoVerifiers; mapping(address => uint256[]) private s_userToNFTs; + mapping(address => address) public s_userToPlanterTokenAddress; + mapping(address => address[]) public s_userToVerifierTokenAddresses; mapping(uint256 => mapping(address => bool)) private s_tokenIDtoUserVerification; - mapping(address => uint256[]) private s_verifierToTokenIDs; + mapping(address => uint256[]) private s_verifierToTreeTokenIDs; mapping(uint256 => TreeNftVerification) private s_tokenIDtoTreeNftVerfication; mapping(uint256 => uint256[]) private s_treeTokenIdToVerifications; + mapping(address => TreeNftVerification[]) private s_userToVerifications; mapping(address => User) s_addressToUser; - constructor( - address _careTokenContract, - address _planterTokenContract, - address _verifierTokenContract, - address _legacyTokenContract - ) Ownable(msg.sender) ERC721("TreeNFT", "TREE") { - s_tokenCounter = 0; + constructor(address _careTokenContract, address _legacyTokenContract) + Ownable(msg.sender) + ERC721("TreeNFT", "TREE") + { + s_treeTokenCounter = 0; s_organisationCounter = 0; s_deathCounter = 0; - s_treeNftVerification = 0; + s_treeNftVerificationCounter = 0; s_userCounter = 0; - if (_careTokenContract == address(0)) revert InvalidInput(); - if (_planterTokenContract == address(0)) revert InvalidInput(); - if (_verifierTokenContract == address(0)) revert InvalidInput(); - if (_legacyTokenContract == address(0)) revert InvalidInput(); - careTokenContract = CareToken(_careTokenContract); - planterTokenContract = PlanterToken(_planterTokenContract); - verifierTokenContract = VerifierToken(_verifierTokenContract); legacyToken = LegacyToken(_legacyTokenContract); } @@ -71,37 +63,41 @@ contract TreeNft is ERC721, Ownable { uint256 longitude, string memory species, string memory imageUri, - string memory qrIpfsHash, + string memory qrPhoto, + string memory metadata, string memory geoHash, - string[] memory initialPhotos + string[] memory initialPhotos, + uint256 numberOfTrees ) public { - // This function mints a new NFT for the user - if (latitude > 180 * 1e6) revert InvalidCoordinates(); if (longitude > 360 * 1e6) revert InvalidCoordinates(); - uint256 tokenId = s_tokenCounter; - s_tokenCounter++; + uint256 tokenId = s_treeTokenCounter; + s_treeTokenCounter++; + _mint(msg.sender, tokenId); address[] memory ancestors = new address[](1); ancestors[0] = msg.sender; + s_tokenIDtoTree[tokenId] = Tree( + tokenId, latitude, longitude, block.timestamp, type(uint256).max, species, imageUri, - qrIpfsHash, + qrPhoto, + metadata, initialPhotos, geoHash, ancestors, block.timestamp, - 0 + 0, + numberOfTrees ); s_userToNFTs[msg.sender].push(tokenId); - planterTokenContract.mint(msg.sender, tokenId); } function tokenURI(uint256 tokenId) public view override returns (string memory) { @@ -134,8 +130,8 @@ contract TreeNft is ERC721, Ownable { function getAllNFTs() public view returns (Tree[] memory) { // This function retrieves all NFTs in the contract - Tree[] memory allTrees = new Tree[](s_tokenCounter); - for (uint256 i = 0; i < s_tokenCounter; i++) { + Tree[] memory allTrees = new Tree[](s_treeTokenCounter); + for (uint256 i = 0; i < allTrees.length; i++) { allTrees[i] = s_tokenIDtoTree[i]; } return allTrees; @@ -149,7 +145,7 @@ contract TreeNft is ERC721, Ownable { // This function retrieves recent trees with pagination if (limit > 50) revert PaginationLimitExceeded(); - uint256 totalTrees = s_tokenCounter; + uint256 totalTrees = s_treeTokenCounter; if (offset >= totalTrees) return (new Tree[](0), totalTrees, false); uint256 available = totalTrees - offset; uint256 toReturn = available < limit ? available : limit; @@ -212,36 +208,98 @@ contract TreeNft is ERC721, Ownable { // This function allows a verifier to verify a tree if (!_exists(_tokenId)) revert InvalidTreeID(); - TreeNftVerification memory treeVerification = - TreeNftVerification(msg.sender, block.timestamp, _proofHashes, _description, false, _tokenId); + address treeOwner = ownerOf(_tokenId); + Tree memory tree = s_tokenIDtoTree[_tokenId]; + if (msg.sender == treeOwner) revert CannotVerifyOwnTree(); + + if (s_tokenIDtoUserVerification[_tokenId][msg.sender]) { + revert AlreadyVerified(); + } + if (s_userToPlanterTokenAddress[msg.sender] == address(0)) { + PlanterToken newPlanterToken = new PlanterToken(msg.sender); + s_userToPlanterTokenAddress[msg.sender] = address(newPlanterToken); + } + address planterTokenContract = s_userToPlanterTokenAddress[msg.sender]; + PlanterToken planterToken = PlanterToken(planterTokenContract); + if (!isVerified(_tokenId, msg.sender)) { + 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_verifierToTokenIDs[msg.sender].push(_tokenId); - s_tokenIDtoTreeNftVerfication[s_treeNftVerification] = treeVerification; - s_treeTokenIdToVerifications[_tokenId].push(s_treeNftVerification); - s_treeNftVerification++; - verifierTokenContract.mint(msg.sender, 1); - planterTokenContract.mint(ownerOf(_tokenId), 1); + s_verifierToTreeTokenIDs[msg.sender].push(_tokenId); + s_tokenIDtoTreeNftVerfication[s_treeNftVerificationCounter] = treeVerification; + 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); } } - function removeVerification(uint256 _verificationId) public { - // This function enables the owner of the NFT to remove verifications as he pleases (in case of fraudalent verification spam) + function removeVerification(uint256 _tokenId, address verifier) public { + // This function facilitates the owner of the tree nft to remove fraudulent verifiers - TreeNftVerification memory treeNftVerification = s_tokenIDtoTreeNftVerfication[_verificationId]; - if (msg.sender != ownerOf(treeNftVerification.treeNftId)) revert NotTreeOwner(); - treeNftVerification.isHidden = true; - User memory user = s_addressToUser[treeNftVerification.verifier]; - user.verificationsRevoked++; - s_addressToUser[treeNftVerification.verifier] = user; - emit VerificationRemoved(_verificationId, treeNftVerification.treeNftId, treeNftVerification.verifier); + if (msg.sender != ownerOf(_tokenId)) revert NotTreeOwner(); + if (!s_tokenIDtoUserVerification[_tokenId][verifier]) { + 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) { + verifiers[i] = verifiers[verifiers.length - 1]; + verifiers.pop(); + break; + } + } + uint256[] storage verifiedTrees = s_verifierToTreeTokenIDs[verifier]; + for (uint256 i = 0; i < verifiedTrees.length; i++) { + if (verifiedTrees[i] == _tokenId) { + verifiedTrees[i] = verifiedTrees[verifiedTrees.length - 1]; + verifiedTrees.pop(); + 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; + + User storage user = s_addressToUser[verifier]; + user.verificationsRevoked++; + address planterTokenAddr = s_userToPlanterTokenAddress[verifier]; + if (planterTokenAddr != address(0)) { + PlanterToken planterToken = PlanterToken(planterTokenAddr); + uint256 tokensToReturn = tree.numberOfTrees * 1e18; + if (planterToken.balanceOf(treeOwner) >= tokensToReturn) { + planterToken.burnFrom(treeOwner, tokensToReturn); + address[] storage verifierTokenAddrs = s_userToVerifierTokenAddresses[treeOwner]; + for (uint256 j = 0; j < verifierTokenAddrs.length; j++) { + if (verifierTokenAddrs[j] == planterTokenAddr) { + verifierTokenAddrs[j] = verifierTokenAddrs[verifierTokenAddrs.length - 1]; + verifierTokenAddrs.pop(); + break; + } + } + } + } + + emit VerificationRemoved(verificationIds[i], _tokenId, verifier); + break; + } + } } function getVerifiedTreesByUser(address verifier) public view returns (Tree[] memory) { // This function retrieves all trees verified by a specific verifier - uint256[] memory verifiedTokens = s_verifierToTokenIDs[verifier]; + 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]; @@ -257,7 +315,7 @@ contract TreeNft is ERC721, Ownable { { // Get the total number of trees verified by this verifier - uint256[] memory verifiedTokens = s_verifierToTokenIDs[verifier]; + uint256[] memory verifiedTokens = s_verifierToTreeTokenIDs[verifier]; totalCount = verifiedTokens.length; if (offset >= totalCount) { return (new Tree[](0), totalCount); @@ -341,33 +399,79 @@ contract TreeNft is ERC721, Ownable { // This function marks a tree as dead if (!_exists(tokenId)) revert InvalidTreeID(); - if (s_tokenIDtoTree[tokenId].death != type(uint256).max) revert TreeAlreadyDead(); + if (s_tokenIDtoTree[tokenId].death != type(uint256).max) { + revert TreeAlreadyDead(); + } if (ownerOf(tokenId) != msg.sender) revert NotTreeOwner(); if (s_tokenIDtoTree[tokenId].planting + minimumTimeToMarkTreeDead >= block.timestamp) { revert MinimumMarkDeadTimeNotReached(); } - legacyToken.mint(msg.sender, 1); + legacyToken.mint(msg.sender, 1 * 1e18); s_tokenIDtoTree[tokenId].death = block.timestamp; s_deathCounter++; } - function registerUserProfile(string memory _name, string memory _profilePhotoHash) public { + function registerUserProfile(string memory _name, string memory _profilePhoto) public { // This function registers a user - if (s_addressToUser[msg.sender].userAddress != address(0)) revert UserAlreadyRegistered(); - User memory user = User(msg.sender, _profilePhotoHash, _name, block.timestamp, 0, 0); + if (s_addressToUser[msg.sender].userAddress != address(0)) { + revert UserAlreadyRegistered(); + } + User memory user = User(msg.sender, _profilePhoto, _name, block.timestamp, 0, 0); s_addressToUser[msg.sender] = user; s_userCounter++; } - function updateUserDetails(string memory _name, string memory _profilePhotoHash) public { + function getUserProfile(address userAddress) public view returns (UserDetails memory userDetails) { + // This function returns the details of the user + + if (s_addressToUser[userAddress].userAddress == address(0)) { + revert UserNotRegistered(); + } + User memory storedUserDetails = s_addressToUser[userAddress]; + userDetails.name = storedUserDetails.name; + userDetails.dateJoined = storedUserDetails.dateJoined; + userDetails.profilePhoto = storedUserDetails.profilePhoto; + userDetails.userAddress = storedUserDetails.userAddress; + userDetails.reportedSpam = storedUserDetails.reportedSpam; + userDetails.verificationsRevoked = storedUserDetails.verificationsRevoked; + userDetails.careTokens = careTokenContract.balanceOf(userAddress); + userDetails.legacyTokens = legacyToken.balanceOf(userAddress); + return userDetails; + } + + function getUserVerifierTokenDetails(address userAddress) + public + view + returns (VerificationDetails[] memory verifierTokenDetails) + { + // This function returns the verifier token address of the user + + TreeNftVerification[] memory userVerifications = s_userToVerifications[userAddress]; + for (uint256 i = 0; i < userVerifications.length; i++) { + PlanterToken planterToken = PlanterToken(userVerifications[i].verifierPlanterTokenAddress); + verifierTokenDetails[i] = VerificationDetails({ + verifier: userVerifications[i].verifier, + timestamp: userVerifications[i].timestamp, + proofHashes: userVerifications[i].proofHashes, + description: userVerifications[i].description, + isHidden: userVerifications[i].isHidden, + numberOfTrees: planterToken.balanceOf(userAddress), + verifierPlanterTokenAddress: userVerifications[i].verifierPlanterTokenAddress + }); + } + } + + function updateUserDetails(string memory _name, string memory _profilePhoto) public { // This function enables a user to change his user details - if (s_addressToUser[msg.sender].userAddress == address(0)) revert UserNotRegistered(); + if (s_addressToUser[msg.sender].userAddress == address(0)) { + revert UserNotRegistered(); + } s_addressToUser[msg.sender].name = _name; - s_addressToUser[msg.sender].profilePhotoIpfs = _profilePhotoHash; + s_addressToUser[msg.sender].profilePhoto = _profilePhoto; } function isVerified(uint256 tokenId, address verifier) public view returns (bool) { @@ -377,6 +481,6 @@ contract TreeNft is ERC721, Ownable { } function _exists(uint256 tokenId) internal view returns (bool) { - return tokenId < s_tokenCounter && tokenId >= 0; + return tokenId < s_treeTokenCounter && tokenId >= 0; } } diff --git a/src/token-contracts/PlanterToken.sol b/src/token-contracts/PlanterToken.sol index d93e8c5..8d7efe4 100644 --- a/src/token-contracts/PlanterToken.sol +++ b/src/token-contracts/PlanterToken.sol @@ -5,9 +5,21 @@ import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; contract PlanterToken is ERC20, Ownable { - constructor(address owner) Ownable(owner) ERC20("PlanterToken", "PRT") {} + address public planterAddress; + + constructor(address _planter) Ownable(msg.sender) ERC20("PlanterToken", "PRT") { + planterAddress = _planter; + } function mint(address to, uint256 amount) external onlyOwner { _mint(to, amount); } + + function burnFrom(address from, uint256 amount) external onlyOwner { + _burn(from, amount); + } + + function getPlanterAddress() external view returns (address) { + return planterAddress; + } } diff --git a/src/token-contracts/VerifierToken.sol b/src/token-contracts/VerifierToken.sol deleted file mode 100644 index 96c07f7..0000000 --- a/src/token-contracts/VerifierToken.sol +++ /dev/null @@ -1,13 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.28; - -import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; -import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; - -contract VerifierToken is ERC20, Ownable { - constructor(address owner) Ownable(owner) ERC20("VerifierToken", "VRT") {} - - function mint(address to, uint256 amount) external onlyOwner { - _mint(to, amount); - } -} diff --git a/src/utils/errors.sol b/src/utils/errors.sol index f39ab9c..d392f5e 100644 --- a/src/utils/errors.sol +++ b/src/utils/errors.sol @@ -27,6 +27,7 @@ error InvalidProposalId(); error AlreadyVoted(); error InvalidInput(); error PaginationLimitExceeded(); +error InvalidContractAddress(); /// Request error InvalidRequestId(); @@ -42,7 +43,13 @@ error InvalidNameInput(); error InvalidTreeID(); error MinimumMarkDeadTimeNotReached(); error InvalidCoordinates(); +error CannotVerifyOwnTree(); +error VerificationNotFound(); /// User error UserAlreadyRegistered(); error UserNotRegistered(); + +/// Deploy + +error OwnershipNotTransferred(); diff --git a/src/utils/structs.sol b/src/utils/structs.sol index 43fb348..1444bb0 100644 --- a/src/utils/structs.sol +++ b/src/utils/structs.sol @@ -5,7 +5,7 @@ struct OrganisationDetails { address contractAddress; string name; string description; - string photoIpfsHash; + string organisationPhoto; address[] owners; address[] members; uint256 ownerCount; @@ -21,6 +21,17 @@ struct TreeNftVerification { string description; bool isHidden; uint256 treeNftId; + address verifierPlanterTokenAddress; +} + +struct VerificationDetails { + address verifier; + uint256 timestamp; + string[] proofHashes; + string description; + bool isHidden; + uint256 numberOfTrees; + address verifierPlanterTokenAddress; } struct OrganisationVerificationRequest { @@ -36,26 +47,40 @@ struct OrganisationVerificationRequest { struct User { address userAddress; - string profilePhotoIpfs; + string profilePhoto; string name; uint256 dateJoined; uint256 verificationsRevoked; uint256 reportedSpam; } +struct UserDetails { + address userAddress; + string profilePhoto; + string name; + uint256 dateJoined; + uint256 verificationsRevoked; + uint256 reportedSpam; + uint256 legacyTokens; + uint256 careTokens; +} + struct Tree { + uint256 id; uint256 latitude; uint256 longitude; uint256 planting; uint256 death; string species; string imageUri; - string qrIpfsHash; + string qrPhoto; + string metadata; string[] photos; string geoHash; address[] ancestors; uint256 lastCareTimestamp; uint256 careCount; + uint256 numberOfTrees; } struct TreePlantingProposal { @@ -64,8 +89,11 @@ struct TreePlantingProposal { uint256 longitude; string species; string imageUri; - string qrIpfsHash; + string qrPhoto; string[] photos; string geoHash; + string metadata; uint256 status; + uint256 numberOfTrees; + address initiator; } diff --git a/test/Organisation.t.sol b/test/Organisation.t.sol index feaf4b8..17fb74b 100644 --- a/test/Organisation.t.sol +++ b/test/Organisation.t.sol @@ -12,7 +12,6 @@ import "../src/TreeNft.sol"; import "../src/token-contracts/CareToken.sol"; import "../src/token-contracts/PlanterToken.sol"; -import "../src/token-contracts/VerifierToken.sol"; import "../src/token-contracts/LegacyToken.sol"; contract OrganisationTest is Test { @@ -20,7 +19,6 @@ contract OrganisationTest is Test { TreeNft private treeNft; CareToken public careToken; PlanterToken public planterToken; - VerifierToken public verifierToken; LegacyToken public legacyToken; address private owner = address(0x1); @@ -32,7 +30,7 @@ contract OrganisationTest is Test { uint256 constant LONGITUDE = 9876543; string constant SPECIES = "Oak"; string constant IMAGE_URI = "https://example.com/tree.jpg"; - string constant QR_IPFS_HASH = "QmTestQrHash"; + string constant QR_HASH = "QmTestQrHash"; string constant GEOHASH = "u4pruydqqvj"; string constant NAME = "Test Organisation"; string constant DESCRIPTION = "This is a test organisation."; @@ -41,32 +39,28 @@ contract OrganisationTest is Test { string constant DESCRIPTION2 = "This is a test organisation."; string constant PHOTO_IPFS_HASH2 = "QmTestPhotoHash"; string constant JOIN_REQUEST_DESCRIPTION = "I want to join this organisation"; + string constant METADATA = "ipfs://metaDataHash"; + uint256 constant NUMBER_OF_TREES = 1; function setUp() public { vm.startPrank(owner); careToken = new CareToken(owner); planterToken = new PlanterToken(owner); - verifierToken = new VerifierToken(owner); legacyToken = new LegacyToken(owner); - treeNft = new TreeNft(address(careToken), address(planterToken), address(verifierToken), address(legacyToken)); + treeNft = new TreeNft(address(careToken), address(legacyToken)); careToken.transferOwnership(address(treeNft)); planterToken.transferOwnership(address(treeNft)); - verifierToken.transferOwnership(address(treeNft)); legacyToken.transferOwnership(address(treeNft)); vm.stopPrank(); assertEq(careToken.owner(), address(treeNft)); - assertEq(planterToken.owner(), address(treeNft)); - assertEq(verifierToken.owner(), address(treeNft)); assertEq(legacyToken.owner(), address(treeNft)); assertEq(address(treeNft.careTokenContract()), address(careToken)); - assertEq(address(treeNft.planterTokenContract()), address(planterToken)); - assertEq(address(treeNft.verifierTokenContract()), address(verifierToken)); assertEq(address(treeNft.legacyToken()), address(legacyToken)); vm.startPrank(owner); @@ -158,7 +152,9 @@ contract OrganisationTest is Test { imageHashes[0] = "QmProofHash"; vm.prank(user3); - treeNft.mintNft(LATITUDE, LONGITUDE, SPECIES, IMAGE_URI, QR_IPFS_HASH, GEOHASH, imageHashes); + treeNft.mintNft( + LATITUDE, LONGITUDE, SPECIES, IMAGE_URI, QR_HASH, METADATA, GEOHASH, imageHashes, NUMBER_OF_TREES + ); vm.stopPrank(); vm.prank(user2); @@ -190,7 +186,9 @@ contract OrganisationTest is Test { imageHashes[0] = "QmProofHash"; vm.prank(user3); - treeNft.mintNft(LATITUDE, LONGITUDE, SPECIES, IMAGE_URI, QR_IPFS_HASH, GEOHASH, imageHashes); + treeNft.mintNft( + LATITUDE, LONGITUDE, SPECIES, IMAGE_URI, QR_HASH, METADATA, GEOHASH, imageHashes, NUMBER_OF_TREES + ); vm.stopPrank(); vm.prank(user1); @@ -240,7 +238,7 @@ contract OrganisationTest is Test { vm.prank(user1); Organisation(orgAddress).plantTreeProposal( - LATITUDE, LONGITUDE, SPECIES, IMAGE_URI, QR_IPFS_HASH, proofHashes, GEOHASH + LATITUDE, LONGITUDE, SPECIES, IMAGE_URI, QR_HASH, METADATA, proofHashes, GEOHASH, NUMBER_OF_TREES ); vm.stopPrank(); @@ -254,7 +252,7 @@ contract OrganisationTest is Test { assertEq(proposal.longitude, LONGITUDE); assertEq(proposal.species, SPECIES); assertEq(proposal.imageUri, IMAGE_URI); - assertEq(proposal.qrIpfsHash, QR_IPFS_HASH); + assertEq(proposal.qrPhoto, QR_HASH); assertEq(proposal.geoHash, GEOHASH); } @@ -270,7 +268,9 @@ contract OrganisationTest is Test { imageHashes[0] = "QmProofHash"; vm.prank(user3); - treeNft.mintNft(LATITUDE, LONGITUDE, SPECIES, IMAGE_URI, QR_IPFS_HASH, GEOHASH, imageHashes); + treeNft.mintNft( + LATITUDE, LONGITUDE, SPECIES, IMAGE_URI, QR_HASH, METADATA, GEOHASH, imageHashes, NUMBER_OF_TREES + ); vm.stopPrank(); vm.prank(user1); @@ -293,7 +293,7 @@ contract OrganisationTest is Test { string[] memory proofHashes = new string[](1); proofHashes[0] = "QmProofHash"; Organisation(orgAddress).plantTreeProposal( - LATITUDE, LONGITUDE, SPECIES, IMAGE_URI, QR_IPFS_HASH, proofHashes, GEOHASH + LATITUDE, LONGITUDE, SPECIES, IMAGE_URI, QR_HASH, METADATA, proofHashes, GEOHASH, NUMBER_OF_TREES ); vm.stopPrank(); diff --git a/test/OrganisationFactory.t.sol b/test/OrganisationFactory.t.sol index 142f665..d247a42 100644 --- a/test/OrganisationFactory.t.sol +++ b/test/OrganisationFactory.t.sol @@ -12,15 +12,12 @@ import "../src/TreeNft.sol"; import "../src/token-contracts/CareToken.sol"; import "../src/token-contracts/PlanterToken.sol"; -import "../src/token-contracts/VerifierToken.sol"; import "../src/token-contracts/LegacyToken.sol"; contract OrganisationFactoryTest is Test { OrganisationFactory private factory; TreeNft private treeNft; CareToken public careToken; - PlanterToken public planterToken; - VerifierToken public verifierToken; LegacyToken public legacyToken; address private owner = address(0x1); @@ -31,36 +28,28 @@ contract OrganisationFactoryTest is Test { string constant NAME1 = "Test Organisation"; string constant DESCRIPTION1 = "This is a test organisation."; - string constant PHOTO_IPFS_HASH1 = "QmTestPhotoHash"; + string constant PHOTO_HASH1 = "QmTestPhotoHash"; string constant NAME2 = "Test Organisation"; string constant DESCRIPTION2 = "This is a test organisation."; - string constant PHOTO_IPFS_HASH2 = "QmTestPhotoHash"; + string constant PHOTO_HASH2 = "QmTestPhotoHash"; function setUp() public { vm.startPrank(owner); careToken = new CareToken(owner); - planterToken = new PlanterToken(owner); - verifierToken = new VerifierToken(owner); legacyToken = new LegacyToken(owner); - treeNft = new TreeNft(address(careToken), address(planterToken), address(verifierToken), address(legacyToken)); + treeNft = new TreeNft(address(careToken), address(legacyToken)); careToken.transferOwnership(address(treeNft)); - planterToken.transferOwnership(address(treeNft)); - verifierToken.transferOwnership(address(treeNft)); legacyToken.transferOwnership(address(treeNft)); vm.stopPrank(); assertEq(careToken.owner(), address(treeNft)); - assertEq(planterToken.owner(), address(treeNft)); - assertEq(verifierToken.owner(), address(treeNft)); assertEq(legacyToken.owner(), address(treeNft)); assertEq(address(treeNft.careTokenContract()), address(careToken)); - assertEq(address(treeNft.planterTokenContract()), address(planterToken)); - assertEq(address(treeNft.verifierTokenContract()), address(verifierToken)); assertEq(address(treeNft.legacyToken()), address(legacyToken)); vm.startPrank(owner); @@ -80,7 +69,7 @@ contract OrganisationFactoryTest is Test { // This test checks if the createOrganisation function works correctly by creating an organisation and verifying its details. vm.prank(user1); - (uint256 orgId, address orgAddress) = factory.createOrganisation(NAME1, DESCRIPTION1, PHOTO_IPFS_HASH1); + (uint256 orgId, address orgAddress) = factory.createOrganisation(NAME1, DESCRIPTION1, PHOTO_HASH1); assertEq(orgId, 0); assertEq(factory.getOrganisationCount(), 1); ( @@ -95,7 +84,7 @@ contract OrganisationFactoryTest is Test { assert(organizationAddress == orgAddress); assertEq(name, NAME1); assertEq(description, DESCRIPTION1); - assertEq(photoIpfsHash, PHOTO_IPFS_HASH1); + assertEq(photoIpfsHash, PHOTO_HASH1); assertEq(owners[0], user1); assertEq(members.length, 1); assertEq(timeOfCreation, block.timestamp); @@ -105,10 +94,10 @@ contract OrganisationFactoryTest is Test { // This test checks if the getMyOrganisations function returns the correct organisation details for a user. vm.prank(user1); - factory.createOrganisation(NAME1, DESCRIPTION1, PHOTO_IPFS_HASH1); + factory.createOrganisation(NAME1, DESCRIPTION1, PHOTO_HASH1); vm.stopPrank(); vm.prank(user2); - factory.createOrganisation(NAME2, DESCRIPTION2, PHOTO_IPFS_HASH2); + factory.createOrganisation(NAME2, DESCRIPTION2, PHOTO_HASH2); vm.stopPrank(); vm.startPrank(user1); @@ -126,22 +115,22 @@ contract OrganisationFactoryTest is Test { // This test checks if the factory can return all organisations correctly. vm.prank(user1); - factory.createOrganisation(NAME1, DESCRIPTION1, PHOTO_IPFS_HASH1); + factory.createOrganisation(NAME1, DESCRIPTION1, PHOTO_HASH1); vm.stopPrank(); vm.prank(user2); - factory.createOrganisation(NAME2, DESCRIPTION2, PHOTO_IPFS_HASH2); + factory.createOrganisation(NAME2, DESCRIPTION2, PHOTO_HASH2); vm.stopPrank(); OrganisationDetails[] memory orgs = factory.getAllOrganisationDetails(); assertEq(orgs.length, 2); assertEq(orgs[0].name, NAME1); assertEq(orgs[0].description, DESCRIPTION1); - assertEq(orgs[0].photoIpfsHash, PHOTO_IPFS_HASH1); + assertEq(orgs[0].organisationPhoto, PHOTO_HASH1); assertEq(orgs[0].ownerCount, 1); assertEq(orgs[0].memberCount, 1); assertEq(orgs[1].name, NAME2); assertEq(orgs[1].description, DESCRIPTION2); - assertEq(orgs[1].photoIpfsHash, PHOTO_IPFS_HASH2); + assertEq(orgs[1].organisationPhoto, PHOTO_HASH2); assertEq(orgs[1].ownerCount, 1); assertEq(orgs[1].memberCount, 1); } @@ -150,10 +139,10 @@ contract OrganisationFactoryTest is Test { // This test checks if the factory can return all organisation IDs correctly. vm.prank(user1); - factory.createOrganisation(NAME1, DESCRIPTION1, PHOTO_IPFS_HASH1); + factory.createOrganisation(NAME1, DESCRIPTION1, PHOTO_HASH1); vm.stopPrank(); vm.prank(user2); - factory.createOrganisation(NAME2, DESCRIPTION2, PHOTO_IPFS_HASH2); + factory.createOrganisation(NAME2, DESCRIPTION2, PHOTO_HASH2); vm.stopPrank(); address[] memory orgAddresses = factory.getAllOrganisations(); diff --git a/test/TreeNft.t.sol b/test/TreeNft.t.sol index 6420559..9783616 100644 --- a/test/TreeNft.t.sol +++ b/test/TreeNft.t.sol @@ -1,532 +1,487 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.28; -import "forge-std/Test.sol"; +import "../lib/forge-std/src/Test.sol"; import "../src/TreeNft.sol"; import "../src/token-contracts/CareToken.sol"; -import "../src/token-contracts/PlanterToken.sol"; -import "../src/token-contracts/VerifierToken.sol"; import "../src/token-contracts/LegacyToken.sol"; +import "../src/token-contracts/PlanterToken.sol"; import "../src/utils/structs.sol"; import "../src/utils/errors.sol"; -contract TreeNftTest is Test { +contract TreeNftVerificationTest is Test { TreeNft public treeNft; CareToken public careToken; - PlanterToken public planterToken; - VerifierToken public verifierToken; LegacyToken public legacyToken; address public owner = address(0x1); - address public user1 = address(0x2); - address public user2 = address(0x3); - address public verifier1 = address(0x4); - address public verifier2 = address(0x5); - address public organisation = address(0x6); - - uint256 constant LATITUDE = 1234567; - uint256 constant LONGITUDE = 9876543; - string constant SPECIES = "Oak Tree"; - string constant IMAGE_URI = "ipfs://QmSampleImageHash"; - string constant QR_IPFS_HASH = "QmSampleQRHash"; - string constant GEO_HASH = "u4pruydqqvj"; + address public planter = address(0x2); + address public verifier1 = address(0x3); + address public verifier2 = address(0x4); + + // Events for testing + event VerificationRemoved(uint256 indexed verificationId, uint256 indexed treeNftId, address indexed verifier); + + uint256 public constant LATITUDE = 45 * 1e6; + uint256 public constant LONGITUDE = 90 * 1e6; + string public constant SPECIES = "Oak"; + string public constant IMAGE_URI = "ipfs://image"; + string public constant QR_HASH = "ipfs://qr"; + string public constant METADATA = "metadata"; + string public constant GEOHASH = "geohash"; + uint256 public constant NUM_TREES = 10; function setUp() public { vm.startPrank(owner); - careToken = new CareToken(owner); - planterToken = new PlanterToken(owner); - verifierToken = new VerifierToken(owner); legacyToken = new LegacyToken(owner); - - treeNft = new TreeNft(address(careToken), address(planterToken), address(verifierToken), address(legacyToken)); - + treeNft = new TreeNft(address(careToken), address(legacyToken)); careToken.transferOwnership(address(treeNft)); - planterToken.transferOwnership(address(treeNft)); - verifierToken.transferOwnership(address(treeNft)); legacyToken.transferOwnership(address(treeNft)); - vm.stopPrank(); - - assertEq(careToken.owner(), address(treeNft)); - assertEq(planterToken.owner(), address(treeNft)); - assertEq(verifierToken.owner(), address(treeNft)); - assertEq(legacyToken.owner(), address(treeNft)); - - assertEq(address(treeNft.careTokenContract()), address(careToken)); - assertEq(address(treeNft.planterTokenContract()), address(planterToken)); - assertEq(address(treeNft.verifierTokenContract()), address(verifierToken)); - assertEq(address(treeNft.legacyToken()), address(legacyToken)); } - function test_MintNft() public { - vm.prank(user1); - string[] memory initialPhotos = new string[](2); - initialPhotos[0] = "photo1.jpg"; - initialPhotos[1] = "photo2.jpg"; - - treeNft.mintNft(LATITUDE, LONGITUDE, SPECIES, IMAGE_URI, QR_IPFS_HASH, GEO_HASH, initialPhotos); - - assertEq(treeNft.ownerOf(0), user1); - assertEq(treeNft.balanceOf(user1), 1); - - Tree memory tree = treeNft.getTreeDetailsbyID(0); - assertEq(tree.latitude, LATITUDE); - assertEq(tree.longitude, LONGITUDE); - assertEq(tree.species, SPECIES); - assertEq(tree.imageUri, IMAGE_URI); - assertEq(tree.qrIpfsHash, QR_IPFS_HASH); - assertEq(tree.geoHash, GEO_HASH); - assertEq(tree.death, type(uint256).max); - assertEq(tree.ancestors[0], user1); - assertEq(tree.photos.length, 2); - assertEq(tree.photos[0], "photo1.jpg"); - assertEq(tree.photos[1], "photo2.jpg"); - } + function test_verifyTree() 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); - function test_MintMultipleNfts() public { - vm.startPrank(user1); - - string[] memory initialPhotos = new string[](0); - - // Mint first NFT - treeNft.mintNft(LATITUDE, LONGITUDE, SPECIES, IMAGE_URI, QR_IPFS_HASH, GEO_HASH, initialPhotos); - - // Mint second NFT - treeNft.mintNft( - LATITUDE + 1000, - LONGITUDE + 1000, - "Pine Tree", - "ipfs://QmPineImage", - "QmPineQR", - "u4pruydqqvk", - initialPhotos - ); + vm.prank(verifier1); + string[] memory proofs = new string[](1); + proofs[0] = "proof1"; + treeNft.verify(0, proofs, "verified"); - vm.stopPrank(); + assertTrue(treeNft.isVerified(0, verifier1)); - assertEq(treeNft.balanceOf(user1), 2); - assertEq(treeNft.ownerOf(0), user1); - assertEq(treeNft.ownerOf(1), user1); - Tree[] memory userTrees = treeNft.getNFTsByUser(user1); - assertEq(userTrees.length, 2); - assertEq(userTrees[0].species, SPECIES); - assertEq(userTrees[1].species, "Pine Tree"); + address planterTokenAddr = treeNft.s_userToPlanterTokenAddress(verifier1); + PlanterToken planterToken = PlanterToken(planterTokenAddr); + assertEq(planterToken.balanceOf(planter), NUM_TREES * 1e18); } - function test_TokenURI() public { - vm.prank(user1); - string[] memory initialPhotos = new string[](0); + function test_cannotVerifyOwnTree() 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(planter); + string[] memory proofs = new string[](1); + proofs[0] = "proof1"; + vm.expectRevert(CannotVerifyOwnTree.selector); + treeNft.verify(0, proofs, "verified"); + } - treeNft.mintNft(LATITUDE, LONGITUDE, SPECIES, IMAGE_URI, QR_IPFS_HASH, GEO_HASH, initialPhotos); + function test_cannotVerifyTwice() 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); - string memory uri = treeNft.tokenURI(0); - assertTrue(bytes(uri).length > 0); - assertEq( - keccak256(abi.encodePacked(substring(uri, 0, 29))), - keccak256(abi.encodePacked("data:application/json;base64,")) - ); - } + vm.prank(verifier1); + string[] memory proofs = new string[](1); + proofs[0] = "proof1"; + treeNft.verify(0, proofs, "verified"); - function test_TokenURIInvalidToken() public { - vm.expectRevert(InvalidTreeID.selector); - treeNft.tokenURI(999); + vm.prank(verifier1); + vm.expectRevert(AlreadyVerified.selector); + treeNft.verify(0, proofs, "verified again"); } - function test_GetAllNFTs() public { - vm.startPrank(user1); - string[] memory initialPhotos = new string[](0); + function test_multipleVerifiers() 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); - treeNft.mintNft(LATITUDE, LONGITUDE, SPECIES, IMAGE_URI, QR_IPFS_HASH, GEO_HASH, initialPhotos); - treeNft.mintNft(LATITUDE + 1, LONGITUDE + 1, "Pine", IMAGE_URI, QR_IPFS_HASH, GEO_HASH, initialPhotos); - vm.stopPrank(); + vm.prank(verifier1); + string[] memory proofs1 = new string[](1); + proofs1[0] = "proof1"; + treeNft.verify(0, proofs1, "verified by v1"); - Tree[] memory allTrees = treeNft.getAllNFTs(); - assertEq(allTrees.length, 2); - assertEq(allTrees[0].species, SPECIES); - assertEq(allTrees[1].species, "Pine"); - } + vm.prank(verifier2); + string[] memory proofs2 = new string[](1); + proofs2[0] = "proof2"; + treeNft.verify(0, proofs2, "verified by v2"); - function test_GetRecentTreesPaginated() public { - vm.startPrank(user1); - string[] memory initialPhotos = new string[](0); - - // Mint 5 trees - for (uint256 i = 0; i < 5; i++) { - treeNft.mintNft( - LATITUDE + i, - LONGITUDE + i, - string(abi.encodePacked("Tree", vm.toString(i))), - IMAGE_URI, - QR_IPFS_HASH, - GEO_HASH, - initialPhotos - ); - } - vm.stopPrank(); - (Tree[] memory trees, uint256 totalCount, bool hasMore) = treeNft.getRecentTreesPaginated(0, 3); + assertTrue(treeNft.isVerified(0, verifier1)); + assertTrue(treeNft.isVerified(0, verifier2)); - assertEq(trees.length, 3); - assertEq(totalCount, 5); - assertTrue(hasMore); - assertEq(trees[0].species, "Tree4"); - assertEq(trees[1].species, "Tree3"); - assertEq(trees[2].species, "Tree2"); - (Tree[] memory remainingTrees, uint256 totalCount2, bool hasMore2) = treeNft.getRecentTreesPaginated(3, 3); + address planterToken1 = treeNft.s_userToPlanterTokenAddress(verifier1); + address planterToken2 = treeNft.s_userToPlanterTokenAddress(verifier2); - assertEq(remainingTrees.length, 2); - assertEq(totalCount2, 5); - assertFalse(hasMore2); - assertEq(remainingTrees[0].species, "Tree1"); - assertEq(remainingTrees[1].species, "Tree0"); + assertEq(PlanterToken(planterToken1).balanceOf(planter), NUM_TREES * 1e18); + assertEq(PlanterToken(planterToken2).balanceOf(planter), NUM_TREES * 1e18); } - function test_GetRecentTreesPaginatedLimitExceeded() public { - vm.expectRevert(PaginationLimitExceeded.selector); - treeNft.getRecentTreesPaginated(0, 51); - } + function test_removeVerification() 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); - function testGetNFTsByUserPaginated() public { - vm.startPrank(user1); - string[] memory initialPhotos = new string[](0); - for (uint256 i = 0; i < 4; i++) { - treeNft.mintNft( - LATITUDE + i, - LONGITUDE + i, - string(abi.encodePacked("UserTree", vm.toString(i))), - IMAGE_URI, - QR_IPFS_HASH, - GEO_HASH, - initialPhotos - ); - } - vm.stopPrank(); - (Tree[] memory trees, uint256 totalCount) = treeNft.getNFTsByUserPaginated(user1, 0, 2); - - assertEq(trees.length, 2); - assertEq(totalCount, 4); - assertEq(trees[0].species, "UserTree0"); - assertEq(trees[1].species, "UserTree1"); - - (Tree[] memory remainingTrees, uint256 totalCount2) = treeNft.getNFTsByUserPaginated(user1, 2, 2); + vm.prank(verifier1); + string[] memory proofs = new string[](1); + proofs[0] = "proof1"; + treeNft.verify(0, proofs, "verified"); + assertTrue(treeNft.isVerified(0, verifier1)); + address planterTokenAddr = treeNft.s_userToPlanterTokenAddress(verifier1); + PlanterToken planterToken = PlanterToken(planterTokenAddr); + assertEq(planterToken.balanceOf(planter), NUM_TREES * 1e18); - assertEq(remainingTrees.length, 2); - assertEq(totalCount2, 4); - assertEq(remainingTrees[0].species, "UserTree2"); - assertEq(remainingTrees[1].species, "UserTree3"); + vm.prank(planter); + treeNft.removeVerification(0, verifier1); + assertFalse(treeNft.isVerified(0, verifier1)); + TreeNftVerification[] memory verifications = treeNft.getTreeNftVerifiers(0); + assertEq(verifications.length, 0); + Tree[] memory verifiedTrees = treeNft.getVerifiedTreesByUser(verifier1); + assertEq(verifiedTrees.length, 0); + assertEq(planterToken.balanceOf(planter), 0); } - function test_GetTreeDetailsbyIDInvalid() public { - vm.expectRevert(InvalidTreeID.selector); - treeNft.getTreeDetailsbyID(999); - } + function test_removeVerificationCompleteCleanup() public { + vm.prank(verifier1); + treeNft.registerUserProfile("Verifier1", "ipfs://profile1"); - function test_Verify() public { - vm.prank(user1); - string[] memory initialPhotos = new string[](0); - treeNft.mintNft(LATITUDE, LONGITUDE, SPECIES, IMAGE_URI, QR_IPFS_HASH, GEO_HASH, initialPhotos); + 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 proofHashes = new string[](2); - proofHashes[0] = "proof1"; - proofHashes[1] = "proof2"; - - treeNft.verify(0, proofHashes, "Tree looks healthy"); + 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"); assertTrue(treeNft.isVerified(0, verifier1)); + assertTrue(treeNft.isVerified(0, verifier2)); + TreeNftVerification[] memory verificationsBeforeRemoval = treeNft.getTreeNftVerifiers(0); + assertEq(verificationsBeforeRemoval.length, 2); + vm.prank(planter); + treeNft.removeVerification(0, verifier1); + assertFalse(treeNft.isVerified(0, verifier1)); + assertTrue(treeNft.isVerified(0, verifier2)); - TreeNftVerification[] memory verifications = treeNft.getTreeNftVerifiers(0); - assertEq(verifications.length, 1); - assertEq(verifications[0].verifier, verifier1); - assertEq(verifications[0].description, "Tree looks healthy"); - assertEq(verifications[0].proofHashes.length, 2); - assertEq(verifications[0].proofHashes[0], "proof1"); - assertEq(verifications[0].proofHashes[1], "proof2"); - assertFalse(verifications[0].isHidden); + TreeNftVerification[] memory verificationsAfterRemoval = treeNft.getTreeNftVerifiers(0); + assertEq(verificationsAfterRemoval.length, 1); + assertEq(verificationsAfterRemoval[0].verifier, verifier2); + UserDetails memory verifier1Details = treeNft.getUserProfile(verifier1); + assertEq(verifier1Details.verificationsRevoked, 1); } - function test_VerifyInvalidToken() public { - vm.prank(verifier1); - string[] memory proofHashes = new string[](0); + function test_removeVerificationTokenBurning() 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.expectRevert(InvalidTreeID.selector); - treeNft.verify(999, proofHashes, "Invalid tree"); + 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); + uint256 initialBalance = planterToken.balanceOf(planter); + assertEq(initialBalance, NUM_TREES * 1e18); + vm.prank(planter); + treeNft.removeVerification(0, verifier1); + + uint256 finalBalance = planterToken.balanceOf(planter); + assertEq(finalBalance, 0); } - function test_VerifyTwiceSameVerifier() public { - vm.prank(user1); - string[] memory initialPhotos = new string[](0); - treeNft.mintNft(LATITUDE, LONGITUDE, SPECIES, IMAGE_URI, QR_IPFS_HASH, GEO_HASH, initialPhotos); + function test_removeVerificationArrayCleanup() public { + vm.prank(planter); + string[] memory photos = new string[](1); + photos[0] = "photo1"; + treeNft.mintNft(LATITUDE, LONGITUDE, "Tree1", IMAGE_URI, QR_HASH, METADATA, GEOHASH, photos, NUM_TREES); - vm.startPrank(verifier1); - string[] memory proofHashes = new string[](1); - proofHashes[0] = "proof1"; + vm.prank(planter); + treeNft.mintNft( + LATITUDE + 1000, LONGITUDE + 1000, "Tree2", IMAGE_URI, QR_HASH, METADATA, GEOHASH, photos, NUM_TREES + ); - treeNft.verify(0, proofHashes, "First verification"); - treeNft.verify(0, proofHashes, "Second verification"); + vm.startPrank(verifier1); + string[] memory proofs = new string[](1); + proofs[0] = "proof1"; + treeNft.verify(0, proofs, "verified tree 0"); + treeNft.verify(1, proofs, "verified tree 1"); vm.stopPrank(); - TreeNftVerification[] memory verifications = treeNft.getTreeNftVerifiers(0); - assertEq(verifications.length, 1); - assertEq(verifications[0].description, "First verification"); + Tree[] memory verifiedTreesBefore = treeNft.getVerifiedTreesByUser(verifier1); + assertEq(verifiedTreesBefore.length, 2); + vm.prank(planter); + treeNft.removeVerification(0, verifier1); + Tree[] memory verifiedTreesAfter = treeNft.getVerifiedTreesByUser(verifier1); + assertEq(verifiedTreesAfter.length, 1); + assertEq(verifiedTreesAfter[0].id, 1); } - function test_MultipleVerifiers() public { - vm.prank(user1); - string[] memory initialPhotos = new string[](0); - treeNft.mintNft(LATITUDE, LONGITUDE, SPECIES, IMAGE_URI, QR_IPFS_HASH, GEO_HASH, initialPhotos); - vm.prank(verifier1); - string[] memory proofHashes1 = new string[](1); - proofHashes1[0] = "proof1"; - treeNft.verify(0, proofHashes1, "Verifier 1 says OK"); - - vm.prank(verifier2); - string[] memory proofHashes2 = new string[](1); - proofHashes2[0] = "proof2"; - treeNft.verify(0, proofHashes2, "Verifier 2 says OK"); - - assertTrue(treeNft.isVerified(0, verifier1)); - assertTrue(treeNft.isVerified(0, verifier2)); + function test_removeNonexistentVerification() 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); - TreeNftVerification[] memory verifications = treeNft.getTreeNftVerifiers(0); - assertEq(verifications.length, 2); + vm.prank(planter); + vm.expectRevert(VerificationNotFound.selector); + treeNft.removeVerification(0, verifier1); } - function test_RemoveVerification() public { - vm.prank(user1); - string[] memory initialPhotos = new string[](0); - treeNft.mintNft(LATITUDE, LONGITUDE, SPECIES, IMAGE_URI, QR_IPFS_HASH, GEO_HASH, initialPhotos); + function test_removeVerificationEmitsEvent() 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 proofHashes = new string[](1); - proofHashes[0] = "proof1"; - treeNft.verify(0, proofHashes, "Verification"); - - vm.prank(user1); + string[] memory proofs = new string[](1); + proofs[0] = "proof1"; + treeNft.verify(0, proofs, "verified"); + vm.prank(planter); vm.expectEmit(true, true, true, false); - emit TreeNft.VerificationRemoved(0, 0, verifier1); - treeNft.removeVerification(0); + emit VerificationRemoved(0, 0, verifier1); + treeNft.removeVerification(0, verifier1); + } + function test_removeVerificationWithInsufficientTokens() 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); + + 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); - assertEq(verifications.length, 1); + assertEq(verifications.length, 0); } - function test_RemoveVerificationNotOwner() public { - vm.prank(user1); - string[] memory initialPhotos = new string[](0); - treeNft.mintNft(LATITUDE, LONGITUDE, SPECIES, IMAGE_URI, QR_IPFS_HASH, GEO_HASH, initialPhotos); + function test_onlyOwnerCanRemoveVerification() 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 proofHashes = new string[](1); - proofHashes[0] = "proof1"; - treeNft.verify(0, proofHashes, "Verification"); + string[] memory proofs = new string[](1); + proofs[0] = "proof1"; + treeNft.verify(0, proofs, "verified"); - vm.prank(user2); + vm.prank(verifier2); vm.expectRevert(NotTreeOwner.selector); - treeNft.removeVerification(0); + treeNft.removeVerification(0, verifier1); } - function test_GetVerifiedTreesByUser() public { - vm.startPrank(user1); - string[] memory initialPhotos = new string[](0); - treeNft.mintNft(LATITUDE, LONGITUDE, "Tree1", IMAGE_URI, QR_IPFS_HASH, GEO_HASH, initialPhotos); - treeNft.mintNft(LATITUDE + 1, LONGITUDE + 1, "Tree2", IMAGE_URI, QR_IPFS_HASH, GEO_HASH, initialPhotos); - vm.stopPrank(); + function test_getTreeNftVerifiers() 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.startPrank(verifier1); - string[] memory proofHashes = new string[](1); - proofHashes[0] = "proof"; - treeNft.verify(0, proofHashes, "OK"); - treeNft.verify(1, proofHashes, "OK"); - vm.stopPrank(); + vm.prank(verifier1); + string[] memory proofs1 = new string[](1); + proofs1[0] = "proof1"; + treeNft.verify(0, proofs1, "verified by v1"); - Tree[] memory verifiedTrees = treeNft.getVerifiedTreesByUser(verifier1); - assertEq(verifiedTrees.length, 2); - assertEq(verifiedTrees[0].species, "Tree1"); - assertEq(verifiedTrees[1].species, "Tree2"); + vm.prank(verifier2); + string[] memory proofs2 = new string[](1); + proofs2[0] = "proof2"; + treeNft.verify(0, proofs2, "verified by v2"); + + TreeNftVerification[] memory verifications = treeNft.getTreeNftVerifiers(0); + assertEq(verifications.length, 2); + assertEq(verifications[0].verifier, verifier1); + assertEq(verifications[1].verifier, verifier2); } - function test_GetVerifiedTreesByUserPaginated() public { - vm.startPrank(user1); - string[] memory initialPhotos = new string[](0); - for (uint256 i = 0; i < 5; i++) { - treeNft.mintNft( - LATITUDE + i, - LONGITUDE + i, - string(abi.encodePacked("Tree", vm.toString(i))), - IMAGE_URI, - QR_IPFS_HASH, - GEO_HASH, - initialPhotos - ); - } - vm.stopPrank(); - vm.startPrank(verifier1); - string[] memory proofHashes = new string[](1); - proofHashes[0] = "proof"; - for (uint256 i = 0; i < 5; i++) { - treeNft.verify(i, proofHashes, "OK"); - } - vm.stopPrank(); + function test_getVerifiedTreesByUser() public { + string[] memory photos = new string[](1); + photos[0] = "photo1"; + vm.prank(planter); + treeNft.mintNft(LATITUDE, LONGITUDE, SPECIES, IMAGE_URI, QR_HASH, METADATA, GEOHASH, photos, NUM_TREES); - (Tree[] memory trees, uint256 totalCount) = treeNft.getVerifiedTreesByUserPaginated(verifier1, 0, 3); + vm.prank(planter); + treeNft.mintNft(LATITUDE, LONGITUDE, SPECIES, IMAGE_URI, QR_HASH, METADATA, GEOHASH, photos, NUM_TREES); - assertEq(trees.length, 3); - assertEq(totalCount, 5); - assertEq(trees[0].species, "Tree0"); - assertEq(trees[1].species, "Tree1"); - assertEq(trees[2].species, "Tree2"); - } + string[] memory proofs = new string[](1); + proofs[0] = "proof1"; - function test_GetTreeNftVerifiersPaginated() public { - vm.prank(user1); - string[] memory initialPhotos = new string[](0); - treeNft.mintNft(LATITUDE, LONGITUDE, SPECIES, IMAGE_URI, QR_IPFS_HASH, GEO_HASH, initialPhotos); + vm.prank(verifier1); + treeNft.verify(0, proofs, "verified tree 0"); - string[] memory proofHashes = new string[](1); - proofHashes[0] = "proof"; + vm.prank(verifier1); + treeNft.verify(1, proofs, "verified tree 1"); - address[] memory verifiers = new address[](5); - verifiers[0] = address(0x10); - verifiers[1] = address(0x11); - verifiers[2] = address(0x12); - verifiers[3] = address(0x13); - verifiers[4] = address(0x14); + Tree[] memory verifiedTrees = treeNft.getVerifiedTreesByUser(verifier1); - for (uint256 i = 0; i < 5; i++) { - vm.prank(verifiers[i]); - treeNft.verify(0, proofHashes, string(abi.encodePacked("Verification", vm.toString(i)))); - } + assertEq(verifiedTrees.length, 2); + assertEq(verifiedTrees[0].id, 0); + assertEq(verifiedTrees[1].id, 1); + } - (TreeNftVerification[] memory verifications, uint256 totalCount, uint256 visibleCount) = - treeNft.getTreeNftVerifiersPaginated(0, 0, 3); + function test_verificationIncreasesRevocationCounter() 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); - assertEq(verifications.length, 3); - assertEq(totalCount, 5); - assertEq(visibleCount, 5); - assertEq(verifications[0].verifier, verifiers[0]); - assertEq(verifications[1].verifier, verifiers[1]); - assertEq(verifications[2].verifier, verifiers[2]); - } + vm.prank(verifier1); + treeNft.registerUserProfile("Verifier1", "ipfs://profile"); - function test_MarkDead() public { - vm.prank(user1); - string[] memory initialPhotos = new string[](0); - treeNft.mintNft(LATITUDE, LONGITUDE, SPECIES, IMAGE_URI, QR_IPFS_HASH, GEO_HASH, initialPhotos); - vm.warp(block.timestamp + 366 days); - vm.prank(user1); - treeNft.markDead(0); - - Tree memory tree = treeNft.getTreeDetailsbyID(0); - assertTrue(tree.death != type(uint256).max); - assertEq(tree.death, block.timestamp); - } + vm.prank(verifier1); + string[] memory proofs = new string[](1); + proofs[0] = "proof1"; + treeNft.verify(0, proofs, "verified"); - function testMarkDeadNotOwner() public { - vm.prank(user1); - string[] memory initialPhotos = new string[](0); - treeNft.mintNft(LATITUDE, LONGITUDE, SPECIES, IMAGE_URI, QR_IPFS_HASH, GEO_HASH, initialPhotos); + vm.prank(planter); + treeNft.removeVerification(0, verifier1); - vm.prank(user2); - vm.expectRevert(NotTreeOwner.selector); - treeNft.markDead(0); + UserDetails memory userDetails = treeNft.getUserProfile(verifier1); + assertEq(userDetails.verificationsRevoked, 1); } - function testMarkDeadInvalidToken() public { - vm.prank(user1); + function test_cannotVerifyInvalidTree() public { + vm.prank(verifier1); + string[] memory proofs = new string[](1); + proofs[0] = "proof1"; vm.expectRevert(InvalidTreeID.selector); - treeNft.markDead(999); + treeNft.verify(999, proofs, "verified"); } - function testMarkDeadAlreadyDead() public { - vm.startPrank(user1); - string[] memory initialPhotos = new string[](0); - treeNft.mintNft(LATITUDE, LONGITUDE, SPECIES, IMAGE_URI, QR_IPFS_HASH, GEO_HASH, initialPhotos); - vm.warp(block.timestamp + 366 days); - treeNft.markDead(0); + function test_planterTokenCreatedOnFirstVerification() 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.expectRevert(TreeAlreadyDead.selector); - treeNft.markDead(0); - vm.stopPrank(); - } + address planterTokenBefore = treeNft.s_userToPlanterTokenAddress(verifier1); + assertEq(planterTokenBefore, address(0)); + + vm.prank(verifier1); + string[] memory proofs = new string[](1); + proofs[0] = "proof1"; + treeNft.verify(0, proofs, "verified"); - function testRegisterUserProfile() public { - vm.prank(user1); - treeNft.registerUserProfile("John Doe", "QmProfileHash"); + address planterTokenAfter = treeNft.s_userToPlanterTokenAddress(verifier1); + assertTrue(planterTokenAfter != address(0)); } - function testRegisterUserProfileAlreadyRegistered() public { - vm.startPrank(user1); - treeNft.registerUserProfile("John Doe", "QmProfileHash"); + function test_removeVerificationTwiceFails() 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.expectRevert(UserAlreadyRegistered.selector); - treeNft.registerUserProfile("Jane Doe", "QmProfileHash2"); - vm.stopPrank(); + vm.prank(verifier1); + string[] memory proofs = new string[](1); + proofs[0] = "proof1"; + treeNft.verify(0, proofs, "verified"); + + vm.prank(planter); + treeNft.removeVerification(0, verifier1); + + vm.prank(planter); + vm.expectRevert(VerificationNotFound.selector); + treeNft.removeVerification(0, verifier1); } - function testUpdateUserDetails() public { - vm.startPrank(user1); - treeNft.registerUserProfile("John Doe", "QmProfileHash"); - treeNft.updateUserDetails("John Smith", "QmNewProfileHash"); + function test_removeVerificationAfterMultipleVerifications() public { + vm.prank(planter); + string[] memory photos = new string[](1); + photos[0] = "photo1"; + 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 + ); + + vm.startPrank(verifier1); + string[] memory proofs = new string[](1); + proofs[0] = "proof1"; + treeNft.verify(0, proofs, "verified tree 0"); + treeNft.verify(1, proofs, "verified tree 1"); + treeNft.verify(2, proofs, "verified tree 2"); vm.stopPrank(); - } - function testUpdateUserDetailsNotRegistered() public { - vm.prank(user1); - vm.expectRevert(UserNotRegistered.selector); - treeNft.updateUserDetails("John Doe", "QmProfileHash"); - } + Tree[] memory verifiedTrees = treeNft.getVerifiedTreesByUser(verifier1); + assertEq(verifiedTrees.length, 3); + + vm.prank(planter); + treeNft.removeVerification(1, verifier1); + Tree[] memory remainingTrees = treeNft.getVerifiedTreesByUser(verifier1); + assertEq(remainingTrees.length, 2); - /// Helper functions - function substring(string memory str, uint256 startIndex, uint256 endIndex) internal pure returns (string memory) { - bytes memory strBytes = bytes(str); - bytes memory result = new bytes(endIndex - startIndex); - for (uint256 i = startIndex; i < endIndex; i++) { - result[i - startIndex] = strBytes[i]; + bool hasTree0 = false; + bool hasTree2 = false; + for (uint256 i = 0; i < remainingTrees.length; i++) { + if (remainingTrees[i].id == 0) hasTree0 = true; + if (remainingTrees[i].id == 2) hasTree2 = true; } - return string(result); + assertTrue(hasTree0, "Tree 0 should still be verified"); + assertTrue(hasTree2, "Tree 2 should still be verified"); + assertFalse(treeNft.isVerified(1, verifier1)); } - function testCompleteWorkflow() public { - vm.prank(user1); - treeNft.registerUserProfile("Tree Planter", "QmPlanterHash"); + function test_removeVerificationPreservesOtherVerifiers() 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); - treeNft.registerUserProfile("Tree Verifier", "QmVerifierHash"); + string[] memory proofs1 = new string[](1); + proofs1[0] = "proof1"; + treeNft.verify(0, proofs1, "verified by v1"); - vm.prank(user1); - string[] memory initialPhotos = new string[](1); - initialPhotos[0] = "initial_photo.jpg"; - treeNft.mintNft(LATITUDE, LONGITUDE, SPECIES, IMAGE_URI, QR_IPFS_HASH, GEO_HASH, initialPhotos); + vm.prank(verifier2); + string[] memory proofs2 = new string[](1); + proofs2[0] = "proof2"; + treeNft.verify(0, proofs2, "verified by v2"); - vm.prank(verifier1); - string[] memory proofHashes = new string[](1); - proofHashes[0] = "verification_proof.jpg"; - treeNft.verify(0, proofHashes, "Tree is healthy and growing well"); + address thirdVerifier = address(0x5); + vm.prank(thirdVerifier); + string[] memory proofs3 = new string[](1); + proofs3[0] = "proof3"; + treeNft.verify(0, proofs3, "verified by v3"); - Tree memory tree = treeNft.getTreeDetailsbyID(0); - assertEq(tree.species, SPECIES); - assertEq(treeNft.ownerOf(0), user1); assertTrue(treeNft.isVerified(0, verifier1)); + assertTrue(treeNft.isVerified(0, verifier2)); + assertTrue(treeNft.isVerified(0, thirdVerifier)); - TreeNftVerification[] memory verifications = treeNft.getTreeNftVerifiers(0); - assertEq(verifications.length, 1); - assertEq(verifications[0].verifier, verifier1); + TreeNftVerification[] memory allVerifications = treeNft.getTreeNftVerifiers(0); + assertEq(allVerifications.length, 3); - vm.prank(user1); - vm.warp(block.timestamp + 366 days); - treeNft.markDead(0); + vm.prank(planter); + treeNft.removeVerification(0, verifier2); - Tree memory deadTree = treeNft.getTreeDetailsbyID(0); - assertTrue(deadTree.death != type(uint256).max); - } + assertTrue(treeNft.isVerified(0, verifier1)); + assertFalse(treeNft.isVerified(0, verifier2)); + assertTrue(treeNft.isVerified(0, thirdVerifier)); - function testGasOptimization() public { - uint256 gasBefore = gasleft(); - vm.prank(user1); - string[] memory initialPhotos = new string[](0); - treeNft.mintNft(LATITUDE, LONGITUDE, SPECIES, IMAGE_URI, QR_IPFS_HASH, GEO_HASH, initialPhotos); - uint256 gasUsed = gasBefore - gasleft(); - assertTrue(gasUsed < 500000, "Minting uses too much gas"); + TreeNftVerification[] memory remainingVerifications = treeNft.getTreeNftVerifiers(0); + assertEq(remainingVerifications.length, 2); } }