Signed Off-Chain Receipt Vault

signed-off-chain-receipt-vault · receipt-attestation · 1459 B runtime · registry-info

scores

source

pragma solidity ^0.8.13;

interface IERC1271 {
    function isValidSignature(bytes32 hash, bytes calldata signature) external view returns (bytes4);
}

contract ReceiptVault {
    bytes32 private constant EIP712_DOMAIN_TYPEHASH =
        keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)");
    bytes32 private constant RECEIPT_TYPEHASH =
        keccak256("Receipt(address claimant,bytes32 actionId,bytes32 payloadHash)");

    bytes32 private immutable domainSeparator;
    mapping(bytes32 => bool) public receiptHashes;

    error InvalidSignature();
    error ReceiptAlreadyStored();

    event ReceiptStored(
        address indexed claimant,
        bytes32 indexed actionId,
        bytes32 indexed payloadHash,
        bytes32 receiptHash
    );

    struct Receipt {
        address claimant;
        bytes32 actionId;
        bytes32 payloadHash;
    }

    constructor() {
        domainSeparator = keccak256(
            abi.encode(
                EIP712_DOMAIN_TYPEHASH,
                keccak256("ReceiptVault"),
                keccak256("1"),
                block.chainid,
                address(this)
            )
        );
    }

    function submitReceipt(Receipt calldata receipt, bytes calldata signature) external {
        bytes32 receiptHash = keccak256(abi.encode(receipt));
        if (receiptHashes[receiptHash]) revert ReceiptAlreadyStored();

        bytes32 structHash = keccak256(
            abi.encode(RECEIPT_TYPEHASH, receipt.claimant, receipt.actionId, receipt.payloadHash)
        );
        bytes32 digest = keccak256(abi.encodePacked("\x19\x01", domainSeparator, structHash));

        _verify(receipt.claimant, digest, signature);

        receiptHashes[receiptHash] = true;
        emit ReceiptStored(receipt.claimant, receipt.actionId, receipt.payloadHash, receiptHash);
    }

    function _verify(address claimant, bytes32 digest, bytes calldata signature) internal view {
        if (claimant.code.length > 0) {
            if (
                IERC1271(claimant).isValidSignature(digest, signature) != IERC1271.isValidSignature.selector
            ) revert InvalidSignature();
        } else {
            (bytes32 r, bytes32 s, uint8 v) = _split(signature);
            address recovered = ecrecover(digest, v, r, s);
            if (recovered == address(0) || recovered != claimant) revert InvalidSignature();
        }
    }

    function _split(bytes calldata sig) private pure returns (bytes32 r, bytes32 s, uint8 v) {
        if (sig.length != 65) revert InvalidSignature();
        assembly {
            r := calldataload(sig.offset)
            s := calldataload(add(sig.offset, 0x20))
            v := byte(0, calldataload(add(sig.offset, 0x40)))
        }
    }
}

published

published on testnet · 0xCf85f3Ad69eF421359dED0a3fd30D24FE2d299Db · tx 0x3ea06c07e60e97ad… · base (84532) · testnet · source as deployed