Skip to content

PSwap Contract#2636

Open
VAIBHAVJINDAL3012 wants to merge 17 commits into0xMiden:nextfrom
inicio-labs:pswap-note
Open

PSwap Contract#2636
VAIBHAVJINDAL3012 wants to merge 17 commits into0xMiden:nextfrom
inicio-labs:pswap-note

Conversation

@VAIBHAVJINDAL3012
Copy link
Copy Markdown

  • Add PSWAP (partially-fillable swap) note to miden-standards — a new note script that enables partial
    fills, creator reclaim, and inflight cross-swaps for decentralized asset exchange

    • MASM script (pswap.masm) uses 18-item KEY+VALUE storage layout, proportional price calculation with
      1e5 precision factor, and an early-return optimization for full fills to avoid integer truncation
    • Rust library (PswapNote) provides note creation, output note construction (P2ID payback + optional
      remainder), storage parsing, and calculate_offered_for_requested convenience method
    • 12 integration tests covering full fill, private fill, partial fill, inflight cross-swap, creator
      reclaim, invalid input rejection, multiple fill amounts, non-exact ratios, fuzz cases, and chained
      partial fills

    Test plan

    • cargo build -p miden-standards — MASM compiles into standards library
    • cargo test -p miden-standards pswap — 4 unit tests (script loading, tag construction, output
      calculation, storage parsing)
    • cargo test -p miden-testing pswap — 12 integration tests (full/partial/private fills, cross-swaps,
      reclaim, fuzz, chained fills)

Copy link
Copy Markdown
Contributor

@PhilippGackstatter PhilippGackstatter left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good, thank you! I primarily looked at the Rust code for now and left a few comments, but on a high level:

  • I think we should use the builder-style way of creating PswapNote and PswapNoteStorage, since both have quite a few fields.
  • Some lints are failing (see CI or run make lint locally).
  • If this PR is ready for review, can we put it ouf of draft mode?
  • Nits: Would be nice to use attachment instead of aux, storage instead of inputs, sender instead of creator, etc.

