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;
}
}
Build and Grow By Nam Le Thanh