Skip to content

improve: replace InPlace with self-transfer - #1574

Merged
Reinis-FRP merged 4 commits into
reinis/svm-v5-core-simplificationfrom
if/svm-v5-self-transfer
Oct 5, 2026
Merged

Reinis-FRP merged 4 commits into
reinis/svm-v5-core-simplificationfrom
if/svm-v5-self-transfer

Conversation

@grasphoper

Copy link
Copy Markdown
Collaborator

No description provided.

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 2, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-02T06:52:21.600121Z ddc93cb PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ddc93cb2e7

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

require!(token_accounts.source.delegate == COption::Some(V5_FILL_DELEGATE), V5Error::InvalidTokenAccount);
V5FillDelivery::Delegated(find_v5_account(remaining_accounts, &V5_FILL_DELEGATE, false)?.clone())
};
let delegate = find_v5_account(remaining_accounts, &V5_FILL_DELEGATE, false)?;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Document the delegate as required for in-place fills

When a client constructs an in-place fill, this unconditional lookup now requires v5_fill_delegate, but the public adapter_execute_across_v5 Required Accounts documentation in programs/svm-spoke/src/lib.rs still says that only external delivery requires it. A builder following that API documentation will omit the account and receive MissingAccount; update the Rustdoc alongside the changed account contract.

AGENTS.md reference: AGENTS.md:L11-L16

Useful? React with 👍 / 👎.

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.

Concrete replacement for the existing documentation issue in programs/svm-spoke/src/lib.rs (the file is outside this PR diff, so this is a patch rather than an applyable inline suggestion):

-    ///   submitter-scoped ["v5_fill_payer"], relay-scoped fill-status PDA, and System Program; external delivery also
-    ///   requires the recipient ATA and ["v5_fill_delegate"]. Account order is unrestricted because each account is
-    ///   resolved by its authenticated expected key.
+    ///   submitter-scoped ["v5_fill_payer"], relay-scoped fill-status PDA, System Program, recipient ATA, and
+    ///   ["v5_fill_delegate"], including when the recipient ATA is the Gateway vault itself. Account order is
+    ///   unrestricted because each account is resolved by its authenticated expected key.

Sent from Reinis Martinsons's Codex CLI Agent using gpt-6-astra 🤖

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.

Fixed in fbe350f. The required-account Rustdoc now includes the recipient ATA and v5_fill_delegate for every fill, including self-transfers into the Gateway vault.


Sent from Reinis Martinsons's Codex CLI Agent using gpt-6-astra 🤖

@Reinis-FRP Reinis-FRP left a comment

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.

The self-transfer simplification looks sound under the existing supported-token and atomic Gateway execution model. These suggestions clarify the remaining delivery requirements and avoid presenting zero approval as a security requirement.


Sent from Reinis Martinsons's Codex CLI Agent using gpt-6-astra 🤖

Comment on lines +68 to +69
// Self-transfers validate balance and authority without debiting funds or allowance.
// The committed Gateway tape must still consume the funds and clear any remaining approval.

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.

Zeroing the allowance is optional cleanup under the current permissionless Gateway model: a later caller can already execute a fresh APPROVE or an owner-authorized TRANSFER. It does not add a fund-security boundary. Keep the actual balance/consumption requirement explicit here, including multiple fills observing the same balance.

Suggested change
// Self-transfers validate balance and authority without debiting funds or allowance.
// The committed Gateway tape must still consume the funds and clear any remaining approval.
// Self-transfers validate balance, frozen state, and authority without debiting funds or allowance.
// The committed Gateway tape must enforce balance checks covering all fill obligations and consume the funds.

Sent from Reinis Martinsons's Codex CLI Agent using gpt-6-astra 🤖

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.

Applied in fbe350f. The comment now keeps balance checks covering all fill obligations and consumption explicit, without requiring approval cleanup.


Sent from Reinis Martinsons's Codex CLI Agent using gpt-6-astra 🤖

