Warning
This is experimental UN-AUDITED code currently under development
Closed-loop stablecoin payments + loyalty + redemption template for Sui, built on the Permissioned Asset Standard (PAS) with role-based access control from openzeppelin/contracts-sui.
A merchant deploys this template to accept stablecoin payments, automatically mint a
soulbound loyalty currency (LOYALTY) on each settlement, and let customers redeem that
loyalty for goods later — all on-chain, with PAS handling the balance/policy layer.
Two settlement flows, mirroring real-world POS. The Merchant shared object is the hub:
it stores open invoices, open vouchers, and settlement receipts in tables and owns both
flows.
- Invoice → Pay — merchant issues an invoice via
merchant::create_invoice(cap-gated) carrying line items and the snapshotted loyalty reward; it's stored in theMerchantand surfaced by ID (off-chain QR). Customer scans, sends stablecoin via PAS throughmerchant::pay, earns LOYALTY balance, and aReceiptis recorded in theMerchant. For open-loop settlement with a plain (non-PAS)Coin<C>,merchant::pay_with_cointransfers the coin directly to the payout address; same loyalty + receipt outcome. - Voucher → Redeem — customer locks LOYALTY balance in a voucher via
merchant::create_voucher(stored in theMerchant, surfaced by ID for the QR); merchant scans +merchant::redeems, the balance burns, and aReceiptis recorded.
Both invoices and vouchers carry the same Item type (variant_id + quantity + snapshot
unit_price), so the on-chain accounting is symmetric. Receipts use a generic Receipt<T>
with a flow-specific payload — Receipt<Payment> and Receipt<Redemption> — stored in the
Merchant in separate invoice_receipts / voucher_receipts tables keyed by the settling
invoice/voucher ID; per-customer history is served off-chain from the InvoicePaid /
VoucherRedeemed event stream.
Access control uses a single AccessControl<MERCHANT> registry with three operational
roles:
MerchantRole— payout address, mint config, display nameCatalogManagerRole— listings + variants CRUD + active toggleCashierRole— invoice issuance + voucher redemption
The deployer is the root holder and grants operational roles separately, so cold-storage admin keys stay out of daily POS operations.
contracts/
├── payments/ # The template (8 modules + 5 test files)
└── stablecoin-mock/ # Testnet/local-only mock PAS stablecoin (for end-to-end tests)
Both packages resolve @pas/pas and @openzeppelin-move/* via MVR — no
vendored deps. Builds require --build-env testnet (or mainnet) so MVR
knows which network to resolve against.
This project is maintained by OpenZeppelin with the goal of providing a secure and reliable starter dApp for PoS systems built on top of the Sui ecosystem.
Refer to SECURITY.md for more details.