This implementation adds per-user nonce tracking for off-chain signed payment authorizations in the social payment contract, preventing replay attacks.
File: contracts/contracts/social_payment/src/lib.rs
- Added
NONCE_KEY_PREFIXconstant for storage key prefix - Added
Nonce(Address)variant toDataKeyenum to store per-user nonces in persistent storage
Added two new error types to the Error enum:
NonceAlreadyUsed = 2: Returned when a nonce has been used or doesn't match current nonceInvalidSignature = 3: Returned when signature verification fails
Added SignedPaymentAuth struct containing:
sender: Address- The account authorizing the paymentreceiver: Address- The payment recipienttoken: Address- The token contract addressamount: i128- Payment amountmemo: String- Payment memovisibility: Visibility- Payment visibility (Public/Friends/Private)nonce: u64- Sequential nonce to prevent replay attacks
- Returns the current nonce for a given user
- Returns 0 for users who have never made a signed payment
- Nonces are stored in persistent storage
pay_with_signature(env: Env, auth: SignedPaymentAuth, sender_pubkey: BytesN<32>, signature: BytesN<64>) -> Result<(), Error>
- Executes a payment using an off-chain Ed25519 signature authorization
- Nonce validation: Checks that
auth.noncematches the sender's current on-chain nonce - Signature verification: Verifies the Ed25519 signature against the provided public key
- Nonce increment: Increments the sender's nonce after successful execution
- No require_auth(): Authorization is proven by signature, not by Soroban auth invocation
- Returns
Error::NonceAlreadyUsedif nonce doesn't match - Returns
Error::InvalidSignatureif signature verification fails (via panic from crypto module)
- Each user maintains an independent nonce counter
- Nonces must be used sequentially (0, 1, 2, 3, ...)
- Once a nonce is consumed, it cannot be reused
- Attempting to use a past nonce returns
NonceAlreadyUsederror - Attempting to use a future nonce also returns
NonceAlreadyUsederror
- Uses Ed25519 signature verification from Soroban's crypto module
- Requires both the signature and the sender's public key
- Verifies that the signature was created by the private key corresponding to the public key
- The authorization payload (containing all payment details) is serialized to XDR and signed
- Nonces are stored in persistent storage using
DataKey::Nonce(Address) - Each address has its own independent nonce counter
- Storage is updated atomically after successful signature verification
// Off-chain: sender creates and signs authorization
let auth = SignedPaymentAuth {
sender: sender_address,
receiver: receiver_address,
token: naira_token_address,
amount: 1000,
memo: String::from_str(&env, "Payment memo"),
visibility: Visibility::Private,
nonce: 0, // First payment for this sender
};
// Sender signs the serialized auth with their private key
let message = auth.to_xdr(&env);
let signature = sign_with_private_key(message); // Off-chain signing
// On-chain: anyone can submit the signed authorization
let result = client.pay_with_signature(
&auth,
&sender_public_key,
&signature
);
// Next payment must use nonce = 1Added comprehensive test coverage:
test_get_nonce_returns_zero_for_new_user: Verifies initial nonce is 0test_nonce_increments_after_successful_signed_payment: Confirms nonce incrementstest_signed_payment_rejects_reused_nonce: Ensures nonces cannot be reusedtest_signed_payment_rejects_future_nonce: Prevents skipping noncestest_signed_payment_different_users_independent_nonces: Validates isolation between users
-
Public Key to Address Mapping: The current implementation requires the caller to provide the sender's public key. In production, you should verify that this public key corresponds to the
auth.senderaddress to prevent unauthorized payments. -
Off-Chain Signing: The signing process happens off-chain. The sender uses their private key to sign the authorization payload, then anyone can submit it on-chain with the signature.
-
Gas Payment: Since the sender doesn't call
require_auth(), a third party (relayer) can submit the signed transaction and pay the gas fees, enabling gasless transactions for end users. -
Nonce Management: The frontend/wallet must track the current nonce for each user to construct valid authorizations. The
get_nonce()function can be queried to get the current value.
- ✅ Store
Nonce(Address)in storage - ✅ Increment user nonce on successful payment execution
- ✅ Reject reused nonces with proper error handling
contracts/contracts/social_payment/src/lib.rs- Main implementation file
No other files needed modification as this is a self-contained feature addition to the social payment contract.