Comment thread programs/svm-spoke/V5_ADAPTER_SPEC.md Outdated
Comment on lines 135 to 140
delegate authority/allowance without debiting funds or consuming allowance. The tape must clear the remaining fill
allowance with `APPROVE(..., 0)` or replace it with the next spender's approval and consume that approval. A step root
may be reused across source deposits, but canonical builders must either allow at most one in-place fill before a post-fill floor and
full-balance terminal consumption, or enforce a cumulative floor covering every in-place fill recorded before that
consumption. The committed terminal outcome must be acceptable to every deposit matching the root. A fixed minimum
for one fill does not prove aggregate delivery.

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.

Suggest distinguishing optional allowance cleanup from the required delivery invariant. The reference tapes may retain the zero approval, but builders should not read it as necessary to protect the shared vault.

Suggested change
delegate authority/allowance without debiting funds or consuming allowance. The tape must clear the remaining fill
allowance with `APPROVE(..., 0)` or replace it with the next spender's approval and consume that approval. A step root
may be reused across source deposits, but canonical builders must either allow at most one in-place fill before a post-fill floor and
full-balance terminal consumption, or enforce a cumulative floor covering every in-place fill recorded before that
consumption. The committed terminal outcome must be acceptable to every deposit matching the root. A fixed minimum
for one fill does not prove aggregate delivery.
delegate authority/allowance without debiting funds or consuming allowance. Clearing the remaining allowance with
`APPROVE(..., 0)` is optional cleanup: permissionless Gateway execution already permits fresh approvals and
owner-authorized transfers, so clearing it does not protect funds left in the shared vault. A step root may be reused
across source deposits, but canonical builders must either allow at most one in-place fill before a post-fill floor
and full-balance terminal consumption, or enforce a cumulative floor covering every in-place fill recorded before
that consumption. The committed terminal outcome must be acceptable to every deposit matching the root. A fixed
minimum for one fill does not prove aggregate delivery.

Sent from Reinis Martinsons's Codex CLI Agent using gpt-6-astra 🤖

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.

Applied in fbe350f. The spec now explains why zero approval is optional cleanup under permissionless Gateway execution, while preserving the aggregate balance and terminal-consumption requirements.


Sent from Reinis Martinsons's Codex CLI Agent using gpt-6-astra 🤖

Comment thread programs/svm-spoke/V5_ADAPTER_SPEC.md Outdated
Comment on lines +238 to +239
self-transfer. The latter still depends on the proportional or aggregate continuing-path rule above, and its
unconsumed allowance must be cleared. Any later failure rolls back token, fill-status, and payer-float changes together.

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.

Same clarification in the execution summary: consumption remains required; allowance cleanup is optional.

Suggested change
self-transfer. The latter still depends on the proportional or aggregate continuing-path rule above, and its
unconsumed allowance must be cleared. Any later failure rolls back token, fill-status, and payer-float changes together.
self-transfer. The latter still depends on the proportional or aggregate continuing-path rule above; clearing its
unconsumed allowance is optional cleanup. Any later failure rolls back token, fill-status, and payer-float changes together.

Sent from Reinis Martinsons's Codex CLI Agent using gpt-6-astra 🤖

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.

Applied in fbe350f. The execution summary now describes allowance cleanup as optional and retains the continuing-path and atomic rollback requirements.


Sent from Reinis Martinsons's Codex CLI Agent using gpt-6-astra 🤖

Signed-off-by: Ihor Farion <ihor@umaproject.org>
@Reinis-FRP
Reinis-FRP force-pushed the reinis/svm-v5-core-simplification branch from 0a6b5ab to bb37ba1 Compare October 5, 2026 07:48
@Reinis-FRP
Reinis-FRP force-pushed the if/svm-v5-self-transfer branch from ddc93cb to bcc69e8 Compare October 5, 2026 07:48
Signed-off-by: Reinis Martinsons <reinis@umaproject.org>
@Reinis-FRP
Reinis-FRP requested a review from droplet-rl October 5, 2026 08:32

@droplet-rl droplet-rl left a comment

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.

The program change is correct. I found no blockers in fill.rs. One test assertion is weak and should be tightened, and a few docs outside the diff are out of date.

