Skip to content

feat: add issuer-signed transfer-restriction override (#589) - #710

Open
sir-emmy1 wants to merge 1 commit into
RevoraOrg:masterfrom
sir-emmy1:feat/transfer-restriction-override-v2
Open

feat: add issuer-signed transfer-restriction override (#589)#710
sir-emmy1 wants to merge 1 commit into
RevoraOrg:masterfrom
sir-emmy1:feat/transfer-restriction-override-v2

Conversation

@sir-emmy1

Copy link
Copy Markdown
Contributor

Closes #589

Summary

Implements one-shot transfer-restriction override attestation per issue #589. An issuer signs an OverrideAttestation struct with their registered ed25519 key to authorize a single (from, to, amount_bps) transfer that bypasses all category-cap and whitelist restrictions.

Contract changes

New types

  • OverrideAttestation (#[contracttype]) — canonical XDR-serialised payload bound to (version=1, contract, issuer, namespace, token, from, to, amount_bps, nonce, expiry). Binding every field prevents cross-contract and cross-tuple replay.

New storage key

  • DataKey2::TransferOverrideNonce(Address, u64) — per-issuer one-shot nonce consumed atomically before share mutation. Presence = burned.

New error codes

  • OverrideAttestationInvalid = 76 — signer not registered or sig invalid.
  • OverrideAlreadyConsumed = 77 — nonce already burned (replay attempt).

New event

  • xfer_ovrd (EVENT_TRANSFER_OVERRIDE_APPLIED) — emitted on every successful call. Topics: (xfer_ovrd, issuer, namespace, token). Data: (from, to, amount_bps, nonce). Sufficient for audit reconstruction.

New entry point

  • ransfer_with_override(issuer, namespace, token, from, to, amount_bps, nonce, expiry, sig) — 10 guards enforced in order:
  1. Global freeze / pause
  2. issuer.require_auth() (dual-auth)
  3. amount_bps > 0
  4. from != to
  5. Offering exists
  6. Offering not frozen
  7. from/to not blacklisted
  8. expiry >= ledger timestamp
  9. Nonce not yet consumed (OverrideAlreadyConsumed)
  10. ed25519 signature valid (host trap on mismatch) Nonce burned BEFORE share mutation; event emitted AFTER.

Supporting fixes (pre-existing bugs, fixed as needed to compile)

  • Fixed duplicate PauseState enum definition
  • Fixed duplicate MinRevenueThreshold/DepositedRevenue in DataKey2
  • Fixed duplicate discriminants in RevoraError (56, 58, 51)
  • Fixed stray 'None => {}' match arm and extra ')' syntax errors
  • Fixed mismatched impl block brace in migration block
  • Fixed inner attribute position in security_assertions.rs
  • Fixed duplicate test module declarations (test_time_windows, test_close_period)
  • Fixed duplicate imports in test_storage_layout_version.rs
  • Fixed named enum variant fields in vesting.rs (Soroban unsupported)
  • Fixed symbol_short strings exceeding 9-char limit
  • Added missing storage layout registry entries to tools/storage_layout_schema.rs

Tests — src/test_transfer_with_override.rs

13 test functions covering:

  • blocked_when_frozen
  • blocked_when_paused
  • zero_amount_rejected
  • self_transfer_rejected
  • unknown_offering_rejected
  • blocked_when_offering_frozen
  • blacklisted_from_rejected
  • blacklisted_to_rejected
  • expired_attestation_rejected
  • nonce_replay_rejected (one-shot enforcement)
  • unregistered_signer_rejected
  • wrong_tuple_sig_rejected (mismatched tuple causes host trap)
  • insufficient_shares_rejected
  • happy_path_full_transfer
  • happy_path_partial_transfer
  • event_payload_correct (audit trail topics + data)
  • shares_updated_correctly (storage invariants + sum conservation)
  • override_bypasses_category_cap
  • different_nonces_are_independent

Security notes

  • Dual-auth: issuer must sign both the Soroban transaction and the off-chain attestation, preventing a stolen sig from being replayed by another caller.
  • One-shot nonce burned before share mutation; replay is permanently rejected even if the share update subsequently panics (host aborts tx, nonce stays).
  • Signature covers contract address preventing cross-contract replay.
  • Blacklist enforcement is preserved; override does not bypass blacklist.

## Summary
Implements one-shot transfer-restriction override attestation per issue RevoraOrg#589.
An issuer signs an OverrideAttestation struct with their registered ed25519 key
to authorize a single (from, to, amount_bps) transfer that bypasses all
category-cap and whitelist restrictions.

## Contract changes

### New types
- OverrideAttestation (#[contracttype]) — canonical XDR-serialised payload
  bound to (version=1, contract, issuer, namespace, token, from, to,
  amount_bps, nonce, expiry). Binding every field prevents cross-contract
  and cross-tuple replay.

### New storage key
- DataKey2::TransferOverrideNonce(Address, u64) — per-issuer one-shot
  nonce consumed atomically before share mutation. Presence = burned.

### New error codes
- OverrideAttestationInvalid = 76 — signer not registered or sig invalid.
- OverrideAlreadyConsumed   = 77 — nonce already burned (replay attempt).

### New event
- xfer_ovrd (EVENT_TRANSFER_OVERRIDE_APPLIED) — emitted on every
  successful call. Topics: (xfer_ovrd, issuer, namespace, token).
  Data: (from, to, amount_bps, nonce). Sufficient for audit reconstruction.

### New entry point
- 	ransfer_with_override(issuer, namespace, token, from, to, amount_bps,
  nonce, expiry, sig) — 10 guards enforced in order:
  1. Global freeze / pause
  2. issuer.require_auth() (dual-auth)
  3. amount_bps > 0
  4. from != to
  5. Offering exists
  6. Offering not frozen
  7. from/to not blacklisted
  8. expiry >= ledger timestamp
  9. Nonce not yet consumed (OverrideAlreadyConsumed)
  10. ed25519 signature valid (host trap on mismatch)
  Nonce burned BEFORE share mutation; event emitted AFTER.

## Supporting fixes (pre-existing bugs, fixed as needed to compile)
- Fixed duplicate PauseState enum definition
- Fixed duplicate MinRevenueThreshold/DepositedRevenue in DataKey2
- Fixed duplicate discriminants in RevoraError (56, 58, 51)
- Fixed stray 'None => {}' match arm and extra ')' syntax errors
- Fixed mismatched impl block brace in migration block
- Fixed inner attribute position in security_assertions.rs
- Fixed duplicate test module declarations (test_time_windows, test_close_period)
- Fixed duplicate imports in test_storage_layout_version.rs
- Fixed named enum variant fields in vesting.rs (Soroban unsupported)
- Fixed symbol_short strings exceeding 9-char limit
- Added missing storage layout registry entries to tools/storage_layout_schema.rs

## Tests — src/test_transfer_with_override.rs
13 test functions covering:
- blocked_when_frozen
- blocked_when_paused
- zero_amount_rejected
- self_transfer_rejected
- unknown_offering_rejected
- blocked_when_offering_frozen
- blacklisted_from_rejected
- blacklisted_to_rejected
- expired_attestation_rejected
- nonce_replay_rejected  (one-shot enforcement)
- unregistered_signer_rejected
- wrong_tuple_sig_rejected  (mismatched tuple causes host trap)
- insufficient_shares_rejected
- happy_path_full_transfer
- happy_path_partial_transfer
- event_payload_correct  (audit trail topics + data)
- shares_updated_correctly  (storage invariants + sum conservation)
- override_bypasses_category_cap
- different_nonces_are_independent

## Security notes
- Dual-auth: issuer must sign both the Soroban transaction and the off-chain
  attestation, preventing a stolen sig from being replayed by another caller.
- One-shot nonce burned before share mutation; replay is permanently rejected
  even if the share update subsequently panics (host aborts tx, nonce stays).
- Signature covers contract address preventing cross-contract replay.
- Blacklist enforcement is preserved; override does not bypass blacklist.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add transfer-restriction override attestation allowing issuer to whitelist specific transfers

1 participant