Autonomous Self-Refunding Merchant Settlement Standard (Sovereign Checkout)

Abstract

This proposal specifies an open, non-custodial merchant settlement interface and escrow architecture. Current retail cryptocurrency payment gateways (e.g., BVNK, BitPay) instruct payers to transfer ERC-20/TRC-20 tokens directly to raw, unmonitored external addresses. When an underpayment or fee shortfall occurs, centralized sweepers absorb funds into proprietary corporate master vaults while backend webhooks flag orders as incomplete, trapping payer funds with zero protocol-level remediation.

This standard specifies an immutable escrow and checkout contract interface where:

  1. Invoices settle atomically upon exact or cumulative payment;
  2. Underpaid funds remain locked in smart contract escrow with zero merchant or gateway custody; and
  3. Payers can autonomously trigger a deterministic refund upon expiration without merchant consent or intermediary intervention.

Motivation

Retail consumers and merchants are currently forced to rely on centralized payment processors that operate as opaque financial black boxes:

  • The Shortfall Trap: Network fee deductions often result in minor underpayments (e.g., 57.33 USDT received vs 64.66 USDT expected).
  • Unjust Enrichment: Centralized gateways sweep underpaid tokens into multi-million dollar master vaults to preserve liquidity, while flagging orders as unfulfilled.
  • Support Stonewalling: Merchants disclaim liability (“address not provided by us”), processors refuse retail consumers (“we only service B2B merchants”), and trapped funds become unallocated corporate float.

By standardizing a smart contract settlement interface, Web3 wallets (e.g., MetaMask, Trust Wallet) can warn users against sending funds to raw deposit addresses and instead mandate interactions with verifiable, self-refunding smart contract checkouts.

Specification

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

interface IERC_SovereignCheckout {
    enum InvoiceStatus { Active, Settled, Refunded, Cancelled }

    struct Invoice {
        bytes32 invoiceId;
        address merchant;
        address payer;
        address refundAddress;
        address token;
        uint256 expectedAmount;
        uint256 amountPaid;
        uint256 expiryTimestamp;
        InvoiceStatus status;
    }

    event InvoiceCreated(bytes32 indexed invoiceId, address indexed merchant, address indexed token, uint256 expectedAmount, uint256 expiryTimestamp);
    event PaymentReceived(bytes32 indexed invoiceId, address indexed payer, uint256 amount, uint256 totalPaid, uint256 remaining);
    event InvoiceSettled(bytes32 indexed invoiceId, address indexed merchant, uint256 totalAmount);
    event AutonomousRefundClaimed(bytes32 indexed invoiceId, address indexed recipient, uint256 amountRefunded);
    event InvoiceCancelled(bytes32 indexed invoiceId);

    function createInvoice(bytes32 invoiceId, address token, uint256 expectedAmount, uint256 durationSeconds) external;
    function payInvoice(bytes32 invoiceId, uint256 amount, address refundAddress) external;
    function claimRefund(bytes32 invoiceId) external;
    function merchantCancelAndRefund(bytes32 invoiceId) external;
}

Rationale

  • No Merchant Custody Before Settlement: To protect the payer, tokens deposited into the escrow MUST NOT be transferable by the merchant unless amountPaid >= expectedAmount.
  • Autonomous Refund Path: The claimRefund function MUST be permissionless to allow automated keeper networks or payers to reclaim tokens immediately once block.timestamp > expiryTimestamp.

Reference Implementation & Repository

Open-source reference implementation and full technical manifesto available on GitHub:

We welcome community feedback on event indexing, gas optimization, and wallet abstraction integration.

What prevents an attacker from making the first dust payment and setting their own refundAddress? payInvoice then accepts funds from other callers without checking them against inv.payer. If the invoice remains underpaid until expiry then claimRefund sends everyone’s deposits to the attacker’s address. Should invoices bind the intended payer before accepting funds or track refundable balances separately for each contributor?

Great catch and excellent security observation!

You have highlighted an important griefing/front-running vector in the draft implementation: allowing the first unauthenticated caller to bind inv.refundAddress for subsequent contributors creates a classic “first-funder trap” on public mempools.

To address this cleanly without breaking decentralized checkout flexibility, we are updating the specification with two patterns:

1. Per-Payer Refund Ledger (Multi-Contributor Safety)

Instead of a single global refundAddress on the invoice, the contract tracks balances per caller:

// invoiceId => payerAddress => amountPaid
mapping(bytes32 => mapping(address => uint256)) public payerDeposits;

If an invoice expires without reaching full settlement, claimRefund(invoiceId) only returns the exact cumulative amount deposited by msg.sender (or an address explicitly mapped to their signature). A dust attacker would only be able to reclaim their own dust, with zero access to funds contributed by the genuine payer.

2. Intended Payer Binding (Strict 1-to-1 Invoices)

For standard retail/e-commerce checkouts where the merchant knows the customer’s wallet or account beforehand, createInvoice can optionally bind an expectedPayer. Any payInvoice call where msg.sender != expectedPayer reverts immediately at the consensus layer.

I am pushing an update to the reference implementation repository reflecting the per-payer balance ledger to guarantee deterministic, isolated refunds for each contributor.

Thank you for the sharp peer review!