What I checked

  • Self-transfer behaviour. I read the transfer processor in spl-token 7.0.0 and spl-token-2022 6.0.0. When source == destination, transfer_checked still checks:

    • frozen state
    • amount <= balance
    • mint and decimals
    • delegate authority, with delegated_amount >= amount

    It then returns before it changes the balance or the allowance.

  • In-place fills are now stricter than the old InPlace branch. The old branch only checked the balance. Now an approval is also required, and the vault must not be frozen.

  • External fills keep the same guarantee. The explicit delegate == V5_FILL_DELEGATE check is gone. SPL still rejects the transfer: if the vault's delegate is a different key, it falls through to owner validation and fails with OwnerMismatch.

  • No Token-2022 side effects. The mint-extension allowlist already excludes permanent delegates, transfer hooks and transfer fees.

  • Account resolution and rollback behaviour are unchanged.

  • Build and tests. cargo check -p svm-spoke is clean, with no unused-import or dead-code warnings. cargo test -p svm-spoke passes 16/16. I couldn't run the anchor TS suites locally, and "Test verified SVM build" is still pending in CI.

Also good: the expectError fix in SvmSpoke.V5Fill.ts. The old helper called assert.fail("Expected X") inside its own try, and its catch then found X in that message. So every expected-error assertion in that file passed even when the transaction succeeded. It now matches the helpers in V5Source and V5FillStatus.

Should fix (tests). "custom program error: 0x1" is a prefix of almost every svm_spoke error code: CommonError 0x1770+, SvmError 0x1b58+, V5Error 0x1f40–0x1f4f. It also prefixes SPL 0x10–0x1f. So the new in-place assertions pass on unrelated failures. That includes the assertion that used to check InsufficientVaultBalance. See the inline comment.

Non-blocking

  • Dead error variant. Nothing returns V5Error::InsufficientVaultBalance (8015) any more. It is the last V5 variant, and V5 hasn't shipped (ERROR_CODES.md lists it with no pre-release code). So you can delete it, together with its ERROR_CODES.md row and the tests/compatibility.rs assertion, without shifting any other code. If you'd rather keep it, mark it as reserved.
  • Out-of-date docs outside the diff:
    • AGENTS.md:60-61 still says "External fills transfer tokens; in-place fills check the shared vault".
    • V5_ADAPTER_SPEC.md:343-344 describes the swap flow as "a single in-place Across fill followed by Gateway APPROVE(inputMint, executor_authority, full balance)…". It omits the APPROVE(fill delegate) that the tape now needs before the fill.

