Revocable Soulbound Receipt NFT
revocable-soulbound-receipt-nft · receipt-attestation · 3365 B runtime · registry-info
scores
- usefulness: 8
- safety: 6
- liveness: 9
- authenticity: 7
- extensibility: 6
- bytecodeDiscipline: 9
- practical: 8
source
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;
contract RevocableSoulboundReceipt {
address public immutable issuer;
uint256 public constant DISPUTE_WINDOW = 7 days;
mapping(address => mapping(uint256 => uint256)) private _balances;
mapping(uint256 => address) private _agentOf;
mapping(uint256 => uint256) private _mintTime;
event TransferSingle(address indexed operator, address indexed from, address indexed to, uint256 id, uint256 value);
event TransferBatch(address indexed operator, address indexed from, address indexed to, uint256[] ids, uint256[] values);
event ApprovalForAll(address indexed account, address indexed operator, bool approved);
constructor() {
issuer = msg.sender;
}
modifier onlyIssuer() {
require(msg.sender == issuer, "not issuer");
_;
}
function balanceOf(address account, uint256 id) public view returns (uint256) {
return _balances[account][id];
}
function balanceOfBatch(address[] calldata accounts, uint256[] calldata ids) external view returns (uint256[] memory) {
require(accounts.length == ids.length, "length mismatch");
uint256[] memory result = new uint256[](accounts.length);
for (uint256 i = 0; i < accounts.length; i++) {
result[i] = _balances[accounts[i]][ids[i]];
}
return result;
}
function setApprovalForAll(address, bool) external pure {
revert("soulbound");
}
function isApprovedForAll(address, address) external pure returns (bool) {
return false;
}
function safeTransferFrom(address, address, uint256, uint256, bytes calldata) external pure {
revert("soulbound");
}
function safeBatchTransferFrom(address, address, uint256[] calldata, uint256[] calldata, bytes calldata) external pure {
revert("soulbound");
}
function mint(address to, bytes32 actionHash) external onlyIssuer returns (uint256 tokenId) {
require(to != address(0), "zero to");
tokenId = uint256(keccak256(abi.encodePacked(issuer, actionHash, to)));
require(_balances[to][tokenId] == 0, "already minted");
_balances[to][tokenId] = 1;
_agentOf[tokenId] = to;
_mintTime[tokenId] = block.timestamp;
emit TransferSingle(msg.sender, address(0), to, tokenId, 1);
}
function revoke(
uint256 tokenId,
address challenger,
uint8 v,
bytes32 r,
bytes32 s
) external onlyIssuer {
uint256 mintTime = _mintTime[tokenId];
require(mintTime != 0, "not minted");
require(block.timestamp <= mintTime + DISPUTE_WINDOW, "window expired");
address agent = _agentOf[tokenId];
require(agent != address(0), "no agent");
bytes32 message = keccak256(abi.encodePacked(address(this), tokenId));
address signer = ecrecover(
keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", message)),
v, r, s
);
require(signer != address(0) && signer == challenger, "invalid sig");
_balances[agent][tokenId] = 0;
delete _mintTime[tokenId];
delete _agentOf[tokenId];
emit TransferSingle(msg.sender, agent, address(0), tokenId, 1);
}
function computeTokenId(address to, bytes32 actionHash) external view returns (uint256) {
return uint256(keccak256(abi.encodePacked(issuer, actionHash, to)));
}
}