Comment on lines +316 to +323
pub fn create<R: FeltRng>(
creator_account_id: AccountId,
offered_asset: Asset,
requested_asset: Asset,
note_type: NoteType,
note_attachment: NoteAttachment,
rng: &mut R,
) -> Result<Note, NoteError> {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As motivated in #2283, I think we should get rid of this method and steer users towards using the builder methods to create notes.

This requires adding a public build method which we could do like this:

// Use a custom build method for custom validation.
impl<S: IsComplete> PswapNoteBuilder<S> {
    pub fn build(self) -> Result<PswapNote, NoteError> {
        let note = self.build_internal();

        // TODO: Validate note if needed.
        // We should probably validate the number of assets here, for instance.

        Ok(note)
    }
}

Vaibhav Jindal and others added 14 commits March 25, 2026 09:14
…MS, remove create_ wrappers, add builder finish_fn

- Rename swapp_tag -> pswap_tag and SWAPp -> PSWAP throughout
- Rename NUM_ITEMS -> NUM_STORAGE_ITEMS for clarity
- Remove create_p2id_payback_note and create_remainder_note wrappers,
  make build_ functions public instead
- Compute p2id_tag inside build_p2id_payback_note from self.storage
- Add #[builder(finish_fn(vis = "", name = build_internal))] to PswapNote
…ctly

Replace all test helper wrappers with direct calls to library functions:
- create_pswap_note -> PswapNote::create()
- create_expected_pswap_p2id_note + create_expected_pswap_remainder_note -> pswap.execute()
- build_pswap_storage -> PswapNoteStorage::from_parts()
- Remove make_pswap_tag, make_note_assets, make_note_args, compute_p2id_tag_*
- Inline calculate_output_amount as PswapNote::calculate_output_amount()
- Replace storage layout list with markdown table
- Remove trivial "Returns the X" docs on simple getters
- Add # Errors sections where relevant
- Rewrite method docs to describe intent, not implementation
- Add one-line docs on From/TryFrom conversion impls
- Tighten PswapNote struct doc
- Rename RpoRandomCoin to RandomCoin (miden-crypto 0.23 rename)
- Store full ASSET_KEY instead of prefix/suffix to preserve callback
  metadata in faucet_id_suffix_and_metadata
- Replace create_fungible_asset calls with direct ASSET_KEY + manual
  ASSET_VALUE construction, avoiding the new enable_callbacks parameter
- Update hardcoded P2ID script root to match current P2idNote::script_root()
- Replace hardcoded P2ID script root with procref.p2id::main for
  compile-time resolution
- Default to full fill when both input and inflight amounts are zero
- Replace magic address 4000 with named P2ID_RECIPIENT_STORAGE constant
- Remove step numbering from comments, fix memory layout docs
- Rename P2ID_RECIPIENT_STORAGE to P2ID_RECIPIENT_SUFFIX/PREFIX
- Add METADATA_HEADER word layout docs with source reference
- Document attachment_scheme parameter in set_word_attachment call
- Add stack views after condition checks
…lper, simplify build_p2id_payback_note

Rename requested_key/requested_value/requested_amount to
requested_asset_key/requested_asset_value/requested_asset_amount for
clarity. Extract offered_asset_amount() helper on PswapNote to
deduplicate offered asset extraction. Simplify build_p2id_payback_note
to take fill_amount: u64 instead of aux_word: Word, constructing the
aux word internally.
- Use NoteTag::default() instead of NoteTag::new(0)
- Change swap_count from u64 to u16 with safe try_into conversion
- Rename p2id_tag to payback_note_tag, p2id_payback_note to payback_note
- Rename build_ prefix to create_ for consistency
- Rename aux_word to attachment_word
- Replace Felt::new with Felt::from where possible
- Rename inputs to note_storage in TryFrom impl
- Make create_payback_note and create_remainder_pswap_note private
- Add offered_faucet_id helper to replace unreachable!()
- Remove PswapNote::create, add public build() with validation on builder
- Add bon::Builder to PswapNoteStorage, remove new() and from_parts()
- Remove payback_note_tag field, compute from creator_account_id on the fly
- Fix clippy warnings (useless conversions, needless borrows)
- Update all unit and integration tests to use builder pattern
- Compare full faucet_id instead of just prefix in build validation
- Rename swap_note to pswap_note in integration tests for consistency
- Extract PswapNoteStorage builder into separate let bindings
- Replace manual current_swap_count counter with enumerate index
- Fix clippy useless_conversion and needless_borrow warnings
Copy link
Copy Markdown
Contributor

@PhilippGackstatter PhilippGackstatter left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the updates! Looks good overall.

I left a few comments regarding the core logic in Rust. I will take a look at the MASM implementation next.

Comment on lines +27 to +36
# PSWAP Note Storage (18 items loaded at address 0)
# REQUESTED_ASSET_WORD_INPUT is the base address of an 8-cell block:
# addresses 0x0000-0x0003 = ASSET_KEY, 0x0004-0x0007 = ASSET_VALUE
const REQUESTED_ASSET_WORD_INPUT = 0x0000
const REQUESTED_ASSET_VALUE_INPUT = 0x0004
const SWAPP_TAG_INPUT = 0x0008
const P2ID_TAG_INPUT = 0x0009
const SWAPP_COUNT_INPUT = 0x000C
const SWAPP_CREATOR_PREFIX_INPUT = 0x0010
const SWAPP_CREATOR_SUFFIX_INPUT = 0x0011
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: These are called inputs, but note inputs were renamed to note storage, so this no longer fits that well. I'd suggest removing the _INPUT or replacing it with _ITEM.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, make sense. I will change it to Item.

Comment on lines +72 to +76
# PSWAP script expects exactly 18 note storage items
const ERR_PSWAP_WRONG_NUMBER_OF_INPUTS="PSWAP wrong number of inputs"

# PSWAP script requires exactly one note asset
const ERR_PSWAP_WRONG_NUMBER_OF_ASSETS="PSWAP wrong number of assets"
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
# PSWAP script expects exactly 18 note storage items
const ERR_PSWAP_WRONG_NUMBER_OF_INPUTS="PSWAP wrong number of inputs"
# PSWAP script requires exactly one note asset
const ERR_PSWAP_WRONG_NUMBER_OF_ASSETS="PSWAP wrong number of assets"
# PSWAP script expects exactly 18 note storage items
const ERR_PSWAP_WRONG_NUMBER_OF_INPUTS="PSWAP script expects exactly 18 note storage items"
# PSWAP script requires exactly one note asset
const ERR_PSWAP_WRONG_NUMBER_OF_ASSETS="PSWAP script requires exactly one note asset"

Nit: More helpful error messages.

Comment on lines +326 to +329
// Ensure offered asset exists and is fungible
if self.assets.num_assets() != 1 {
return Err(NoteError::other("Swap note must have exactly 1 offered asset"));
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We're checking this condition in build now, so this seems redundant.

Comment on lines +130 to +140
/// Extracts the faucet ID of the requested asset from the key word.
pub fn requested_faucet_id(&self) -> Result<AccountId, NoteError> {
AccountId::try_from_elements(self.requested_asset_key[2], self.requested_asset_key[3])
.map_err(|e| NoteError::other_with_source("failed to parse faucet ID from key", e))
}

/// Extracts the requested token amount from the value word.
pub fn requested_asset_amount(&self) -> u64 {
self.requested_asset_value[0].as_canonical_u64()
}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we define PswapNoteStorage as:

pub struct PswapNoteStorage {
    requested_asset: FungibleAsset,
    /// ...
}

So we can work with the proper fungible asset type and can avoid these low-level operations?

The getters would just become:

pub fn requested_asset(&self) -> Asset {
    self.requested_asset.into()
}

pub fn requested_faucet_id(&self) -> AccountId {
    self.requested_asset.faucet_id()
}

pub fn requested_asset_amount(&self) -> u64 {
    self.requested_asset.amount()
}

Though not sure we need the id and amount getters, since it's easy to do storage.requested_asset().faucet_id(), etc.

This would also make the PswapNoteStorage builder type-safer (since it would not take Words as inputs anymore).

Comment on lines +315 to +320
pub fn execute(
&self,
consumer_account_id: AccountId,
input_amount: u64,
inflight_amount: u64,
) -> Result<(Note, Option<PswapNote>), NoteError> {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this intended to be used in production or is this for testing purposes?

If the goal is just testing, this seems fine, but then I'd move it behind #[cfg(any(feature = "testing", test))].

If it's for production, I'd change the input and inflight amounts to FungibleAsset, so the function has a guarantee that the input amounts do not exceed FungibleAsset::MAX_AMOUNT and that the function can check whether the faucet ID matches the requested faucet ID (which it currently assumes, but does not validate).

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, it is intended to be used by dex operators or market makers who are going to deal with Pswap notes. Instead of them writing their own execute code to create the notes, they can directly import it.

Okay, I will take the complete FungibleAsset as input in the code wherever I am taking asset_amount as input and creating the asset in the function.

Comment on lines +458 to +462
pub fn calculate_output_amount(
offered_total: u64,
requested_total: u64,
input_amount: u64,
) -> u64 {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If this took &self it'd b equivalent to calculate_offered_for_requested - do we need both functions in the public API?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just added this function with the thought process that any dex operator might require it as helping function.But you are right they can simply call calculate_offered_for_requested , we might not need this as public.

Comment on lines +540 to +548
let remainder_serial_num = Word::from([
Felt::new(self.serial_number[0].as_canonical_u64() + 1),
self.serial_number[1],
self.serial_number[2],
self.serial_number[3],
]);

let attachment_word = Word::from([Felt::new(offered_amount_for_fill), ZERO, ZERO, ZERO]);
let attachment = NoteAttachment::new_word(NoteAttachmentScheme::none(), attachment_word);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would be good to avoid Felt::new in general, but especially production code.

We can replace this with:

self.serial_number[0] + Felt::ONE

and

let attachment_word = Word::from([
    Felt::try_from(offered_amount_for_fill).expect("explain why this should fit"),
    ZERO,
    ZERO,
    ZERO,
]);

Comment on lines +514 to +520
fn create_remainder_pswap_note(
&self,
consumer_account_id: AccountId,
remaining_offered_asset: Asset,
remaining_requested_amount: u64,
offered_amount_for_fill: u64,
) -> Result<PswapNote, NoteError> {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With the other proposed changes to use FungibleAsset in execute, we may be able to take FungibleAsset as an input here as well so we can avoid constructing the assets internally.

Comment on lines +492 to +493
let p2id_serial_digest = Hasher::merge(&[swap_count_word, self.serial_number]);
let p2id_serial_num: Word = p2id_serial_digest;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
let p2id_serial_digest = Hasher::merge(&[swap_count_word, self.serial_number]);
let p2id_serial_num: Word = p2id_serial_digest;
let p2id_serial_num = Hasher::merge(&[swap_count_word, self.serial_number]);

Comment on lines +561 to +580
# Calculate offered_out for input_amount
mem_load.AMT_REQUESTED_IN
mem_load.AMT_REQUESTED
mem_load.AMT_OFFERED
# => [offered, requested, input_amount]
exec.calculate_tokens_offered_for_requested
# => [input_offered_out]

mem_store.AMT_OFFERED_OUT_INPUT
# => []

# Calculate offered_out for inflight_amount
mem_load.AMT_REQUESTED_INFLIGHT
mem_load.AMT_REQUESTED
mem_load.AMT_OFFERED
# => [offered, requested, inflight_amount]

exec.calculate_tokens_offered_for_requested
# => [inflight_offered_out]
mem_store.AMT_OFFERED_OUT_INFLIGHT
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the advantage of calculating the input and inflight output amounts separately? We could save a few cycles if we added them together and called calculate_tokens_offered_for_requested only once? If there's a reason, it'd be great to document it in a comment here.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need to calculate the offered amount corresponding to the input_amount seperately, so that we can send that value to the consumer's vault.
We need to calculate the total offered out to create the Pswap note for the remaining asset.

…ve safety and docs

- PswapNoteStorage: store FungibleAsset instead of raw key/value Words
- PswapNote: store offered_asset as FungibleAsset instead of NoteAssets
- execute(): take Option<FungibleAsset> for input/inflight assets
- Split execute into execute() and execute_full_fill_network() for clarity
- create_tag(): accept &FungibleAsset instead of &Asset
- Eliminate all Felt::new usage (use Felt::from, Felt::try_from, + ONE)
- Add swap_count overflow protection via checked_add
- Add script root verification in TryFrom<&Note>
- Add MASM fill validation (fill_amount <= requested)
- Rename MASM constants: _INPUT -> _ITEM, SWAPP_ -> PSWAP_
- Make calculate_output_amount private, add precision doc comment
- Add doc comments on all public accessors and methods
- Add network account full fill test
- Document network transaction compatibility throughout
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.

2 participants