Comment thread test/svm/SvmSpoke.V5Fill.ts Outdated
await expectError(execute(input, undefined, { ...options, approval: null }), "custom program error: 0x4");
await expectError(
execute(input, undefined, { ...options, approval: outputAmount - 1n }),
"custom program error: 0x1"

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.

expectError does a substring match. "custom program error: 0x1" therefore also matches:

  • 0x1f49 (InvalidTokenAccount)
  • 0x1f4d (FillCommitmentMismatch)
  • all of CommonError (0x1770+)
  • 0x11 (AccountFrozen)

So this assertion, the ones at 385 and 409, and the existing one at 254 would still pass if the spoke rejected the fill for an unrelated reason. Before this PR, line 385 checked for InsufficientVaultBalance specifically.

You could let expectError accept a RegExp and match the whole code:

/custom program error: 0x1\b/   // InsufficientFunds only, not 0x11 / 0x1f49 / 0x1770…

Alternatively, match the SPL log line ("Error: insufficient funds", "Error: owner does not match", "Error: Account is frozen"). This only works if the token program on the test validator writes those messages to its logs.

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.

Fixed in 3a028ee. The SPL assertions now match the failing token program and the complete hexadecimal code with a word boundary, so 0x1 cannot accept 0x11, 0x1f49, or a failure from another program. I also removed the unused trailing InsufficientVaultBalance variant and updated the error table/range test without shifting the remaining codes. All 12 V5 fill tests pass against the rebuilt programs, including SPL Token and Token-2022 cases.


Sent from Reinis Martinsons's Codex CLI Agent using gpt-6-astra 🤖


await expectError(execute(undefined, undefined, { approval: outputAmount - 1n }), "custom program error: 0x1");
await expectError(execute(undefined, undefined, { delegate: Keypair.generate().publicKey }), "InvalidTokenAccount");
await expectError(execute(undefined, undefined, { delegate: Keypair.generate().publicKey }), "MissingAccount");

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.

This case now fails earlier, when the loader looks up V5_FILL_DELEGATE. So the file no longer tests a vault that is approved to a different delegate while the real fill-delegate account is supplied. The removed explicit InvalidTokenAccount check used to cover that case.

SPL still rejects it with OwnerMismatch (0x4), because it falls through to owner validation. Since the program no longer has its own guard, one assertion would be useful here. For example, a RealGateway path that approves some other key before an external fill should fail with 0x4. Non-blocking.

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.

Added in 3a028ee. The real-Gateway regression approves the deposit delegate but supplies the correct fill-delegate account to an external fill. It submits an actual failing transaction, checks the exact SPL OwnerMismatch (0x4) log, and verifies rollback of funding, recipient balance, fill status, payer rent, and approval. The focused real-Gateway test passes.


Sent from Reinis Martinsons's Codex CLI Agent using gpt-6-astra 🤖

Comment on lines +70 to +76
transfer_from(
accounts.transfer,
accounts.token_program,
relay.output_amount,
accounts.mint_decimals,
V5_FILL_DELEGATE_SEED,
)?;

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.

Non-blocking. The delegate, allowance and frozen checks now fail inside the token CPI with raw SPL codes. They also run after the fill-status account has been created. That is rolled back, so it isn't a safety issue. The main cost is clarity: a missing APPROVE shows up as OwnerMismatch (0x4), which is a confusing signal for someone debugging a tape. If you want a typed error back, one pre-check in load keeps the single transfer path:

require!(token_accounts.source.delegate == COption::Some(V5_FILL_DELEGATE), V5Error::InvalidTokenAccount);

Possible follow-up: transfer_from re-derives the delegate PDA with find_program_address on every call, even though V5_FILL_DELEGATE_BUMP is a constant. With bump 252 that is 4 create_program_address attempts, about 6k CU. In-place fills now pay that plus the token CPI, inside the same CU budget as the swap legs.

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.

Keeping the single transfer path and token-program validation here. 3a028ee documents OwnerMismatch (0x4) for missing/wrong approval instead of restoring the redundant pre-check. The constant address/seed/bump optimization is already implemented separately in #1573.


Sent from Reinis Martinsons's Codex CLI Agent using gpt-6-astra 🤖

Comment on lines 130 to 131
`8DWnJFMBTSDYWsUUSqna9tx9LJbU1yUfq7jTiPJDf8sX` with bump 252. Builders must use this PDA as both the approval
target and the supplied deposit delegate account.

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.

Every fill path, including in-place, now needs an APPROVE to the fill delegate. Consider pinning its address here the same way as the deposit delegate: v5_fill_delegate derives D27f3mVXRL6N3bgja49UWLQu7kt57sy1aZYy7ZEwdxn1 with bump 252. That matches constants.rs and the /pdas/fillDelegate fixture. Builders must use this PDA as both the approval target and the fill-delegate account they pass in.

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.

Added in 3a028ee: the fill-delegate address and bump, its use as both approval target and supplied account, and the requirement for sufficient approval when the fill executes. Also updated the AGENTS overview and destination-swap sequence to reflect self-transfers and the initial fill approval.


Sent from Reinis Martinsons's Codex CLI Agent using gpt-6-astra 🤖

@droplet-rl

Copy link
Copy Markdown
Contributor

🔎 View trace

Match exact token-program error codes, verify wrong-delegate rejection and atomic rollback through the real Gateway, and remove the unused InsufficientVaultBalance error. Update the delegate and self-transfer documentation.

Signed-off-by: Reinis Martinsons <reinis@umaproject.org>
@Reinis-FRP
Reinis-FRP requested a review from droplet-rl October 5, 2026 09:06

@droplet-rl droplet-rl left a comment

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.

Approving. 3a028ee addresses all of my earlier feedback, and the program change was already correct.

What changed since my last review

  • Error matching in the tests is now exact. The new tokenError(code) regex requires the failing program to be the token program, plus the full hex code followed by a word boundary. So 0x1 no longer accepts 0x11, 0x1f49, or an error raised by the spoke itself. tokenProgram is read when the regex is built, so the Token-2022 loop also checks against the Token-2022 program ID.
  • Wrong-delegate coverage is back, through the real Gateway. The new test approves the deposit delegate, supplies the correct fill-delegate account, and runs an external fill. It checks for the exact OwnerMismatch (0x4) log line and confirms rollback of the funding, recipient balance, fill status, payer rent and approval.
  • InsufficientVaultBalance is removed. It was the last variant, so no other error code shifts. compatibility.rs now pins FillOutputAmountTooLow = 8014, so the end of the range is still checked.
  • The docs are now accurate:
    • AGENTS.md describes self-transfers.
    • The spec gives the fill-delegate address and bump (they match constants.rs).
    • The destination swap sequence now starts with APPROVE(v5_fill_delegate).

cargo test -p svm-spoke passes 16/16 after the variant removal. I didn't run the anchor TS suites locally, and the "Test verified SVM build" CI job was still pending when I looked.

On the declined suggestion: keeping one transfer path and documenting 0x4 instead of a typed pre-check is a reasonable call. The PDA-bump optimisation being handled in #1573 is fine.

I left one optional wording nit inline.

Comment thread programs/svm-spoke/V5_ADAPTER_SPEC.md Outdated
Comment on lines +135 to +136
Sufficient approval must exist when the fill executes. Missing or wrong delegate approval fails in the token program
with `OwnerMismatch` (0x4).

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.

Optional wording nit. OwnerMismatch (0x4) only applies when the vault's delegate slot is empty or holds some other key. If the slot already holds the fill delegate but the allowance is too small, the token program fails with InsufficientFunds (0x1) instead.

That case includes a zeroed approval: SPL approve with amount 0 keeps the delegate set and only sets the allowance to 0, unless the Gateway calls revoke for zero. Builders debugging a tape will hit both codes, so it may be worth listing both:

Suggested change
Sufficient approval must exist when the fill executes. Missing or wrong delegate approval fails in the token program
with `OwnerMismatch` (0x4).
Sufficient approval must exist when the fill executes. A missing approval, or one to another delegate, fails in the token program
with `OwnerMismatch` (0x4); an insufficient fill-delegate allowance (including a zeroed one) fails with `InsufficientFunds` (0x1).

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.

Fixed in ef3bc66. The spec now distinguishes an unset/wrong delegate (OwnerMismatch, 0x4) from insufficient fill-delegate allowance (InsufficientFunds, 0x1), including zeroed allowance for a nonzero fill. Documentation-only change; the pre-commit checks passed.


Sent from Reinis Martinsons's Codex CLI Agent using gpt-6-astra 🤖

@droplet-rl

Copy link
Copy Markdown
Contributor

🔎 View trace

Signed-off-by: Reinis Martinsons <reinis@umaproject.org>
@Reinis-FRP
Reinis-FRP merged commit 90efcbf into reinis/svm-v5-core-simplification Oct 5, 2026
7 checks passed
@Reinis-FRP
Reinis-FRP deleted the if/svm-v5-self-transfer branch October 5, 2026 09:45
Reinis-FRP added a commit that referenced this pull request Oct 5, 2026
* replace InPlace with self-transfer

Signed-off-by: Ihor Farion <ihor@umaproject.org>

* docs(svm): clarify self-transfer approval and delivery requirements

Signed-off-by: Reinis Martinsons <reinis@umaproject.org>

* test(svm): tighten fill error checks and wrong-delegate coverage

Match exact token-program error codes, verify wrong-delegate rejection and atomic rollback through the real Gateway, and remove the unused InsufficientVaultBalance error. Update the delegate and self-transfer documentation.

Signed-off-by: Reinis Martinsons <reinis@umaproject.org>

* docs(svm): distinguish fill delegate and allowance errors

Signed-off-by: Reinis Martinsons <reinis@umaproject.org>

---------

Signed-off-by: Ihor Farion <ihor@umaproject.org>
Signed-off-by: Reinis Martinsons <reinis@umaproject.org>
Co-authored-by: Reinis Martinsons <reinis@umaproject.org>
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.

3 participants