Created
April 6, 2025 14:09
-
-
Save baku89/c34ea8f1fd7233f719eaeb4f4e944245 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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