Carbon-Offset Tracker NFT

What it does: Generates NFTs tied to verified climate projects. When users buy offsets, the NFTs update to reflect total carbon impact.

Why it matters: Adds transparency and accountability to environmental claims.

How it works:

  • Each NFT links to on-chain proofs and off-chain oracles.

  • Minting only occurs after independent validation.

  • Burning NFTs corresponds to “retiring” offsets from circulation.

  • Public dashboards verify authenticity.

      // SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

/**
 * Carbon Offset Tracker NFT
 *
 * PURPOSE:
 * - Each NFT represents an account tied to real-world offsets.
 * - Verifiers (trusted entities) append verified offset records.
 * - Token metadata (URI) can point to detailed evidence.
 *
 * IMPORTANT:
 * - Integrate with credible registries and auditors.
 * - Use off-chain oracles to bridge certifications to on-chain proofs.
 * - Audit thoroughly before mainnet deployment.
 */

import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";

contract CarbonOffsetNFT is ERC721, Ownable, ReentrancyGuard {
    struct OffsetRecord {
        uint256 amountTons;         // metric tons CO2e
        string projectId;           // registry/project identifier
        string evidenceURI;         // link to documentation/certificates
        uint256 timestamp;          // when added (block time)
        address verifier;           // who verified the offset
    }

    // Token-level data
    struct TokenData {
        uint256 totalRetired;       // lifetime tons retired
        bool retired;               // optional permanent retirement flag
        string baseMetadataURI;     // metadata base for token
    }

    // Mappings
    mapping(uint256 => TokenData) private tokens;
    mapping(uint256 => OffsetRecord[]) private offsetHistory;

    // Role management — approved verifiers
    mapping(address => bool) public approvedVerifiers;

    // Incrementing token ID
    uint256 private nextTokenId = 1;

    // Events
    event VerifierAdded(address indexed verifier);
    event VerifierRemoved(address indexed verifier);
    event TokenMinted(address indexed to, uint256 indexed tokenId, string metadataURI);
    event OffsetRecorded(
        uint256 indexed tokenId,
        uint256 amountTons,
        string projectId,
        string evidenceURI,
        address indexed verifier
    );
    event TokenRetired(uint256 indexed tokenId, uint256 totalRetired);

    constructor() ERC721("Carbon Offset NFT", "CO2NFT") {}

    // --------- VERIFIER CONTROL (OWNER) ----------

    function addVerifier(address verifier) external onlyOwner {
        require(!approvedVerifiers[verifier], "Already verifier");
        approvedVerifiers[verifier] = true;
        emit VerifierAdded(verifier);
    }

    function removeVerifier(address verifier) external onlyOwner {
        require(approvedVerifiers[verifier], "Not verifier");
        approvedVerifiers[verifier] = false;
        emit VerifierRemoved(verifier);
    }

    // --------- MINTING ----------

    /**
     * @dev Mint NFT to recipient. baseMetadataURI can point to IPFS/HTTPS JSON.
     */
    function mint(address to, string calldata baseMetadataURI)
        external
        onlyOwner
        returns (uint256)
    {
        uint256 tokenId = nextTokenId;
        nextTokenId++;

        _safeMint(to, tokenId);

        tokens[tokenId] = TokenData({
            totalRetired: 0,
            retired: false,
            baseMetadataURI: baseMetadataURI
        });

        emit TokenMinted(to, tokenId, baseMetadataURI);
        return tokenId;
    }

    // --------- ADD OFFSET RECORD (VERIFIERS ONLY) ----------

    function recordOffset(
        uint256 tokenId,
        uint256 amountTons,
        string calldata projectId,
        string calldata evidenceURI
    ) external nonReentrant {
        require(_exists(tokenId), "Token does not exist");
        require(approvedVerifiers[msg.sender], "Not authorized verifier");
        require(!tokens[tokenId].retired, "Token retired");
        require(amountTons > 0, "Amount must be > 0");

        tokens[tokenId].totalRetired += amountTons;

        offsetHistory[tokenId].push(
            OffsetRecord({
                amountTons: amountTons,
                projectId: projectId,
                evidenceURI: evidenceURI,
                timestamp: block.timestamp,
                verifier: msg.sender
            })
        );

        emit OffsetRecorded(tokenId, amountTons, projectId, evidenceURI, msg.sender);
    }

    // --------- RETIRE TOKEN (OPTIONAL) ----------

    /**
     * @dev Permanently mark NFT as retired; cannot receive more offsets.
     * Can be invoked by token owner or contract owner.
     */
    function retireToken(uint256 tokenId) external {
        require(_exists(tokenId), "Token does not exist");
        require(
            ownerOf(tokenId) == msg.sender || msg.sender == owner(),
            "Not authorized"
        );
        require(!tokens[tokenId].retired, "Already retired");

        tokens[tokenId].retired = true;

        emit TokenRetired(tokenId, tokens[tokenId].totalRetired);
    }

    // --------- METADATA ----------

    function tokenURI(uint256 tokenId)
        public
        view
        override
        returns (string memory)
    {
        require(_exists(tokenId), "Token does not exist");
        return tokens[tokenId].baseMetadataURI;
    }

    function getTokenData(uint256 tokenId)
        external
        view
        returns (TokenData memory)
    {
        require(_exists(tokenId), "Token does not exist");
        return tokens[tokenId];
    }

    function getOffsetHistory(uint256 tokenId)
        external
        view
        returns (OffsetRecord[] memory)
    {
        require(_exists(tokenId), "Token does not exist");
        return offsetHistory[tokenId];
    }

    // --------- ADMIN UTILITIES ----------

    function updateBaseURI(uint256 tokenId, string calldata newURI)
        external
        onlyOwner
    {
        require(_exists(tokenId), "Token does not exist");
        tokens[tokenId].baseMetadataURI = newURI;
    }
}