Skip to content

Instantly share code, notes, and snippets.

@baku89
Created April 6, 2025 14:09
Show Gist options
  • Save baku89/c34ea8f1fd7233f719eaeb4f4e944245 to your computer and use it in GitHub Desktop.
Save baku89/c34ea8f1fd7233f719eaeb4f4e944245 to your computer and use it in GitHub Desktop.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.28;
import {ERC721URIStorage, ERC721} from "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol";
import {Base64} from "@openzeppelin/contracts/utils/Base64.sol";
import {Strings} from "@openzeppelin/contracts/utils/Strings.sol";
error NotOwner();
error CannotExceed100Percent();
error NoFundsSent();
error QueueEmpty();
error NoRefundAvailable();
error InvalidTokenId();
error NotTokenOwner();
error InvalidTokenURI();
contract GlispProjectLicenseV1 is ERC721URIStorage {
uint256 private _nextTokenId;
uint public refundRatio = 20;
address public owner;
uint public projectBalance;
struct Supporter {
uint totalAmountPaid;
uint refundableAmount;
uint refundCredit;
uint firstSupportTime;
bool isContributor;
bool involvedInResale;
}
mapping(address => Supporter) public supporters;
address[] public refundQueue;
event Supported(address supporter, uint amount);
event LicenseIssued(address supporter);
event RefundRatioSet(uint newRatio);
event ContributorUpdated(address supporter, bool isContributor);
event RefundClaimed(address supporter, uint amount);
constructor() ERC721("GlispProjectLicenseV1", "NPLV1") {
owner = msg.sender;
}
modifier onlyOwner() {
if (msg.sender != owner) revert NotOwner();
_;
}
function setRefundRatio(uint newRatio) external onlyOwner {
if (newRatio > 100) revert CannotExceed100Percent();
refundRatio = newRatio;
emit RefundRatioSet(newRatio);
}
function support() external payable {
if (msg.value == 0) revert NoFundsSent();
Supporter storage s = supporters[msg.sender];
if (s.totalAmountPaid == 0) {
refundQueue.push(msg.sender);
s.firstSupportTime = block.timestamp;
uint newItemId = _nextTokenId++;
_mint(msg.sender, newItemId);
s.involvedInResale = false;
emit LicenseIssued(msg.sender);
}
s.totalAmountPaid += msg.value;
s.refundableAmount = msg.value;
uint refundPool = msg.value * refundRatio / 100;
refundNextPatron(refundPool);
projectBalance += msg.value;
emit Supported(msg.sender, msg.value);
}
function refundNextPatron(uint refundPool) internal {
while (refundQueue.length > 0 && refundPool > 0) {
address supporterAddr = refundQueue[0];
Supporter storage supporter = supporters[supporterAddr];
uint owed = supporter.refundableAmount - supporter.refundCredit;
if (owed == 0 || supporter.involvedInResale) {
removeFirst(refundQueue);
continue;
}
uint actualRefund = refundPool > owed ? owed : refundPool;
supporter.refundCredit += actualRefund;
refundPool -= actualRefund;
if (supporter.refundCredit == supporter.refundableAmount) {
removeFirst(refundQueue);
}
}
}
function removeFirst(address[] storage queue) internal {
if (queue.length == 0) revert QueueEmpty();
for (uint i = 0; i < queue.length - 1; i++) {
queue[i] = queue[i + 1];
}
queue.pop();
}
function setContributor(address supporter, bool isContributor) external onlyOwner {
supporters[supporter].isContributor = isContributor;
emit ContributorUpdated(supporter, isContributor);
}
function claimRefund() external {
Supporter storage s = supporters[msg.sender];
if (s.refundCredit == 0) revert NoRefundAvailable();
uint amount = s.refundCredit;
s.refundCredit = 0;
projectBalance -= amount;
(bool success, ) = msg.sender.call{value: amount}("");
if (!success) revert();
emit RefundClaimed(msg.sender, amount);
}
function setTokenURI(uint256 tokenId, string memory _tokenURI) external {
if (_ownerOf(tokenId) == address(0)) revert InvalidTokenId();
if (ownerOf(tokenId) != msg.sender) revert NotTokenOwner();
if (bytes(_tokenURI).length == 0) revert InvalidTokenURI();
_setTokenURI(tokenId, _tokenURI);
}
function tokenURI(uint256 tokenId) public view override returns (string memory) {
// Check if token exists by verifying the owner is not zero address
if (ownerOf(tokenId) == address(0)) revert InvalidTokenId();
Supporter memory s = supporters[ownerOf(tokenId)];
bytes memory dataURI = abi.encodePacked(
'{',
'"name": "Project License #', Strings.toString(tokenId), '",',
'"description": "On-chain License for supporters.",',
'"attributes": [',
'{ "trait_type": "Contributor", "value": "', s.isContributor ? "Yes" : "No", '" },',
'{ "trait_type": "Resold", "value": "', s.involvedInResale ? "Yes" : "No", '" }',
']',
'}'
);
return string(
abi.encodePacked(
"data:application/json;base64,",
Base64.encode(dataURI)
)
);
}
function _update(address to, uint256 tokenId, address auth) internal override returns (address) {
// Get the current owner (from) before updating
address from = _ownerOf(tokenId);
address updatedFrom = super._update(to, tokenId, auth);
if (from != address(0) && to != address(0)) {
supporters[from].involvedInResale = true;
supporters[from].refundableAmount = 0;
supporters[from].refundCredit = 0;
supporters[from].isContributor = false;
supporters[to].involvedInResale = true;
}
return updatedFrom;
}
receive() external payable {}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment