Revocable Soulbound Receipt NFT

revocable-soulbound-receipt-nft · receipt-attestation · 3365 B runtime · registry-info

scores

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)));
    }
}