Skip to content

feat(soroban): return typed execute receipts#422

Merged
carrion256 merged 4 commits into
devfrom
codex/soroban-execute-receipts
Jun 8, 2026
Merged

feat(soroban): return typed execute receipts#422
carrion256 merged 4 commits into
devfrom
codex/soroban-execute-receipts

Conversation

@carrion256

@carrion256 carrion256 commented Apr 29, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • replace the generic execute result envelope with typed opaque receipt bytes
  • carry completed withdrawal owner/receiver/assets/share data out of the runtime
  • update the ERC-4626 proxy to emit Withdraw only after a completed payout receipt

Auditor notes

Please focus review on:

  • contract/vault/soroban/shared-types/src/lib.rs receipt codecs and trailing-byte rejection
  • contract/vault/soroban/src/contract/entrypoints.rs and curator_vault.rs execute receipt plumbing
  • contract/proxy-4626-soroban/src/contract.rs receipt decoding and RedeemRequest/Withdraw event semantics

No storage migration, no command payload change, and no governance/share-token/adapter behavior change intended.

Verification

  • cargo test -p templar-soroban-shared-types -- --nocapture
  • cargo test -p templar-vault-kernel --lib -- --nocapture
  • cargo test -p templar-soroban-runtime -- --nocapture
  • cargo test -p templar-4626-proxy-soroban -- --nocapture
  • just -f contract/vault/soroban/justfile size-budget-check

Size

  • baseline runtime deploy: 94449 bytes
  • final runtime deploy: 96199 bytes
  • delta: +1750 bytes
  • gate: 96199 <= 131072 bytes

This change is Reviewable

@coderabbitai

coderabbitai Bot commented Apr 29, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Replace the older VaultCommandResult model with tagged receipt types; vault execute returns ABI-encoded receipt bytes; proxy and consumers decode receipts for events/returns; shared-types, kernel, tests, CLI, README, and workspace lint policy updated.

Changes

Receipt migration and downstream alignment

Layer / File(s) Summary
Receipt type definitions and codec
contract/vault/soroban/shared-types/src/lib.rs
DepositReceipt, RequestWithdrawReceipt, ExecuteWithdrawReceipt, I128Receipt, EmptyReceipt, and ReceiptAddress replace VaultCommandResult with tag-driven encode/decode and STRKEY validation/tests.
Vault entrypoints and result types
contract/vault/soroban/src/contract/entrypoints.rs, contract/vault/soroban/src/contract/mod.rs, contract/vault/soroban/src/contract/types.rs
execute_public_command now returns ABI-encoded receipt Bytes; request_withdraw_impl/execute_withdraw_impl return typed receipts; ExecuteWithdrawResult/CompletedWithdrawal added.
Kernel and helper wiring
contract/vault/kernel/src/actions/mod.rs, contract/vault/soroban/src/contract/helpers.rs
IdlePayoutPlan adds owner; address validation calls shared strkey::validate_address_strkey; helper wiring updated accordingly.
Proxy crate manifest & re-exports
contract/proxy-4626-soroban/Cargo.toml, contract/proxy-4626-soroban/src/lib.rs
Add local governance dev-dependency and re-export receipt types from shared-types (remove VaultCommandResult).
Proxy contract core and receipt decoding
contract/proxy-4626-soroban/src/contract.rs
Proxy methods invoke vault and decode returned receipt Bytes into DepositReceipt/RequestWithdrawReceipt/ExecuteWithdrawReceipt/I128Receipt/EmptyReceipt; invoke_vault_execute returns raw Bytes.
Proxy tests and mock vault
contract/proxy-4626-soroban/src/tests.rs
MockVault supports NextReceipt passthrough and configured CompletedWithdrawal; adds tests for trailing-bytes rejection and execute_withdraw event emission.
Vault contract tests and integration
contract/vault/soroban/src/tests.rs, contract/vault/soroban/tests/blend_e2e.rs, contract/vault/soroban/tests/integration_tests.rs
Tests and helpers updated to accept raw Bytes from vault execute and decode receipts for assertions; integration tests assert Completed and NoPayout execute-withdraw receipts.
CLI, README, governance mock
contract/vault/soroban/scripts/vault_payload.py, contract/vault/soroban/README.md, contract/vault/soroban/governance/src/tests.rs
decode_receipt replaces decode_result with strict parsing; README documents ExecuteWithdrawReceipt byte format; governance mock returns EmptyReceipt for unit commands.
Workspace lint and examples
Cargo.toml, test-utils/*
Workspace Clippy unwrap_used changed from allow to deny; test-utils add targeted #![allow(clippy::unwrap_used)] where intentional.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • Templar-Protocol/contracts#419: Main PR updates contract/proxy-curator-soroban to switch the vault execute path from decoding/validating WireVaultCommandResult to returning and decoding raw receipt Bytes, which directly builds on the curator proxy’s original VaultCommandResult-based implementation.

Suggested reviewers

  • peer2f00l
  • royalf00l
🚥 Pre-merge checks | ✅ 2
✅ Passed checks (2 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch codex/soroban-execute-receipts

Warning

Review ran into problems

🔥 Problems

Errors were encountered while retrieving linked issues.

Errors (1)
  • LINEAR integration encountered authorization issues. Please disconnect and reconnect the integration in the CodeRabbit UI.

Comment @coderabbitai help to get the list of available commands and usage tips.

@carrion256
carrion256 force-pushed the spr/refactor/vault-ergonomics/4641dafc branch from b4f1402 to 411d3fd Compare April 29, 2026 13:23
@carrion256
carrion256 force-pushed the codex/soroban-execute-receipts branch from 2c89d70 to 1695e3c Compare April 29, 2026 13:31
@carrion256
carrion256 force-pushed the spr/refactor/vault-ergonomics/4641dafc branch from 91fcfc6 to 6db2fc4 Compare April 30, 2026 15:11
@carrion256

Copy link
Copy Markdown
Collaborator Author

@coderabbitai review

@carrion256

Copy link
Copy Markdown
Collaborator Author

@coderabbitai for the benefit of the human reviewers - please output a description of this PR and any critique on the usability of the design when you are reviewing.

@coderabbitai

coderabbitai Bot commented May 1, 2026

Copy link
Copy Markdown
Contributor
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai

coderabbitai Bot commented May 1, 2026

Copy link
Copy Markdown
Contributor

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

Let me gather the key files to give human reviewers a thorough description and design critique.


🏁 Script executed:

#!/bin/bash
# Read the key files mentioned in the PR objectives

echo "=== shared-types/src/lib.rs ==="
fd "lib.rs" --full-path --search-path contract/vault/soroban/shared-types/src | head -5 | xargs cat

echo ""
echo "=== curator_vault.rs ==="
fd "curator_vault.rs" --full-path --search-path contract/vault/soroban/src | head -5 | xargs cat

echo ""
echo "=== proxy-4626-soroban contract.rs ==="
fd "contract.rs" --full-path --search-path contract/proxy-4626-soroban/src | head -5 | xargs cat

Length of output: 82845


🏁 Script executed:

#!/bin/bash
# Read entrypoints.rs - focus on execute and receipt-related sections
fd "entrypoints.rs" --full-path --search-path contract/vault/soroban/src | head -3 | xargs grep -n "receipt\|execute\|Receipt\|ExecuteWithdraw\|CompletedWithdrawal\|completed" -i | head -120

echo ""
echo "=== execute function in entrypoints.rs ==="
fd "entrypoints.rs" --full-path --search-path contract/vault/soroban/src | head -3 | xargs grep -n "fn execute\|ExecuteWithdraw\|receipt\|Receipt\|Bytes\|encode\|NoPayout\|Completed" | head -80

Length of output: 3584


🏁 Script executed:

#!/bin/bash
# Get the execute_withdraw_impl and encode_receipt function bodies, and surrounding context
fd "entrypoints.rs" --full-path --search-path contract/vault/soroban/src | head -3 | xargs sed -n '320,420p'

echo ""
echo "=== execute_public_command and encode_receipt ==="
fd "entrypoints.rs" --full-path --search-path contract/vault/soroban/src | head -3 | xargs sed -n '670,760p'

echo ""
echo "=== execute entrypoint ==="
fd "entrypoints.rs" --full-path --search-path contract/vault/soroban/src | head -3 | xargs sed -n '850,875p'

Length of output: 7330


🏁 Script executed:

#!/bin/bash
# Find encode_receipt and address_from_alloc_string / sdk_string_to_alloc helpers
fd "entrypoints.rs" --full-path --search-path contract/vault/soroban/src | head -3 | xargs grep -n "fn encode_receipt\|fn address_from_alloc\|fn sdk_string_to_alloc\|fn decode_command" 

echo ""
echo "=== encode_receipt and decode_command implementations ==="
fd "entrypoints.rs" --full-path --search-path contract/vault/soroban/src | head -3 | xargs grep -n -A 8 "fn encode_receipt\|fn decode_command\|fn sdk_string_to_alloc"

Length of output: 59


🏁 Script executed:

#!/bin/bash
# Search across the whole soroban src for these helper functions
rg -n "fn encode_receipt|fn decode_command|fn sdk_string_to_alloc|fn address_from_alloc" --type rust

echo ""
echo "=== Tag assignments across receipts ==="
rg -n "push_u8.*out.*[0-9]|tag.*0\|tag.*1\|tag.*2\|tag.*3\|tag.*4" contract/vault/soroban/shared-types/src/ --type rust | head -30

echo ""
echo "=== execute_withdraw emit_withdraw_event in proxy ==="
rg -n "emit_withdraw|emit_redeem|Withdraw\|RedeemRequest" contract/proxy-4626-soroban/src/ --type rust | head -30

Length of output: 4043


🏁 Script executed:

#!/bin/bash
# Read mod.rs for encode_receipt and decode_command
fd "mod.rs" --full-path --search-path contract/vault/soroban/src/contract | head -3 | xargs sed -n '55,75p'

echo ""
# Read helpers.rs for sdk_string_to_alloc
fd "helpers.rs" --full-path --search-path contract/vault/soroban/src/contract | head -3 | xargs sed -n '360,380p'

echo ""
# Check proxy tests for receipt decoding coverage
rg -n "execute_withdraw\|NoPayout\|Completed\|receipt" contract/proxy-4626-soroban/src/tests.rs | head -40

echo ""
# Check if there are fuzz/property tests for the codec
fd "." --search-path contract/vault/soroban/shared-types -e rs | xargs grep -l "fuzz\|proptest\|quickcheck\|arbitrary" 2>/dev/null

Length of output: 1206


🏁 Script executed:

#!/bin/bash
# Check proxy tests for execute_withdraw / receipt coverage
rg -n "execute_withdraw\|NoPayout\|Completed\|receipt\|Withdraw" contract/proxy-4626-soroban/src/tests.rs 2>/dev/null | head -40

echo ""
# Check shared-types tests for fuzz/property/arbitrary
rg -rn "fuzz\|proptest\|quickcheck\|arbitrary" contract/vault/soroban/shared-types/ 2>/dev/null | head -20

echo ""
# Check execute_withdraw_impl address resolution - is the strkey validated in load_address path?
rg -n "fn load_address\|validate_address\|strkey\|from_str\|to_string" contract/vault/soroban/src/ --type rust | head -30

echo ""
# Check the assets_out / shares_burned sign handling - what if completed.assets_out > i128::MAX?
rg -n "to_i128\|assets_out\|shares_burned" contract/vault/soroban/src/contract/entrypoints.rs | head -30

Length of output: 3


🏁 Script executed:

#!/bin/bash
# Look for proxy tests and strkey validation
find . -name "tests.rs" -path "*/proxy-4626*" 2>/dev/null | head -5 | xargs grep -n "execute_withdraw\|NoPayout\|Completed\|receipt" 2>/dev/null | head -40

echo "=== strkey and load_address ==="
find . -name "*.rs" -path "*/vault/soroban/src/*" 2>/dev/null | xargs grep -l "load_address\|from_str" 2>/dev/null | head -5 | xargs grep -n "fn load_address\|from_str" 2>/dev/null | head -20

echo "=== to_i128 in entrypoints ==="
find . -name "entrypoints.rs" -path "*/vault/soroban/*" 2>/dev/null | head -3 | xargs grep -n "to_i128\|assets_out\|shares_burned" 2>/dev/null | head -30

echo "=== shared types test coverage ==="
find . -name "*.rs" -path "*/shared-types/*" 2>/dev/null | head -10

echo "=== proxy test file ==="
find . -name "tests.rs" -path "*/proxy-4626*" 2>/dev/null | head -5 | xargs wc -l 2>/dev/null

echo "=== execute_withdraw in proxy tests ==="
find . -name "*.rs" -path "*/proxy*" 2>/dev/null | xargs grep -n "execute_withdraw\|NoPayout\|receipt" 2>/dev/null | head -40

Length of output: 7746


@carrion256 — here is a summary description and design critique for the human reviewers.


PR #422 — Description

What this PR does

This PR replaces the previous opaque/untyped byte blob returned by the vault's execute() cross-contract entrypoint with a set of typed, self-describing receipt structs, each with its own binary codec. The change threads typed completion data — particularly for ExecuteWithdraw — all the way from the vault kernel, through the runtime, out of the entrypoint, and into the ERC-4626 proxy so that events can be emitted with accurate, real data.

Layer 1 — shared-types/src/lib.rs: New receipt codec

Five typed receipts are introduced, each with a mandatory leading tag byte and a hand-rolled little-endian binary codec:

Type Tag Fields
DepositReceipt 0 shares_out: i128
RequestWithdrawReceipt 1 request_id: u64, shares_escrowed: i128
ExecuteWithdrawReceipt 2 Enum: NoPayout (inner 0) or Completed (inner 1) with request_id, owner, receiver, assets_out, shares_burned
I128Receipt 3 value: i128
EmptyReceipt 4 (none)

Every decoder enforces trailing-byte rejection via ensure_finished(), preventing corrupt/padded blobs from silently succeeding.

Layer 2 — entrypoints.rs: Receipt plumbing

execute_public_command() now dispatches each VaultCommand variant and returns an appropriate encoded receipt as Bytes. The execute() public entrypoint thus returns Bytes (previously a generic result). execute_withdraw_impl() maps the kernel-internal CompletedWithdrawal struct into ExecuteWithdrawReceipt::Completed, performing a reverse address lookup (kernel AddressSdkAddress → strkey string) before encoding.

Layer 3 — curator_vault.rs: Propagating completion data

execute_withdraw() now fills ExecuteWithdrawResult.completed: Option<CompletedWithdrawal>, carrying the payout owner, receiver, amounts, and request ID up from the kernel into the entrypoint layer.

Layer 4 — proxy-4626-soroban/src/contract.rs: Receipt decoding and event correction

The proxy now decodes each receipt type and:

  • Emits Deposit events with real shares_out from DepositReceipt.
  • Emits RedeemRequest events with real request_id / shares_escrowed from RequestWithdrawReceipt.
  • Only emits Withdraw events when ExecuteWithdrawReceipt::Completed is returned — the key correctness fix; no-op executions (NoPayout) are now silent.

Design Critique

✅ What's well done

  1. Trailing-byte rejection is correct and critical. Without ensure_finished(), a malformed or re-used byte blob could decode without error.
  2. Leading type-tag bytes on receipts prevent cross-type decoding (e.g., feeding a DepositReceipt to RequestWithdrawReceipt::decode() fails fast). The command_receipt_decoders_reject_wrong_tags test covers this.
  3. Withdraw event is now gated on Completed — the pre-existing bug where a no-op execute_withdraw() call could emit a phantom event is fixed.
  4. address_from_wire() validates StrKeys in the proxy via a hand-rolled base32 + CRC16-XMODEM check before calling Address::from_str, preventing malformed strings from silently reaching Soroban.

⚠️ Design concerns for auditors

1. Overlapping numeric tag namespaces between commands and receipts

VaultCommand tag bytes (0=DepositWithMin, 1=RequestWithdraw, 2=ExecuteWithdraw, …) overlap numerically with receipt tag bytes (0=DepositReceipt, 1=RequestWithdrawReceipt, 2=ExecuteWithdrawReceipt). In the current design, commands and receipts flow in opposite directions so mixing is not possible today. But if a future consumer stores an encoded receipt and later tries to decode it as a command (or vice versa), the codec will happily accept it as long as the first byte matches — it will not reject it as a wrong message type. A simple MessageKind outer envelope (0x00 = command, 0x01 = receipt) or non-overlapping tag ranges would seal this.

2. Double-tag byte on ExecuteWithdrawReceipt

The structure is [0x02][0x00|0x01][payload] — outer type tag then variant tag. This is technically correct but unusual. Auditors may want to confirm that nothing in the decode path attempts to interpret the inner 0x00/0x01 as a length or option-presence flag by mistake.

3. Late storage lookup failure after effects are committed

In execute_withdraw_impl():

let owner = storage
    .load_address(&completed.owner)
    .ok_or(ContractError::StorageError)?;

This lookup happens after vault.execute_withdraw() has run — meaning the asset transfer and share burn effects are already committed to the Soroban host (via execute_effects). If the address mapping is missing (e.g., due to a corrupt or unpopulated address store), the execute() call returns ContractError::StorageError and the transaction reverts. Because Soroban ledger writes are transactional, the revert would roll back the effects — but reviewers should confirm that this revert boundary holds cleanly, since the effects include token contract cross-calls.

4. u128 → i128 conversion on the hot path

assets_out and shares_burned are u128 in the kernel and are cast via to_i128() before being placed into ExecuteWithdrawReceipt::Completed. If the vault ever accumulates more than i128::MAX (≈ 1.7 × 10³⁸) base units in a single withdrawal, this conversion returns an error and the execute() call fails after effects are applied. While astronomically unlikely for any realistic token denomination, the asymmetry between kernel u128 accounting and the wire's i128 type is a latent risk that is not called out in the PR description.

5. No codec versioning; ensure_finished() blocks forward-compatibility

The codec has no version field. Adding a new field to any receipt type in a future PR would require a new tag value (breaking), a new inner variant, or changing the schema. Appending fields is blocked because ensure_finished() rejects trailing bytes. This is a reasonable tradeoff for simplicity now, but it should be explicitly documented as a design constraint so future contributors don't assume they can extend receipts in a backward-compatible way.

6. I128Receipt is semantically ambiguous for future consumers

Both Allocate and RefreshMarkets return I128Receipt { value: new_external_assets }. A receipt recipient cannot determine which command produced it without out-of-band context. This is fine for the current proxy (which never calls those commands), but is a footgun for any future integrator consuming raw execute() bytes.

7. Proxy execute_withdraw() returns ()

pub fn execute_withdraw(env: Env, caller: Address) -> Result<(), ContractError>

The payout data (request_id, assets_out, receiver) is available in the decoded receipt but is silently dropped. A keeper or on-chain integrator calling this cannot inspect the outcome without parsing emitted events — which is not possible within a contract call. Returning at least the request_id (or a small struct) would make the API more composable for future keeper contract use.

8. withdraw() / redeem() return u64, not i128

These return request_id rather than shares burned / assets transferred, making them ERC-7540-style async functions, not ERC-4626 synchronous ones. This is architecturally correct given the queued withdrawal lifecycle, but auditors comparing against ERC-4626 ABI expectations should note this divergence explicitly.

9. Hand-rolled StrKey validator in proxy

validate_address_strkey() is a 50-line base32 + CRC16-XMODEM implementation. It should be compared against the Stellar StrKey spec (SEP-0023) to ensure edge cases (odd padding bits, wrong version bytes) are handled identically to how Soroban's native Address::from_str would behave. The version-byte check allows both ACCOUNT_VERSION (6 << 3 = 48) and CONTRACT_VERSION (2 << 3 = 16), which is correct for this context.


🧠 Learnings used
Learnt from: CR
Repo: Templar-Protocol/contracts PR: 0
File: contract/vault/soroban/AGENTS.md:0-0
Timestamp: 2026-04-24T14:44:42.034Z
Learning: Applies to contract/vault/soroban/src/contract/entrypoints.rs : withdraw() and redeem() include an idle-only atomic path that bypasses the queued withdrawal lifecycle. Do not change queue semantics without checking both paths

Learnt from: CR
Repo: Templar-Protocol/contracts PR: 0
File: contract/vault/soroban/AGENTS.md:0-0
Timestamp: 2026-04-24T14:44:42.034Z
Learning: Applies to contract/vault/soroban/** : Read README.md, STRIDE.md, SIZE_BUDGET.md, src/contract/entrypoints.rs, src/effects/mod.rs, and src/storage/mod.rs before making non-trivial changes to the Soroban vault runtime

Learnt from: CR
Repo: Templar-Protocol/contracts PR: 0
File: contract/vault/soroban/AGENTS.md:0-0
Timestamp: 2026-04-24T14:44:42.034Z
Learning: Applies to contract/vault/soroban/src/contract/entrypoints.rs : Review auth on every privileged Soroban entrypoint. Do not rely on outer routing alone

Learnt from: carrion256
Repo: Templar-Protocol/contracts PR: 417
File: contract/vault/soroban/STRIDE.md:0-0
Timestamp: 2026-04-28T08:36:25.374Z
Learning: In `contract/vault/soroban/src/contract/entrypoints.rs` (Templar-Protocol/contracts, Soroban vault), `execute_governance()` decodes the payload and dispatches to `execute_governance_command()` **without** an auth check at the entrypoint. Authorization is enforced inside each command handler: `set_governance_config_impl`, `set_governance_policy_impl`, and `skim_impl` each call `require_governance(env, &caller)?` as their first action. There is no `require_auth()` wrapping `execute_governance()` itself. Any future `GovernanceCommand` variant added must include its own `require_governance` call; do not assume entrypoint-level auth exists.

Learnt from: carrion256
Repo: Templar-Protocol/contracts PR: 417
File: contract/vault/curator-primitives/src/policy/cooldown/mod.rs:17-23
Timestamp: 2026-04-27T09:33:55.992Z
Learning: In Templar-Protocol/contracts, the Soroban vault (`contract/vault/soroban/`) and NEAR vault (`contract/vault/near/`) are greenfield deployments with no live persisted state. Do not flag internal serialization format changes (e.g., field type changes in structs like `Cooldown`, `RefreshThrottle`, or policy state) as breaking migration risks — there is no existing stored data to break.

Learnt from: CR
Repo: Templar-Protocol/contracts PR: 0
File: contract/vault/soroban/AGENTS.md:0-0
Timestamp: 2026-04-24T14:44:42.034Z
Learning: Applies to contract/vault/soroban/** : If behavior changes around governance bridging, withdrawal lifecycle, or share-token auth, update the docs in contract/vault/soroban/ in the same change

Learnt from: CR
Repo: Templar-Protocol/contracts PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-04-24T14:44:16.597Z
Learning: Watch for `OpState` transitions, escrow accounting, keeper-routed withdrawals, callback ordering, idle-balance resync, fee accrual, and reconciliation after partial failures in the vault contract

Learnt from: CR
Repo: Templar-Protocol/contracts PR: 0
File: contract/vault/soroban/AGENTS.md:0-0
Timestamp: 2026-04-24T14:44:42.034Z
Learning: Applies to contract/vault/soroban/src/** : Run `cargo test -p templar-soroban-runtime -- --nocapture`, integration tests, and property tests before submitting changes to the runtime

Learnt from: CR
Repo: Templar-Protocol/contracts PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-04-24T14:44:16.597Z
Learning: Run `cargo test -p templar-vault-contract -- --nocapture` as minimum verification when changing the vault contract

Learnt from: carrion256
Repo: Templar-Protocol/contracts PR: 417
File: contract/vault/soroban/src/contract/entrypoints.rs:806-836
Timestamp: 2026-04-28T08:29:30.164Z
Learning: In `contract/vault/soroban/src/contract/entrypoints.rs` (Templar-Protocol/contracts, Soroban vault), the `SorobanVaultContract::initialize()` entrypoint intentionally has no caller-authorization check. Deployment is expected to go through a controlled/factory flow where `initialize` is called atomically with contract deployment, eliminating any front-running window. Do not flag the missing auth check on `initialize` in this contract as a security issue.

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@contract/proxy-4626-soroban/src/contract.rs`:
- Around line 225-246: execute_withdraw currently discards the request_id from
ExecuteWithdrawReceipt::Completed and the Withdraw event lacks that id, breaking
traceability; update the Withdraw emission and plumbing to include request_id:
modify emit_withdraw_event to accept a request_id parameter (and its emitted
event to include the id), extract request_id from
ExecuteWithdrawReceipt::Completed inside execute_withdraw, and pass that
request_id into emit_withdraw_event (or alternatively return the request_id from
execute_withdraw if you prefer callers to receive it) so indexers/relayers can
correlate the completion to the original RedeemRequest.
- Around line 129-193: The delegated withdraw/redeem flow (functions withdraw
and redeem) allows caller != owner via require_auth_or_allowance but then calls
invoke_vault_execute with VaultCommand::RequestWithdraw which requires the
owner's signature in the vault, causing failure; as a short-term guard, add a
check in both withdraw and redeem that returns an error if caller != owner
(e.g., require!(caller == owner, ContractError::DelegatedNotSupported) or a new
explicit ContractError variant) before calling invoke_vault_execute so delegated
callers are rejected until end-to-end delegated auth is implemented.

In `@contract/proxy-4626-soroban/src/tests.rs`:
- Around line 114-148: Add a unit test in the same tests.rs that constructs a
vault command payload (use WireVaultCommand::DepositWithMin or
WireVaultCommand::ExecuteWithdraw), then arrange the mocked vault response to be
a well-formed DepositReceipt.encode() or ExecuteWithdrawReceipt.encode() with
extra trailing garbage bytes (append some arbitrary bytes) by writing that byte
vector into the mock storage keys (MockVaultDataKey::RecordedPayloads or
MockVaultDataKey::CompletedWithdrawal as appropriate), call the execute(env,
payload) helper, and assert the proxy rejects the malformed receipt (i.e., fails
strict decode or returns the error/empty-receipt path you expect); this will
exercise execute, WireVaultCommand decoding,
DepositReceipt/ExecuteWithdrawReceipt handling and ensure malformed
trailing-bytes are rejected.

In `@contract/vault/soroban/tests/integration_tests.rs`:
- Around line 175-191: The tests never decode the new
ExecuteWithdrawReceipt::Completed payload, so update the helper(s) to add a
typed execute_withdraw (or similar) assertion path: add a method (e.g.
execute_withdraw) alongside execute and execute_unit that calls
self.execute(command), decodes the returned bytes into ExecuteWithdrawReceipt
(handling decoding errors to map into
templar_soroban_runtime::ContractError::InvalidInput), and returns the typed
fields (owner, receiver, assets_out, shares_burned) or asserts on them in tests;
reference the existing execute, execute_unit, EmptyReceipt, and the enum variant
ExecuteWithdrawReceipt::Completed when locating where to add the decoding and
return/assert logic.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 51b95be4-6558-4392-9690-472c530c2dce

📥 Commits

Reviewing files that changed from the base of the PR and between 6db2fc4 and 1695e3c.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (18)
  • .gitignore
  • Cargo.toml
  • contract/proxy-4626-soroban/Cargo.toml
  • contract/proxy-4626-soroban/src/contract.rs
  • contract/proxy-4626-soroban/src/error.rs
  • contract/proxy-4626-soroban/src/lib.rs
  • contract/proxy-4626-soroban/src/tests.rs
  • contract/proxy-4626-soroban/tests/integration_tests.rs
  • contract/vault/kernel/src/actions/mod.rs
  • contract/vault/soroban/justfile
  • contract/vault/soroban/shared-types/src/lib.rs
  • contract/vault/soroban/src/contract/curator_vault.rs
  • contract/vault/soroban/src/contract/entrypoints.rs
  • contract/vault/soroban/src/contract/mod.rs
  • contract/vault/soroban/src/contract/types.rs
  • contract/vault/soroban/src/tests.rs
  • contract/vault/soroban/tests/blend_e2e.rs
  • contract/vault/soroban/tests/integration_tests.rs

Comment thread contract/proxy-4626-soroban/src/contract.rs
Comment thread contract/proxy-4626-soroban/src/contract.rs Outdated
Comment thread contract/proxy-4626-soroban/src/tests.rs
Comment thread contract/vault/soroban/tests/integration_tests.rs
@carrion256
carrion256 force-pushed the codex/soroban-execute-receipts branch from 1695e3c to 98b1d01 Compare June 2, 2026 11:40
@carrion256
carrion256 changed the base branch from spr/refactor/vault-ergonomics/4641dafc to halborn-audit June 2, 2026 11:41
Base automatically changed from halborn-audit to dev June 4, 2026 16:06
An error occurred while trying to automatically change base from halborn-audit to dev June 4, 2026 16:06
@carrion256
carrion256 force-pushed the codex/soroban-execute-receipts branch from 98b1d01 to bbf10d7 Compare June 5, 2026 08:48

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@Cargo.toml`:
- Line 141: Revert the workspace-level Clippy override for unwraps by changing
the unwrap_used setting in Cargo.toml from "allow" back to a stricter policy
(e.g., "deny" or remove the override) so accidental unwrap() calls in non-test
code are guarded; ensure test files still compile by relying on per-crate or
test-module allowances instead of a workspace-wide "allow" for unwrap_used and
update any workspace-level comment to reflect the stricter policy (look for the
unwrap_used entry in Cargo.toml).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 23f7f040-85a1-4660-a88f-025cc8d4e1d3

📥 Commits

Reviewing files that changed from the base of the PR and between 1695e3c and bbf10d7.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (19)
  • .gitignore
  • Cargo.toml
  • contract/proxy-4626-soroban/Cargo.toml
  • contract/proxy-4626-soroban/src/contract.rs
  • contract/proxy-4626-soroban/src/error.rs
  • contract/proxy-4626-soroban/src/lib.rs
  • contract/proxy-4626-soroban/src/tests.rs
  • contract/proxy-4626-soroban/tests/integration_tests.rs
  • contract/vault/kernel/src/actions/mod.rs
  • contract/vault/soroban/governance/src/tests.rs
  • contract/vault/soroban/justfile
  • contract/vault/soroban/shared-types/src/lib.rs
  • contract/vault/soroban/src/contract/curator_vault.rs
  • contract/vault/soroban/src/contract/entrypoints.rs
  • contract/vault/soroban/src/contract/mod.rs
  • contract/vault/soroban/src/contract/types.rs
  • contract/vault/soroban/src/tests.rs
  • contract/vault/soroban/tests/blend_e2e.rs
  • contract/vault/soroban/tests/integration_tests.rs
💤 Files with no reviewable changes (11)
  • contract/vault/soroban/justfile
  • contract/vault/soroban/tests/integration_tests.rs
  • contract/vault/soroban/src/contract/types.rs
  • contract/vault/soroban/src/contract/mod.rs
  • contract/vault/kernel/src/actions/mod.rs
  • contract/vault/soroban/governance/src/tests.rs
  • contract/vault/soroban/shared-types/src/lib.rs
  • contract/vault/soroban/tests/blend_e2e.rs
  • contract/vault/soroban/src/contract/entrypoints.rs
  • contract/vault/soroban/src/tests.rs
  • contract/vault/soroban/src/contract/curator_vault.rs

@coderabbitai coderabbitai Bot 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.

Caution

Inline review comments failed to post. This is likely due to GitHub's internal server error or limits when posting large numbers of comments. If you are seeing this consistently it is likely a permissions issue. Please check "Moderation" -> "Code review limits" under your organization settings.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@Cargo.toml`:
- Line 141: Revert the workspace-level Clippy override for unwraps by changing
the unwrap_used setting in Cargo.toml from "allow" back to a stricter policy
(e.g., "deny" or remove the override) so accidental unwrap() calls in non-test
code are guarded; ensure test files still compile by relying on per-crate or
test-module allowances instead of a workspace-wide "allow" for unwrap_used and
update any workspace-level comment to reflect the stricter policy (look for the
unwrap_used entry in Cargo.toml).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 23f7f040-85a1-4660-a88f-025cc8d4e1d3

📥 Commits

Reviewing files that changed from the base of the PR and between 1695e3c and bbf10d7.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (19)
  • .gitignore
  • Cargo.toml
  • contract/proxy-4626-soroban/Cargo.toml
  • contract/proxy-4626-soroban/src/contract.rs
  • contract/proxy-4626-soroban/src/error.rs
  • contract/proxy-4626-soroban/src/lib.rs
  • contract/proxy-4626-soroban/src/tests.rs
  • contract/proxy-4626-soroban/tests/integration_tests.rs
  • contract/vault/kernel/src/actions/mod.rs
  • contract/vault/soroban/governance/src/tests.rs
  • contract/vault/soroban/justfile
  • contract/vault/soroban/shared-types/src/lib.rs
  • contract/vault/soroban/src/contract/curator_vault.rs
  • contract/vault/soroban/src/contract/entrypoints.rs
  • contract/vault/soroban/src/contract/mod.rs
  • contract/vault/soroban/src/contract/types.rs
  • contract/vault/soroban/src/tests.rs
  • contract/vault/soroban/tests/blend_e2e.rs
  • contract/vault/soroban/tests/integration_tests.rs
💤 Files with no reviewable changes (11)
  • contract/vault/soroban/justfile
  • contract/vault/soroban/tests/integration_tests.rs
  • contract/vault/soroban/src/contract/types.rs
  • contract/vault/soroban/src/contract/mod.rs
  • contract/vault/kernel/src/actions/mod.rs
  • contract/vault/soroban/governance/src/tests.rs
  • contract/vault/soroban/shared-types/src/lib.rs
  • contract/vault/soroban/tests/blend_e2e.rs
  • contract/vault/soroban/src/contract/entrypoints.rs
  • contract/vault/soroban/src/tests.rs
  • contract/vault/soroban/src/contract/curator_vault.rs
🛑 Comments failed to post (1)
Cargo.toml (1)

141-141: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Re-enable clippy::unwrap_used at workspace scope.

Setting this to allow removes an important guardrail and makes accidental unwrap() in non-test contract code easier to merge unnoticed.

Suggested change
-unwrap_used = "allow"
+unwrap_used = "warn"

As per coding guidelines, !(*.test.rs|test_*.rs|**/tests/**/*.rs)/**/*.rs: “Avoid introducing unwrap() in non-test Rust code unless the surrounding file already relies on an invariant and documents it clearly”.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

unwrap_used = "warn"
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Cargo.toml` at line 141, Revert the workspace-level Clippy override for
unwraps by changing the unwrap_used setting in Cargo.toml from "allow" back to a
stricter policy (e.g., "deny" or remove the override) so accidental unwrap()
calls in non-test code are guarded; ensure test files still compile by relying
on per-crate or test-module allowances instead of a workspace-wide "allow" for
unwrap_used and update any workspace-level comment to reflect the stricter
policy (look for the unwrap_used entry in Cargo.toml).

@codecov

codecov Bot commented Jun 5, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 92.43697% with 9 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
...t/vault/curator-primitives/src/policy/state/mod.rs 0.00% 6 Missing ⚠️
.../curator-primitives/src/policy/supply_queue/mod.rs 75.00% 2 Missing ⚠️
contract/vault/kernel/src/actions/mod.rs 0.00% 1 Missing ⚠️

📢 Thoughts on this report? Let us know!

@carrion256
carrion256 force-pushed the codex/soroban-execute-receipts branch from bbf10d7 to ba61e97 Compare June 5, 2026 13:03

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 7

♻️ Duplicate comments (1)
contract/proxy-4626-soroban/src/contract.rs (1)

297-314: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

request_id omitted from Withdraw event breaks traceability.

The ExecuteWithdrawReceipt::Completed includes request_id (line 302 elides it with ..), but emit_withdraw_event (lines 669-686) does not accept or emit this field. Indexers/relayers cannot correlate the Withdraw event to the original RedeemRequest event when multiple withdrawals have similar amounts.

🔗 Proposed fix to include request_id in event

Update the event helper signature and emission:

 #[allow(deprecated)]
 pub(crate) fn emit_withdraw_event(
     env: &Env,
     sender: &Address,
     receiver: &Address,
     owner: &Address,
+    request_id: u64,
     assets: i128,
     shares: i128,
 ) {
     env.events().publish(
         (
             symbol_short!("Withdraw"),
             sender.clone(),
             receiver.clone(),
             owner.clone(),
+            request_id,
         ),
         (assets, shares),
     );
 }

Then pass request_id at the call site:

         if let ExecuteWithdrawReceipt::Completed {
             owner,
             receiver,
             assets_out,
             shares_burned,
-            ..
+            request_id,
         } = receipt
         {
             let owner = address_from_wire(&env, &owner)?;
             let receiver = address_from_wire(&env, &receiver)?;
             emit_withdraw_event(
                 &env,
                 &operator,
                 &receiver,
                 &owner,
+                request_id,
                 assets_out,
                 shares_burned,
             );
         }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@contract/proxy-4626-soroban/src/contract.rs` around lines 297 - 314, The
Withdraw event is missing request_id: when matching
ExecuteWithdrawReceipt::Completed, capture the request_id instead of eliding it
and pass it into emit_withdraw_event; update the emit_withdraw_event function
signature and its internal event emission to include a request_id field (and
update any other call sites to pass the new parameter) so the emitted Withdraw
event contains request_id for traceability (symbols:
ExecuteWithdrawReceipt::Completed, emit_withdraw_event, request_id,
RedeemRequest).
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@contract/vault/kernel/src/actions/mod.rs`:
- Around line 952-962: The helper has_actionable_withdrawal_liquidity currently
requires full funding (available_assets >= expected_assets) which blocks partial
payouts; change its logic to return true when there is any available idle
liquidity to make progress (e.g., available_assets > 0) so queued withdrawals
can take partial payouts and allow compute_idle_settlement to run; update the
helper function has_actionable_withdrawal_liquidity and ensure callers in the
queue classifier and plan_idle_payout still call this helper (so they allow
progress when available_assets > 0 rather than requiring full coverage).
- Around line 1384-1395: The issue is that handle_finish_allocating calls
complete_allocation(..., None) which forces the operation to end in Idle and
drops any queued withdrawal; change the call to propagate the pending withdrawal
identifier instead of None so the allocation completion resumes the withdrawal
workflow. Specifically, update handle_finish_allocating to call
complete_allocation(mem::take(&mut state.op_state), op_id, Some(<appropriate
withdraw/op id from the taken OpState>)) (or otherwise extract and pass the
queued withdraw id contained in the OpState) so apply_transition_result(state,
transition) receives a transition that continues the withdrawal-rebalance
lifecycle rather than hard-ending it.

In `@contract/vault/soroban/README.md`:
- Around line 188-196: Update the README keeper guidance to reference the actual
ExecuteWithdrawReceipt enum variants (NoPayout and Completed) instead of
non-existent fields; instruct keepers to derive the head request's expected
assets from the indexed WithdrawalRequested event stream, compare that to
current idle assets from proxy_view, treat a NoPayout receipt as a signal to
free liquidity and retry, and treat a Completed receipt with zero assets_paid
(or equivalent within Completed) as an unexpected no-progress state to alert on;
also mention that the A-002 fix aims to reject zero-progress transitions before
persistence so automation should not rely on a bare Unit success.
- Around line 178-186: The README's description of ExecuteWithdrawReceipt is
incorrect; update the docs to reflect that ExecuteWithdrawReceipt is a
two-variant enum (ExecuteWithdrawReceipt::NoPayout with tag 0 and
ExecuteWithdrawReceipt::Completed with tag 1) and list the Completed fields as
request_id: u64, owner: Address, receiver: Address, assets_out: i128,
shares_burned: i128 instead of
op_state_before/op_state_after/assets_transferred/events_emitted; reference the
typed execute_withdraw entrypoint and the generic Execute(payload) path
(VaultCommand::ExecuteWithdraw) in the text so readers can match the documented
receipt shape to the receipt codec and keeper integration expectations.

In `@contract/vault/soroban/shared-types/src/lib.rs`:
- Around line 509-521: The public receipt ExecuteWithdrawReceipt::Completed
currently narrows assets_out and shares_burned to i128 which can overflow at the
ABI boundary; change those fields to u128 to match the runtime's unsigned domain
(and update any related pattern matches/consumers of ExecuteWithdrawReceipt and
the entrypoint that serializes this enum to avoid downcasting), or alternatively
add validation where withdrawals are queued (e.g., in the function that creates
queued withdrawals) to reject amounts > i128::MAX so the signed narrowing can
never occur; prefer the first approach (change ExecuteWithdrawReceipt::Completed
assets_out and shares_burned to u128 and remove any downcasts in the entrypoint)
to make the state representable and safe.

In `@contract/vault/soroban/src/tests.rs`:
- Around line 364-366: The tests are decoding the DepositWithMin variant using
I128Receipt; change those decodes to use DepositReceipt instead to match the
receipt ABI. Locate occurrences where DepositWithMin is passed to decode as
I128Receipt (e.g., the decode calls near the symbols DepositWithMin, I128Receipt
and the surrounding test helpers in tests.rs around the failing cases) and
replace the type parameter with DepositReceipt so the tag and ABI validation
align with the typed receipt contract.

---

Duplicate comments:
In `@contract/proxy-4626-soroban/src/contract.rs`:
- Around line 297-314: The Withdraw event is missing request_id: when matching
ExecuteWithdrawReceipt::Completed, capture the request_id instead of eliding it
and pass it into emit_withdraw_event; update the emit_withdraw_event function
signature and its internal event emission to include a request_id field (and
update any other call sites to pass the new parameter) so the emitted Withdraw
event contains request_id for traceability (symbols:
ExecuteWithdrawReceipt::Completed, emit_withdraw_event, request_id,
RedeemRequest).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: af492c10-e39e-4b22-913e-9eae204533da

📥 Commits

Reviewing files that changed from the base of the PR and between bbf10d7 and ba61e97.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (16)
  • contract/proxy-4626-soroban/Cargo.toml
  • contract/proxy-4626-soroban/src/contract.rs
  • contract/proxy-4626-soroban/src/lib.rs
  • contract/proxy-4626-soroban/src/tests.rs
  • contract/vault/kernel/src/actions/mod.rs
  • contract/vault/soroban/README.md
  • contract/vault/soroban/governance/src/tests.rs
  • contract/vault/soroban/scripts/vault_payload.py
  • contract/vault/soroban/shared-types/src/lib.rs
  • contract/vault/soroban/src/contract/curator_vault.rs
  • contract/vault/soroban/src/contract/entrypoints.rs
  • contract/vault/soroban/src/contract/mod.rs
  • contract/vault/soroban/src/contract/types.rs
  • contract/vault/soroban/src/tests.rs
  • contract/vault/soroban/tests/blend_e2e.rs
  • contract/vault/soroban/tests/integration_tests.rs

@coderabbitai coderabbitai Bot 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.

Caution

Inline review comments failed to post. This is likely due to GitHub's internal server error or limits when posting large numbers of comments. If you are seeing this consistently it is likely a permissions issue. Please check "Moderation" -> "Code review limits" under your organization settings.

Actionable comments posted: 7

♻️ Duplicate comments (1)
contract/proxy-4626-soroban/src/contract.rs (1)

297-314: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

request_id omitted from Withdraw event breaks traceability.

The ExecuteWithdrawReceipt::Completed includes request_id (line 302 elides it with ..), but emit_withdraw_event (lines 669-686) does not accept or emit this field. Indexers/relayers cannot correlate the Withdraw event to the original RedeemRequest event when multiple withdrawals have similar amounts.

🔗 Proposed fix to include request_id in event

Update the event helper signature and emission:

 #[allow(deprecated)]
 pub(crate) fn emit_withdraw_event(
     env: &Env,
     sender: &Address,
     receiver: &Address,
     owner: &Address,
+    request_id: u64,
     assets: i128,
     shares: i128,
 ) {
     env.events().publish(
         (
             symbol_short!("Withdraw"),
             sender.clone(),
             receiver.clone(),
             owner.clone(),
+            request_id,
         ),
         (assets, shares),
     );
 }

Then pass request_id at the call site:

         if let ExecuteWithdrawReceipt::Completed {
             owner,
             receiver,
             assets_out,
             shares_burned,
-            ..
+            request_id,
         } = receipt
         {
             let owner = address_from_wire(&env, &owner)?;
             let receiver = address_from_wire(&env, &receiver)?;
             emit_withdraw_event(
                 &env,
                 &operator,
                 &receiver,
                 &owner,
+                request_id,
                 assets_out,
                 shares_burned,
             );
         }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@contract/proxy-4626-soroban/src/contract.rs` around lines 297 - 314, The
Withdraw event is missing request_id: when matching
ExecuteWithdrawReceipt::Completed, capture the request_id instead of eliding it
and pass it into emit_withdraw_event; update the emit_withdraw_event function
signature and its internal event emission to include a request_id field (and
update any other call sites to pass the new parameter) so the emitted Withdraw
event contains request_id for traceability (symbols:
ExecuteWithdrawReceipt::Completed, emit_withdraw_event, request_id,
RedeemRequest).
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@contract/vault/kernel/src/actions/mod.rs`:
- Around line 952-962: The helper has_actionable_withdrawal_liquidity currently
requires full funding (available_assets >= expected_assets) which blocks partial
payouts; change its logic to return true when there is any available idle
liquidity to make progress (e.g., available_assets > 0) so queued withdrawals
can take partial payouts and allow compute_idle_settlement to run; update the
helper function has_actionable_withdrawal_liquidity and ensure callers in the
queue classifier and plan_idle_payout still call this helper (so they allow
progress when available_assets > 0 rather than requiring full coverage).
- Around line 1384-1395: The issue is that handle_finish_allocating calls
complete_allocation(..., None) which forces the operation to end in Idle and
drops any queued withdrawal; change the call to propagate the pending withdrawal
identifier instead of None so the allocation completion resumes the withdrawal
workflow. Specifically, update handle_finish_allocating to call
complete_allocation(mem::take(&mut state.op_state), op_id, Some(<appropriate
withdraw/op id from the taken OpState>)) (or otherwise extract and pass the
queued withdraw id contained in the OpState) so apply_transition_result(state,
transition) receives a transition that continues the withdrawal-rebalance
lifecycle rather than hard-ending it.

In `@contract/vault/soroban/README.md`:
- Around line 188-196: Update the README keeper guidance to reference the actual
ExecuteWithdrawReceipt enum variants (NoPayout and Completed) instead of
non-existent fields; instruct keepers to derive the head request's expected
assets from the indexed WithdrawalRequested event stream, compare that to
current idle assets from proxy_view, treat a NoPayout receipt as a signal to
free liquidity and retry, and treat a Completed receipt with zero assets_paid
(or equivalent within Completed) as an unexpected no-progress state to alert on;
also mention that the A-002 fix aims to reject zero-progress transitions before
persistence so automation should not rely on a bare Unit success.
- Around line 178-186: The README's description of ExecuteWithdrawReceipt is
incorrect; update the docs to reflect that ExecuteWithdrawReceipt is a
two-variant enum (ExecuteWithdrawReceipt::NoPayout with tag 0 and
ExecuteWithdrawReceipt::Completed with tag 1) and list the Completed fields as
request_id: u64, owner: Address, receiver: Address, assets_out: i128,
shares_burned: i128 instead of
op_state_before/op_state_after/assets_transferred/events_emitted; reference the
typed execute_withdraw entrypoint and the generic Execute(payload) path
(VaultCommand::ExecuteWithdraw) in the text so readers can match the documented
receipt shape to the receipt codec and keeper integration expectations.

In `@contract/vault/soroban/shared-types/src/lib.rs`:
- Around line 509-521: The public receipt ExecuteWithdrawReceipt::Completed
currently narrows assets_out and shares_burned to i128 which can overflow at the
ABI boundary; change those fields to u128 to match the runtime's unsigned domain
(and update any related pattern matches/consumers of ExecuteWithdrawReceipt and
the entrypoint that serializes this enum to avoid downcasting), or alternatively
add validation where withdrawals are queued (e.g., in the function that creates
queued withdrawals) to reject amounts > i128::MAX so the signed narrowing can
never occur; prefer the first approach (change ExecuteWithdrawReceipt::Completed
assets_out and shares_burned to u128 and remove any downcasts in the entrypoint)
to make the state representable and safe.

In `@contract/vault/soroban/src/tests.rs`:
- Around line 364-366: The tests are decoding the DepositWithMin variant using
I128Receipt; change those decodes to use DepositReceipt instead to match the
receipt ABI. Locate occurrences where DepositWithMin is passed to decode as
I128Receipt (e.g., the decode calls near the symbols DepositWithMin, I128Receipt
and the surrounding test helpers in tests.rs around the failing cases) and
replace the type parameter with DepositReceipt so the tag and ABI validation
align with the typed receipt contract.

---

Duplicate comments:
In `@contract/proxy-4626-soroban/src/contract.rs`:
- Around line 297-314: The Withdraw event is missing request_id: when matching
ExecuteWithdrawReceipt::Completed, capture the request_id instead of eliding it
and pass it into emit_withdraw_event; update the emit_withdraw_event function
signature and its internal event emission to include a request_id field (and
update any other call sites to pass the new parameter) so the emitted Withdraw
event contains request_id for traceability (symbols:
ExecuteWithdrawReceipt::Completed, emit_withdraw_event, request_id,
RedeemRequest).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: af492c10-e39e-4b22-913e-9eae204533da

📥 Commits

Reviewing files that changed from the base of the PR and between bbf10d7 and ba61e97.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (16)
  • contract/proxy-4626-soroban/Cargo.toml
  • contract/proxy-4626-soroban/src/contract.rs
  • contract/proxy-4626-soroban/src/lib.rs
  • contract/proxy-4626-soroban/src/tests.rs
  • contract/vault/kernel/src/actions/mod.rs
  • contract/vault/soroban/README.md
  • contract/vault/soroban/governance/src/tests.rs
  • contract/vault/soroban/scripts/vault_payload.py
  • contract/vault/soroban/shared-types/src/lib.rs
  • contract/vault/soroban/src/contract/curator_vault.rs
  • contract/vault/soroban/src/contract/entrypoints.rs
  • contract/vault/soroban/src/contract/mod.rs
  • contract/vault/soroban/src/contract/types.rs
  • contract/vault/soroban/src/tests.rs
  • contract/vault/soroban/tests/blend_e2e.rs
  • contract/vault/soroban/tests/integration_tests.rs
🛑 Comments failed to post (7)
contract/vault/kernel/src/actions/mod.rs (2)

952-962: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Don't require full liquidity before a queued withdrawal can make progress.

This helper now returns true only when available_assets >= expected_assets, and both the queue classifier and plan_idle_payout() consult it before compute_idle_settlement(). That removes the partial-payout path entirely: a withdrawal that could legally burn part of the escrow and transfer some idle assets now stays blocked until the vault can fund the full request.

Also applies to: 1881-1891

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@contract/vault/kernel/src/actions/mod.rs` around lines 952 - 962, The helper
has_actionable_withdrawal_liquidity currently requires full funding
(available_assets >= expected_assets) which blocks partial payouts; change its
logic to return true when there is any available idle liquidity to make progress
(e.g., available_assets > 0) so queued withdrawals can take partial payouts and
allow compute_idle_settlement to run; update the helper function
has_actionable_withdrawal_liquidity and ensure callers in the queue classifier
and plan_idle_payout still call this helper (so they allow progress when
available_assets > 0 rather than requiring full coverage).

1384-1395: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

FinishAllocating now breaks the withdrawal-rebalance lifecycle.

complete_withdraw_allocation() still ends by dispatching FinishAllocating, but this path now hardcodes complete_allocation(..., None). After liquidity is rebalanced back from a market, the vault drops to Idle instead of resuming the queued withdrawal, so the request now needs an extra explicit execute_withdraw hop. Based on learnings: Watch for OpState transitions, escrow accounting, keeper-routed withdrawals, callback ordering, idle-balance resync, fee accrual, and reconciliation after partial failures in the vault contract.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@contract/vault/kernel/src/actions/mod.rs` around lines 1384 - 1395, The issue
is that handle_finish_allocating calls complete_allocation(..., None) which
forces the operation to end in Idle and drops any queued withdrawal; change the
call to propagate the pending withdrawal identifier instead of None so the
allocation completion resumes the withdrawal workflow. Specifically, update
handle_finish_allocating to call complete_allocation(mem::take(&mut
state.op_state), op_id, Some(<appropriate withdraw/op id from the taken
OpState>)) (or otherwise extract and pass the queued withdraw id contained in
the OpState) so apply_transition_result(state, transition) receives a transition
that continues the withdrawal-rebalance lifecycle rather than hard-ending it.
contract/vault/soroban/README.md (2)

178-186: ⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Critical: Documented receipt fields do not match actual ExecuteWithdrawReceipt structure.

The documentation describes ExecuteWithdrawReceipt as containing op_state_before, op_state_after, assets_transferred, and events_emitted fields. Per the PR objectives and receipt codec implementation, ExecuteWithdrawReceipt is actually a two-variant enum:

  • ExecuteWithdrawReceipt::NoPayout (inner tag 0): no fields
  • ExecuteWithdrawReceipt::Completed (inner tag 1): request_id: u64, owner: Address, receiver: Address, assets_out: i128, shares_burned: i128

This mismatch will break keeper integrations that attempt to decode the receipt using the documented field names.

📝 Proposed documentation fix
-The typed `execute_withdraw` entrypoint keeps returning `Result<(), _>` for the stable contract
-ABI. The generic `execute(payload)` command path returns
-`ExecuteWithdrawReceipt` bytes for `VaultCommand::ExecuteWithdraw`, with:
-
-- `op_state_before` and `op_state_after`: kernel operation-state codes
-  (`0 = Idle`, `1 = Allocating`, `2 = Withdrawing`, `3 = Refreshing`,
-  `4 = Payout`).
-- `assets_transferred`: assets paid to receivers during this command.
-- `events_emitted`: kernel/runtime events emitted while processing the command.
+The typed `execute_withdraw` entrypoint keeps returning `Result<(), _>` for the stable contract
+ABI. The generic `execute(payload)` command path returns `ExecuteWithdrawReceipt` bytes for
+`VaultCommand::ExecuteWithdraw`. `ExecuteWithdrawReceipt` is a two-variant enum:
+
+- `ExecuteWithdrawReceipt::NoPayout` (inner tag 0): No withdrawal was completed; the queue head
+  was not cooled down or insufficient idle liquidity was available.
+- `ExecuteWithdrawReceipt::Completed` (inner tag 1): A withdrawal was completed with fields:
+  - `request_id: u64`: the queue request that was paid out
+  - `owner: Address`: the original withdrawal requester
+  - `receiver: Address`: the destination address that received assets
+  - `assets_out: i128`: underlying assets transferred to the receiver
+  - `shares_burned: i128`: escrowed shares burned during settlement
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@contract/vault/soroban/README.md` around lines 178 - 186, The README's
description of ExecuteWithdrawReceipt is incorrect; update the docs to reflect
that ExecuteWithdrawReceipt is a two-variant enum
(ExecuteWithdrawReceipt::NoPayout with tag 0 and
ExecuteWithdrawReceipt::Completed with tag 1) and list the Completed fields as
request_id: u64, owner: Address, receiver: Address, assets_out: i128,
shares_burned: i128 instead of
op_state_before/op_state_after/assets_transferred/events_emitted; reference the
typed execute_withdraw entrypoint and the generic Execute(payload) path
(VaultCommand::ExecuteWithdraw) in the text so readers can match the documented
receipt shape to the receipt codec and keeper integration expectations.

188-196: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Fix keeper guidance to reference actual receipt structure.

This section references assets_transferred and op_state_after fields that do not exist in ExecuteWithdrawReceipt. The guidance should be rewritten to work with the actual two-variant enum structure (NoPayout vs Completed).

📝 Proposed guidance fix
-Keepers should treat a failed `ExecuteWithdraw` with the kernel low-liquidity error as a signal to
-free market liquidity before retrying. The error is intentionally compact and does not carry
-`needed` / `available` amounts; automation should derive the head request's `expected_assets` from
-the indexed `WithdrawalRequested` event stream and compare it with the current idle assets exposed
-by `proxy_view` before choosing how much liquidity to free. A successful command with
-`assets_transferred == 0` and a non-idle `op_state_after` should be alerted as an unexpected
-no-progress withdrawal state. The A-002 fix is intended to reject that zero-progress transition
-before it is persisted, but the structured result keeps automation from relying on a bare `Unit`
-success.
+Keepers should treat a failed `ExecuteWithdraw` with the kernel low-liquidity error as a signal to
+free market liquidity before retrying. The error is intentionally compact and does not carry
+`needed` / `available` amounts; automation should derive the head request's `expected_assets` from
+the indexed `WithdrawalRequested` event stream and compare it with the current idle assets exposed
+by `proxy_view` before choosing how much liquidity to free. A successful `ExecuteWithdraw` that
+returns `ExecuteWithdrawReceipt::NoPayout` indicates that no withdrawal was completed (queue head
+not cooled down or insufficient idle liquidity); keepers should verify the reason and free
+liquidity if needed before retrying. `ExecuteWithdrawReceipt::Completed` with `assets_out > 0`
+confirms successful payout; the `Completed` variant carries the full settlement details for
+reconciliation.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@contract/vault/soroban/README.md` around lines 188 - 196, Update the README
keeper guidance to reference the actual ExecuteWithdrawReceipt enum variants
(NoPayout and Completed) instead of non-existent fields; instruct keepers to
derive the head request's expected assets from the indexed WithdrawalRequested
event stream, compare that to current idle assets from proxy_view, treat a
NoPayout receipt as a signal to free liquidity and retry, and treat a Completed
receipt with zero assets_paid (or equivalent within Completed) as an unexpected
no-progress state to alert on; also mention that the A-002 fix aims to reject
zero-progress transitions before persistence so automation should not rely on a
bare Unit success.
contract/vault/soroban/shared-types/src/lib.rs (1)

509-521: ⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Keep completed-withdraw receipt amounts in the same unsigned domain as the runtime.

CompletedWithdrawal now carries assets_out and shares_burned as u128, but this public receipt narrows both to i128. The new entrypoint has to downcast before replying, so a valid queued withdrawal above i128::MAX will fail at the ABI boundary instead of completing. Either keep these fields unsigned on the wire or reject oversized requests when they are created so this state cannot reach execution. As per coding guidelines: "Prefer stronger types over loosely constrained values in Rust" and "make invalid states unrepresentable."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@contract/vault/soroban/shared-types/src/lib.rs` around lines 509 - 521, The
public receipt ExecuteWithdrawReceipt::Completed currently narrows assets_out
and shares_burned to i128 which can overflow at the ABI boundary; change those
fields to u128 to match the runtime's unsigned domain (and update any related
pattern matches/consumers of ExecuteWithdrawReceipt and the entrypoint that
serializes this enum to avoid downcasting), or alternatively add validation
where withdrawals are queued (e.g., in the function that creates queued
withdrawals) to reject amounts > i128::MAX so the signed narrowing can never
occur; prefer the first approach (change ExecuteWithdrawReceipt::Completed
assets_out and shares_burned to u128 and remove any downcasts in the entrypoint)
to make the state representable and safe.
contract/vault/soroban/src/contract/entrypoints.rs (1)

1660-1666: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

max_redeem is rounded the wrong way.

Using convert_to_shares(idle_assets) under-reports redeemable shares because the redeem path floors on the asset side. For example, when one share would redeem to exactly one idle asset after flooring, this code can still return 0 here. Derive max_redeem from the redeem predicate instead of the inverse floor conversion.

contract/vault/soroban/src/tests.rs (1)

364-366: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Decode DepositWithMin as DepositReceipt instead of I128Receipt.

DepositWithMin is being decoded with I128Receipt at Line 2675 and Line 2762. That conflicts with the typed receipt contract (deposit has its own receipt type), so these tests can fail on tag mismatch or validate the wrong ABI.

Suggested fix
     use templar_soroban_shared_types::{
-        EmptyReceipt, ExecuteWithdrawReceipt, ExecuteWithdrawStatus, GovernanceCommand,
+        DepositReceipt, EmptyReceipt, ExecuteWithdrawReceipt, ExecuteWithdrawStatus, GovernanceCommand,
         I128Receipt, RequestWithdrawReceipt, VaultCommand,
         GOVERNANCE_CONFIG_KIND_IDLE_RESYNC_COOLDOWN, GOVERNANCE_CONFIG_KIND_VIRTUAL_OFFSETS,
         GOVERNANCE_CONFIG_KIND_WITHDRAWAL_COOLDOWN,
     };
@@
     fn decode_i128_receipt(bytes: &Bytes) -> i128 {
         I128Receipt::decode(&bytes.to_alloc_vec())
             .expect("decode i128 receipt")
             .value
     }
+
+    fn decode_deposit_receipt(bytes: &Bytes) -> DepositReceipt {
+        DepositReceipt::decode(&bytes.to_alloc_vec()).expect("decode deposit receipt")
+    }
@@
-        let minted = decode_i128_receipt(&minted);
+        let minted = decode_deposit_receipt(&minted).shares_out;
@@
-            assert_eq!(decode_i128_receipt(&deposit_receipt), deposit_assets);
+            assert_eq!(
+                decode_deposit_receipt(&deposit_receipt).shares_out,
+                deposit_assets
+            );

Also applies to: 468-472, 2675-2675, 2752-2763

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@contract/vault/soroban/src/tests.rs` around lines 364 - 366, The tests are
decoding the DepositWithMin variant using I128Receipt; change those decodes to
use DepositReceipt instead to match the receipt ABI. Locate occurrences where
DepositWithMin is passed to decode as I128Receipt (e.g., the decode calls near
the symbols DepositWithMin, I128Receipt and the surrounding test helpers in
tests.rs around the failing cases) and replace the type parameter with
DepositReceipt so the tag and ABI validation align with the typed receipt
contract.

@carrion256
carrion256 force-pushed the codex/soroban-execute-receipts branch 3 times, most recently from 3c9f43e to 8194eb6 Compare June 5, 2026 13:52

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 4

♻️ Duplicate comments (1)
contract/proxy-4626-soroban/src/tests.rs (1)

114-156: 🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Malformed-receipt regression test is still missing.

All mocked vault responses are currently well-formed, so the proxy tests never exercise the strict ensure_finished() decode path that rejects trailing bytes. A test case returning a valid receipt (e.g., DepositReceipt or ExecuteWithdrawReceipt) with appended garbage bytes would catch regressions where the proxy accidentally stops rejecting malformed payloads.

As per coding guidelines, **/tests/**/*.rs: Write comprehensive unit tests for new, non-trivial Rust logic.

🧪 Suggested test structure

Add a unit test that:

  1. Constructs a WireVaultCommand::DepositWithMin or WireVaultCommand::ExecuteWithdraw payload
  2. Arranges the mock to return a well-formed DepositReceipt.encode() or ExecuteWithdrawReceipt.encode() with extra trailing garbage bytes appended by extending MockVaultDataKey::RecordedPayloads or adding a new mock storage key
  3. Calls the proxy's deposit or execute_withdraw via the fixture
  4. Asserts the proxy rejects the malformed receipt (returns an error or expected fallback behavior)

This exercises the ensure_finished() validation and prevents future regressions.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@contract/proxy-4626-soroban/src/tests.rs` around lines 114 - 156, Add a unit
test that mocks a vault response containing a valid encoded receipt plus
trailing garbage bytes so the proxy's ensure_finished() path is exercised;
specifically, in tests for execute (which pushes to
MockVaultDataKey::RecordedPayloads) construct a WireVaultCommand::DepositWithMin
or ::ExecuteWithdraw payload, arrange the mock RecordedPayloads to return
DepositReceipt::encode() or ExecuteWithdrawReceipt::encode() with extra bytes
appended, invoke the proxy deposit or execute_withdraw fixture, and assert the
call rejects the malformed receipt (error/fallback) to ensure ensure_finished()
enforces no trailing data.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@contract/vault/soroban/shared-types/src/lib.rs`:
- Around line 1265-1288: Add tests exercising malformed ExecuteWithdrawReceipt
decoding: create an encoded ExecuteWithdrawReceipt with a wrong inner
discriminant to assert ExecuteWithdrawReceipt::decode(...) returns
Err(CodecError::InvalidTag), and create a truncated encoding for the Completed
variant (e.g., drop bytes from a Completed payload) to assert
ExecuteWithdrawReceipt::decode(...) returns Err(CodecError::InvalidEncoding);
place these alongside the existing command_receipt_decoders_* tests and reuse
the same encode/decode helpers so the new tests reference
ExecuteWithdrawReceipt, Completed, decode, and the CodecError variants.
- Around line 514-520: The Completed receipt variant currently uses signed i128
for assets_out and shares_burned which can reject valid u128 kernel values;
change those fields in the Completed enum to unsigned (u128 or a dedicated
newtype like Amount) and update any related ABI/serialization code so the
receipt ABI expresses non-negative quantities. Then update
contract/vault/soroban/src/contract/entrypoints.rs to stop narrowing kernel u128
values into i128 and instead construct the receipt using the new unsigned types
(or validated newtype) with only necessary checks for overflow in serialization,
ensuring all conversions/serde traits align with the new types.

In `@contract/vault/soroban/src/tests.rs`:
- Around line 364-365: The tests decode DepositWithMin receipts using the wrong
codec (I128Receipt) causing mismatches; locate the decoding branches that handle
the DepositWithMin path/variant (where I128Receipt is used) and replace the
decoder/type usage with DepositReceipt so deposit receipts are decoded as their
typed DepositReceipt; apply the same change for the other occurrences mentioned
(the other DepositWithMin decoding sites in the file that reference I128Receipt)
to ensure all deposit receipts use DepositReceipt.

In `@contract/vault/soroban/tests/blend_e2e.rs`:
- Around line 36-43: The match in execute_command currently uses a wildcard
branch decoding everything not VaultCommand::DepositWithMin as I128Receipt,
which can hide invalid command/receipt pairings; update the match to enumerate
the exact VaultCommand variants used by this test file (e.g., list each command
like VaultCommand::Withdraw { .. }, VaultCommand::SomeOtherCommand { .. } that
expect I128Receipt) and decode each explicitly with I128Receipt::decode(...).
Keep DepositWithMin using DepositReceipt::decode(...) and preserve the map_err
to templar_soroban_runtime::ContractError::InvalidInput for all decode failures
so only valid command/receipt combinations are accepted.

---

Duplicate comments:
In `@contract/proxy-4626-soroban/src/tests.rs`:
- Around line 114-156: Add a unit test that mocks a vault response containing a
valid encoded receipt plus trailing garbage bytes so the proxy's
ensure_finished() path is exercised; specifically, in tests for execute (which
pushes to MockVaultDataKey::RecordedPayloads) construct a
WireVaultCommand::DepositWithMin or ::ExecuteWithdraw payload, arrange the mock
RecordedPayloads to return DepositReceipt::encode() or
ExecuteWithdrawReceipt::encode() with extra bytes appended, invoke the proxy
deposit or execute_withdraw fixture, and assert the call rejects the malformed
receipt (error/fallback) to ensure ensure_finished() enforces no trailing data.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 0f9505b5-2e34-4b62-acfd-8d96ca572449

📥 Commits

Reviewing files that changed from the base of the PR and between ba61e97 and 2e834ae.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (16)
  • contract/proxy-4626-soroban/Cargo.toml
  • contract/proxy-4626-soroban/src/contract.rs
  • contract/proxy-4626-soroban/src/lib.rs
  • contract/proxy-4626-soroban/src/tests.rs
  • contract/vault/kernel/src/actions/mod.rs
  • contract/vault/soroban/README.md
  • contract/vault/soroban/governance/src/tests.rs
  • contract/vault/soroban/scripts/vault_payload.py
  • contract/vault/soroban/shared-types/src/lib.rs
  • contract/vault/soroban/src/contract/curator_vault.rs
  • contract/vault/soroban/src/contract/entrypoints.rs
  • contract/vault/soroban/src/contract/mod.rs
  • contract/vault/soroban/src/contract/types.rs
  • contract/vault/soroban/src/tests.rs
  • contract/vault/soroban/tests/blend_e2e.rs
  • contract/vault/soroban/tests/integration_tests.rs

Comment thread contract/vault/soroban/shared-types/src/lib.rs
Comment thread contract/vault/soroban/src/tests.rs Outdated
Comment thread contract/vault/soroban/tests/blend_e2e.rs
@carrion256
carrion256 force-pushed the codex/soroban-execute-receipts branch 2 times, most recently from 35919b1 to f64d0f3 Compare June 5, 2026 14:10
coderabbitai[bot]
coderabbitai Bot previously requested changes Jun 5, 2026

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
contract/vault/soroban/src/contract/curator_vault.rs (1)

560-606: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Resolve completed-withdraw address mappings before payout effects.

CompletedWithdrawal now carries raw kernel Address values, but the receipt ABI eventually serializes owner and receiver as strings. This path starts the payout flow before proving both mappings exist, so a missing owner mapping can turn an otherwise valid withdrawal into a late execute_withdraw error. Preflight both mappings here before withdrawal_settled / transfer work, or store already-mapped wire values in the completion payload.

♻️ Minimal hardening
     fn complete_withdrawal_from_idle(
         &mut self,
         now_ns: u64,
     ) -> Result<ExecuteWithdrawResult, RuntimeError> {
         let min_withdrawal_assets = self.kernel_config().min_withdrawal_assets;
         let idle_payout =
             transition_to_runtime(plan_idle_payout(self.state()?, min_withdrawal_assets))?;
 
+        self.ensure_mapped(&idle_payout.owner)?;
+        self.ensure_mapped(&idle_payout.receiver)?;
+
         let assets_out = idle_payout.assets_out;
         let burn_shares = idle_payout.burn_shares;
         let op_id = idle_payout.op_id;
         let completed = CompletedWithdrawal {
             request_id: idle_payout.request_id,

Based on learnings, evaluate edge cases around asynchronous receipt execution and cross-contract call ordering in Rust smart contract code; watch for TOCTOU-style issues.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@contract/vault/soroban/src/contract/curator_vault.rs` around lines 560 - 606,
The code constructs CompletedWithdrawal with raw kernel Address values
(idle_payout.owner and idle_payout.receiver) but then begins the payout flow
(calling withdrawal_settled, executing effects and TransferAssets) before
ensuring those addresses are mapped, which can cause late execute_withdraw
failures; fix by pre-flighting/mapping the owner and receiver addresses (call
ensure_effect_addresses_mapped or otherwise convert/map idle_payout.owner and
idle_payout.receiver into wire/mapped forms) immediately after obtaining
idle_payout and before calling withdrawal_settled/execute_effects/creating
transfer_effects, or alternatively populate CompletedWithdrawal with the
already-mapped wire values so subsequent code uses mapped addresses; update uses
of CompletedWithdrawal, withdrawal_settled, transfer_effects, and
ensure_effect_addresses_mapped accordingly.
♻️ Duplicate comments (1)
contract/vault/soroban/tests/blend_e2e.rs (1)

36-42: 🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Make unsupported command/receipt pairs impossible in this helper.

The wildcard arm still routes any future VaultCommand through I128Receipt, so an unsupported command becomes a generic decode failure instead of an explicit mismatch. Match only the variants this file actually uses and make the fallback unreachable.

As per coding guidelines: "**/*.rs: Prefer parsing over validation in Rust code; express invariants in types wherever practical, and make invalid states unrepresentable"

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@contract/vault/soroban/tests/blend_e2e.rs` around lines 36 - 42, The match
currently routes all non-DepositWithMin VaultCommand variants through
I128Receipt, hiding unsupported command/receipt mismatches; update the match on
VaultCommand to list only the concrete variants this test actually expects
(e.g., VaultCommand::DepositWithMin => decode as DepositReceipt and each other
used command => decode with the correct receipt type like I128Receipt) and
replace the wildcard arm with an explicit unreachable!() or panic with a clear
message (e.g., unreachable!("unsupported VaultCommand in helper")) so future
unsupported commands fail loudly; keep the existing .map_err(|_|
templar_soroban_runtime::ContractError::InvalidInput) behavior for decode
failures.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@contract/proxy-4626-soroban/src/contract.rs`:
- Around line 702-764: Duplicate StrKey logic: move the shared
validate_address_strkey and crc16_xmodem implementations into a single shared
module and import it from both places (instead of copying). Create a small
common module (e.g., a new helpers/strkey module or crate) that exposes
validate_address_strkey and crc16_xmodem, update contract.rs to call that shared
functions (same names), and update
contract/vault/soroban/src/contract/helpers.rs to re-export or call the same
shared implementations; do not add the stellar-strkey crate (keep the inline
implementation to avoid WASM size regression).

In `@contract/vault/soroban/tests/integration_tests.rs`:
- Around line 1225-1292: Add a new #[rstest] test (e.g.,
soroban_contract_execute_withdraw_decodes_no_payout_receipt) that mirrors the
existing Completed test but drives the path that yields
ExecuteWithdrawReceipt::NoPayout: use the SorobanContractFixture, VaultProxy,
and StellarAssetClient to create a withdrawal request that will produce no
payout (for example by arranging deposits/shares or balances so assets_out would
be zero), advance the ledger timestamp as needed, call
proxy.execute_withdraw(&curator) inside env.as_contract, match the returned
receipt against ExecuteWithdrawReceipt::NoPayout and assert the expected fields
(request_id, owner/receiver wire values, and any zero/empty payout indicators);
reference and reuse symbols ExecuteWithdrawReceipt::NoPayout, execute_withdraw,
VaultCommand::RequestWithdraw, VaultProxy, and MIN_WITHDRAWAL_ASSETS to locate
and implement the test.

---

Outside diff comments:
In `@contract/vault/soroban/src/contract/curator_vault.rs`:
- Around line 560-606: The code constructs CompletedWithdrawal with raw kernel
Address values (idle_payout.owner and idle_payout.receiver) but then begins the
payout flow (calling withdrawal_settled, executing effects and TransferAssets)
before ensuring those addresses are mapped, which can cause late
execute_withdraw failures; fix by pre-flighting/mapping the owner and receiver
addresses (call ensure_effect_addresses_mapped or otherwise convert/map
idle_payout.owner and idle_payout.receiver into wire/mapped forms) immediately
after obtaining idle_payout and before calling
withdrawal_settled/execute_effects/creating transfer_effects, or alternatively
populate CompletedWithdrawal with the already-mapped wire values so subsequent
code uses mapped addresses; update uses of CompletedWithdrawal,
withdrawal_settled, transfer_effects, and ensure_effect_addresses_mapped
accordingly.

---

Duplicate comments:
In `@contract/vault/soroban/tests/blend_e2e.rs`:
- Around line 36-42: The match currently routes all non-DepositWithMin
VaultCommand variants through I128Receipt, hiding unsupported command/receipt
mismatches; update the match on VaultCommand to list only the concrete variants
this test actually expects (e.g., VaultCommand::DepositWithMin => decode as
DepositReceipt and each other used command => decode with the correct receipt
type like I128Receipt) and replace the wildcard arm with an explicit
unreachable!() or panic with a clear message (e.g., unreachable!("unsupported
VaultCommand in helper")) so future unsupported commands fail loudly; keep the
existing .map_err(|_| templar_soroban_runtime::ContractError::InvalidInput)
behavior for decode failures.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: a16e5701-7c3e-46e2-8030-bdc6cd467d8e

📥 Commits

Reviewing files that changed from the base of the PR and between 2e834ae and 35919b1.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (23)
  • Cargo.toml
  • contract/proxy-4626-soroban/Cargo.toml
  • contract/proxy-4626-soroban/src/contract.rs
  • contract/proxy-4626-soroban/src/lib.rs
  • contract/proxy-4626-soroban/src/tests.rs
  • contract/vault/curator-primitives/src/policy/cap_group/mod.rs
  • contract/vault/curator-primitives/src/policy/cooldown/mod.rs
  • contract/vault/curator-primitives/src/policy/state/mod.rs
  • contract/vault/curator-primitives/src/policy/supply_queue/mod.rs
  • contract/vault/kernel/src/actions/mod.rs
  • contract/vault/soroban/README.md
  • contract/vault/soroban/governance/src/tests.rs
  • contract/vault/soroban/scripts/vault_payload.py
  • contract/vault/soroban/shared-types/src/lib.rs
  • contract/vault/soroban/src/contract/curator_vault.rs
  • contract/vault/soroban/src/contract/entrypoints.rs
  • contract/vault/soroban/src/contract/mod.rs
  • contract/vault/soroban/src/contract/types.rs
  • contract/vault/soroban/src/tests.rs
  • contract/vault/soroban/tests/blend_e2e.rs
  • contract/vault/soroban/tests/integration_tests.rs
  • test-utils/examples/registry_add_version_args.rs
  • test-utils/src/lib.rs

Comment thread contract/proxy-4626-soroban/src/contract.rs Outdated
Comment thread contract/vault/soroban/tests/integration_tests.rs
@carrion256
carrion256 force-pushed the codex/soroban-execute-receipts branch 3 times, most recently from 9cd1e3c to 9e1f010 Compare June 5, 2026 22:32

@coderabbitai coderabbitai Bot 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.

Warning

CodeRabbit couldn't request changes on this pull request because it doesn't have sufficient GitHub permissions.

Please grant CodeRabbit Pull requests: Read and write permission and re-run the review.

👉 Steps to fix this

Actionable comments posted: 5

♻️ Duplicate comments (1)
contract/vault/soroban/tests/blend_e2e.rs (1)

36-42: 🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Enumerate the supported receipt decoders here.

The wildcard I128Receipt arm lets any future VaultCommand compile through this helper even when its receipt is a different type, so unsupported command/receipt pairings degrade into ambiguous InvalidInput failures instead of failing obviously in the test. Match only the commands this file actually exercises.

Suggested change
     match command {
         VaultCommand::DepositWithMin { .. } => DepositReceipt::decode(&bytes)
             .map(|receipt| receipt.shares_out)
             .map_err(|_| templar_soroban_runtime::ContractError::InvalidInput),
-        _ => I128Receipt::decode(&bytes)
+        VaultCommand::Allocate { .. } | VaultCommand::RefreshMarkets { .. } => {
+            I128Receipt::decode(&bytes)
             .map(|receipt| receipt.value)
-            .map_err(|_| templar_soroban_runtime::ContractError::InvalidInput),
+            .map_err(|_| templar_soroban_runtime::ContractError::InvalidInput)
+        }
+        _ => panic!("unsupported command for execute_command helper"),
     }

As per coding guidelines: "Prefer parsing over validation in Rust code; express invariants in types wherever practical, and make invalid states unrepresentable."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@contract/vault/soroban/tests/blend_e2e.rs` around lines 36 - 42, The match
currently defaults to decoding every non-DepositWithMin command with
I128Receipt, which hides unsupported command/receipt mismatches; update the
match in the helper so it enumerates only the VaultCommand variants exercised by
these tests (e.g., VaultCommand::DepositWithMin ->
DepositReceipt::decode(...).map(|r| r.shares_out) and explicitly match each
other tested variant to its correct decoder (e.g., VaultCommand::SomeOtherCmd ->
SomeOtherReceipt::decode(...).map(|r| r.field)), and add a final _ => return
Err(templar_soroban_runtime::ContractError::InvalidInput) to fail fast for
unknown commands rather than silently decoding with I128Receipt.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@contract/proxy-4626-soroban/src/contract.rs`:
- Around line 697-700: The current address_from_wire uses
validate_address_strkey then calls Address::from_str which can panic; wrap the
conversion in a panic-safe guard and return ContractError::InvalidInput on
failure. Specifically, inside address_from_wire keep or run
validate_address_strkey(value.as_bytes()) and then call Address::from_str(env,
value) inside std::panic::catch_unwind (or use any non-panicking SDK helper if
available) and map any panic or invalid result to
Err(ContractError::InvalidInput) so the function never panics while still
returning Ok(Address) on success.

In `@contract/vault/soroban/shared-types/src/lib.rs`:
- Around line 583-589: The Completed receipt variant is exposing owner and
receiver as unchecked String; change those fields to a validated StrKey newtype
(e.g., ReceiptAddress or StrKeyAddress) so decoding enforces address format at
receipt parse time. Add a newtype that wraps the existing StrKey/Address
validator used elsewhere (implement Deserialize/Serialize, FromStr, Display, and
TryFrom<String>) and replace owner: String and receiver: String in the Completed
enum variant with owner: ReceiptAddress and receiver: ReceiptAddress; update any
receipt decode/construct paths (the receipt decode function and any
builders/constructors that create Completed) to validate/convert strings into
ReceiptAddress and update call-sites to use the new type (or call .to_string()
only where a raw string is absolutely needed). Ensure the newtype reuses the
repo's canonical strkey/address validation utilities to avoid duplicated logic.

In `@contract/vault/soroban/src/contract/entrypoints.rs`:
- Around line 1208-1224: The branch handlers (VaultCommand::AtomicWithdraw,
AtomicRedeem, Allocate, RefreshMarkets) all return an I128Receipt via
encode_receipt, but the meaning of its field `value` differs per command; change
to command-specific receipt types (e.g., AtomicWithdrawReceipt,
AtomicRedeemReceipt, AllocateReceipt, RefreshMarketsReceipt) or a single enum
Receipt that encodes the command tag plus semantic field names, update
encode_receipt to accept these new types (or the enum) and their encode
implementations, and replace the use of I128Receipt::encode in the
atomic_withdraw_impl/atomic_redeem_impl/allocate_impl/refresh_markets_impl
return paths so the ABI bytes are self-describing rather than reusing
I128Receipt.

In `@contract/vault/soroban/src/contract/types.rs`:
- Around line 45-50: The current ExecuteWithdrawResult struct allows invalid
combinations of summary and completed; replace it with an enum (e.g.,
ExecuteWithdrawResult::Payout { summary: EffectSummary, completed:
CompletedWithdrawal } and ExecuteWithdrawResult::NoPayout { summary:
EffectSummary }) so the two wire receipt variants are represented explicitly;
update all construction sites and pattern matches that currently access
ExecuteWithdrawResult.summary or .completed to use the new enum variants and
adjust any (de)serialization/derive attributes used on ExecuteWithdrawResult to
match the enum shape.

In `@contract/vault/soroban/tests/integration_tests.rs`:
- Around line 1354-1367: The test verifies that execute_withdraw returns a
NoPayout receipt and validates the status fields, but never checks whether the
queued restricted request was actually consumed from the queue. After the
existing assertions on the status object (op_state_before, op_state_after,
assets_transferred, events_emitted), add an assertion that verifies the queue
state to confirm that the restricted head request was dequeued or marked as
consumed by the NoPayout operation.

---

Duplicate comments:
In `@contract/vault/soroban/tests/blend_e2e.rs`:
- Around line 36-42: The match currently defaults to decoding every
non-DepositWithMin command with I128Receipt, which hides unsupported
command/receipt mismatches; update the match in the helper so it enumerates only
the VaultCommand variants exercised by these tests (e.g.,
VaultCommand::DepositWithMin -> DepositReceipt::decode(...).map(|r|
r.shares_out) and explicitly match each other tested variant to its correct
decoder (e.g., VaultCommand::SomeOtherCmd ->
SomeOtherReceipt::decode(...).map(|r| r.field)), and add a final _ => return
Err(templar_soroban_runtime::ContractError::InvalidInput) to fail fast for
unknown commands rather than silently decoding with I128Receipt.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 1c0ce67b-f7d9-4737-b4c9-3a3083136351

📥 Commits

Reviewing files that changed from the base of the PR and between 2e834ae and 9e1f010.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (24)
  • Cargo.toml
  • contract/proxy-4626-soroban/Cargo.toml
  • contract/proxy-4626-soroban/src/contract.rs
  • contract/proxy-4626-soroban/src/lib.rs
  • contract/proxy-4626-soroban/src/tests.rs
  • contract/vault/curator-primitives/src/policy/cap_group/mod.rs
  • contract/vault/curator-primitives/src/policy/cooldown/mod.rs
  • contract/vault/curator-primitives/src/policy/state/mod.rs
  • contract/vault/curator-primitives/src/policy/supply_queue/mod.rs
  • contract/vault/kernel/src/actions/mod.rs
  • contract/vault/soroban/README.md
  • contract/vault/soroban/governance/src/tests.rs
  • contract/vault/soroban/scripts/vault_payload.py
  • contract/vault/soroban/shared-types/src/lib.rs
  • contract/vault/soroban/src/contract/curator_vault.rs
  • contract/vault/soroban/src/contract/entrypoints.rs
  • contract/vault/soroban/src/contract/helpers.rs
  • contract/vault/soroban/src/contract/mod.rs
  • contract/vault/soroban/src/contract/types.rs
  • contract/vault/soroban/src/tests.rs
  • contract/vault/soroban/tests/blend_e2e.rs
  • contract/vault/soroban/tests/integration_tests.rs
  • test-utils/examples/registry_add_version_args.rs
  • test-utils/src/lib.rs

Comment thread contract/proxy-4626-soroban/src/contract.rs Outdated
Comment thread contract/vault/soroban/shared-types/src/lib.rs
Comment thread contract/vault/soroban/src/contract/entrypoints.rs
Comment thread contract/vault/soroban/src/contract/types.rs
Comment thread contract/vault/soroban/tests/integration_tests.rs

@coderabbitai coderabbitai Bot 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.

Warning

CodeRabbit couldn't request changes on this pull request because it doesn't have sufficient GitHub permissions.

Please grant CodeRabbit Pull requests: Read and write permission and re-run the review.

👉 Steps to fix this

Actionable comments posted: 3

♻️ Duplicate comments (3)
contract/vault/soroban/src/contract/types.rs (1)

45-50: 🛠️ Refactor suggestion | 🟠 Major | 🏗️ Heavy lift

Represent execute-withdraw outcomes as explicit variants.

ExecuteWithdrawResult { summary, completed: Option<_> } still admits impossible combinations and leaves the internal model looser than the wire receipt shape. An enum with NoPayout { summary } and Completed { summary, completed } keeps the runtime and receipt contracts aligned.

As per coding guidelines, **/*.rs should “prefer stronger types over loosely constrained values” and “make invalid states unrepresentable.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@contract/vault/soroban/src/contract/types.rs` around lines 45 - 50, Replace
the loose struct ExecuteWithdrawResult with a closed enum to make impossible
states unrepresentable: define enum ExecuteWithdrawResult { NoPayout { summary:
EffectSummary }, Completed { summary: EffectSummary, completed:
CompletedWithdrawal } } (keeping existing derives like Clone/PartialEq/Eq and
cfg_attr for Debug) and update all usages that construct or pattern-match
ExecuteWithdrawResult to use the NoPayout and Completed variants instead of
checking completed: Option<_>; ensure any serialization/deserialization or
receipt mapping mirrors the new enum shape so the runtime and wire receipt
remain aligned.

Source: Coding guidelines

contract/vault/soroban/src/contract/entrypoints.rs (1)

1208-1264: 🛠️ Refactor suggestion | 🟠 Major | 🏗️ Heavy lift

Make scalar execute receipts command-specific.

These four branches all return I128Receipt { value }, but value means shares burned, assets out, or new external assets depending on the command. The receipt bytes are therefore not self-describing, which undercuts the move to typed execute receipts.

As per coding guidelines, **/*.rs should “prefer stronger types over loosely constrained values” and “make invalid states unrepresentable.”

Source: Coding guidelines

contract/vault/soroban/shared-types/src/lib.rs (1)

583-589: 🛠️ Refactor suggestion | 🟠 Major | 🏗️ Heavy lift

Decode receipt addresses into a validated type.

Completed still stores owner and receiver as arbitrary Strings, so malformed receipt bytes can pass codec parsing and rely on every consumer to remember a second validation step. Wrap these fields in a validated StrKey newtype and enforce validate_address_strkey during decode.

Based on learnings and coding guidelines, this receipt boundary is security-relevant, and **/*.rs should “express invariants in types wherever practical” and “make invalid states unrepresentable.”

Also applies to: 943-979

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@contract/vault/soroban/shared-types/src/lib.rs` around lines 583 - 589, The
Completed receipt variant currently stores owner and receiver as raw String;
change those fields to a validated StrKey newtype (e.g., AddressStrKey or reuse
existing StrKey) and update the Decodable/codec implementation where Completed
is parsed to call validate_address_strkey on the incoming strings, failing
decode on invalid addresses so malformed receipt bytes cannot produce a
Completed with arbitrary strings; update the Completed definition and its decode
logic (and similarly adjust the other receipt variants/locations mentioned
around the 943-979 region) to use the new type and validation function so the
invariant is enforced at type level.

Sources: Coding guidelines, Learnings

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@contract/proxy-4626-soroban/src/tests.rs`:
- Around line 516-549: Add tests mirroring
test_deposit_rejects_receipt_with_trailing_bytes for the other two receipt
decoders: create malformed_receipt bytes from RequestWithdrawReceipt and
ExecuteWithdrawReceipt by encoding a valid struct and appending trailing bytes,
call the corresponding proxy entrypoints (use
Soroban4626ProxyContract::request_withdraw and
Soroban4626ProxyContract::execute_withdraw via fixture.env.as_contract with
Fixture::new, fixture.initialize, fixture.set_next_receipt), assert the call
returns Err(ContractError::InvalidInput), and verify fixture.recorded_payloads()
contains exactly one payload whose decode_command equals the expected
WireVaultCommand for that operation; to avoid duplication, parameterize these
cases with #[rstest] or a small helper that takes the receipt-constructor,
proxy-method-invocation closure, and expected WireVaultCommand.

In `@contract/vault/curator-primitives/src/policy/supply_queue/mod.rs`:
- Around line 178-181: In supply_queue::mod.rs where is_full() is used, replace
the unreachable None-branch that currently returns
Err(SupplyQueueError::LengthOverflow) with an explicit unreachable!() because
is_full() guarantees self.max_length is Some; specifically update the pattern
match in the code that does `let Some(max_length) = self.max_length else { ...
};` to call unreachable!("is_full() guarantees max_length is Some") in the else
branch and keep returning Err(SupplyQueueError::QueueFull { max_length }); so
the error semantics remain correct (refer to is_full(),
SupplyQueueError::QueueFull and SupplyQueueError::LengthOverflow).

In `@contract/vault/soroban/shared-types/src/lib.rs`:
- Around line 15-82: Add unit tests that call validate_address_strkey (and
indirectly crc16_xmodem) with explicit accept/reject vectors: include at least
one valid ACCOUNT_STRKEY and one valid CONTRACT_STRKEY to assert Ok(()), plus
separate failing inputs for bad checksum (corrupt trailing bytes so expected !=
actual), bad alphabet (introduce characters outside 'A'-'Z' and '2'-'7'), bad
length (shorter and longer than STRKEY_LEN), and bad version byte (valid base32
payload but with out[0] not equal to ACCOUNT_VERSION or CONTRACT_VERSION) to
assert Err(CodecError::InvalidEncoding); place tests near other strkey tests and
use the constants STRKEY_LEN, BINARY_LEN, ACCOUNT_VERSION, CONTRACT_VERSION to
construct/validate vectors so the parser behavior cannot silently drift.

---

Duplicate comments:
In `@contract/vault/soroban/shared-types/src/lib.rs`:
- Around line 583-589: The Completed receipt variant currently stores owner and
receiver as raw String; change those fields to a validated StrKey newtype (e.g.,
AddressStrKey or reuse existing StrKey) and update the Decodable/codec
implementation where Completed is parsed to call validate_address_strkey on the
incoming strings, failing decode on invalid addresses so malformed receipt bytes
cannot produce a Completed with arbitrary strings; update the Completed
definition and its decode logic (and similarly adjust the other receipt
variants/locations mentioned around the 943-979 region) to use the new type and
validation function so the invariant is enforced at type level.

In `@contract/vault/soroban/src/contract/types.rs`:
- Around line 45-50: Replace the loose struct ExecuteWithdrawResult with a
closed enum to make impossible states unrepresentable: define enum
ExecuteWithdrawResult { NoPayout { summary: EffectSummary }, Completed {
summary: EffectSummary, completed: CompletedWithdrawal } } (keeping existing
derives like Clone/PartialEq/Eq and cfg_attr for Debug) and update all usages
that construct or pattern-match ExecuteWithdrawResult to use the NoPayout and
Completed variants instead of checking completed: Option<_>; ensure any
serialization/deserialization or receipt mapping mirrors the new enum shape so
the runtime and wire receipt remain aligned.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 59a4f155-ad7e-4097-be22-14bf586717fd

📥 Commits

Reviewing files that changed from the base of the PR and between 9e1f010 and dd83754.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (24)
  • Cargo.toml
  • contract/proxy-4626-soroban/Cargo.toml
  • contract/proxy-4626-soroban/src/contract.rs
  • contract/proxy-4626-soroban/src/lib.rs
  • contract/proxy-4626-soroban/src/tests.rs
  • contract/vault/curator-primitives/src/policy/cap_group/mod.rs
  • contract/vault/curator-primitives/src/policy/cooldown/mod.rs
  • contract/vault/curator-primitives/src/policy/state/mod.rs
  • contract/vault/curator-primitives/src/policy/supply_queue/mod.rs
  • contract/vault/kernel/src/actions/mod.rs
  • contract/vault/soroban/README.md
  • contract/vault/soroban/governance/src/tests.rs
  • contract/vault/soroban/scripts/vault_payload.py
  • contract/vault/soroban/shared-types/src/lib.rs
  • contract/vault/soroban/src/contract/curator_vault.rs
  • contract/vault/soroban/src/contract/entrypoints.rs
  • contract/vault/soroban/src/contract/helpers.rs
  • contract/vault/soroban/src/contract/mod.rs
  • contract/vault/soroban/src/contract/types.rs
  • contract/vault/soroban/src/tests.rs
  • contract/vault/soroban/tests/blend_e2e.rs
  • contract/vault/soroban/tests/integration_tests.rs
  • test-utils/examples/registry_add_version_args.rs
  • test-utils/src/lib.rs

Comment thread contract/proxy-4626-soroban/src/tests.rs
Comment thread contract/vault/curator-primitives/src/policy/supply_queue/mod.rs
Comment thread contract/vault/soroban/shared-types/src/lib.rs
@carrion256
carrion256 force-pushed the codex/soroban-execute-receipts branch from dd83754 to d704cbf Compare June 8, 2026 09:05

@coderabbitai coderabbitai Bot 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.

Warning

CodeRabbit couldn't request changes on this pull request because it doesn't have sufficient GitHub permissions.

Please grant CodeRabbit Pull requests: Read and write permission and re-run the review.

👉 Steps to fix this

Actionable comments posted: 1

♻️ Duplicate comments (1)
contract/proxy-4626-soroban/src/tests.rs (1)

518-551: 🧹 Nitpick | 🔵 Trivial | ⚡ Quick win

Malformed receipt test coverage incomplete for RequestWithdraw and ExecuteWithdraw paths.

The test test_deposit_rejects_receipt_with_trailing_bytes validates trailing-byte rejection for DepositReceipt, but parallel tests for RequestWithdrawReceipt (used by withdraw, redeem, request_withdraw) and ExecuteWithdrawReceipt (used by execute_withdraw) are missing. All three decoders enforce ensure_finished() per the PR objectives, so all three paths should be tested for robustness.

Add parameterized malformed-receipt tests for the remaining decoders:

#[rstest]
#[case::deposit(/* DepositReceipt + trailing */)]
#[case::request_withdraw(/* RequestWithdrawReceipt + trailing */)]
#[case::execute_withdraw(/* ExecuteWithdrawReceipt + trailing */)]
fn test_receipt_rejects_trailing_bytes(#[case] malformed: Vec<u8>, #[case] entrypoint: ...) {
    // arrange: fixture.set_next_receipt(&malformed)
    // act: call entrypoint
    // assert: Err(ContractError::InvalidInput)
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@contract/proxy-4626-soroban/src/tests.rs` around lines 518 - 551, Extend test
coverage beyond test_deposit_rejects_receipt_with_trailing_bytes by adding
parameterized tests that assert trailing-byte receipts are rejected for the
other decoders: construct malformed vectors by encoding RequestWithdrawReceipt
and ExecuteWithdrawReceipt (like how DepositReceipt was encoded then extended
with extra bytes), call the matching entrypoints (withdraw, redeem,
request_withdraw for RequestWithdrawReceipt and execute_withdraw for
ExecuteWithdrawReceipt) via the fixture (use Fixture::set_next_receipt to inject
the malformed receipt and call the contract through fixture.env.as_contract),
and assert the call returns Err(ContractError::InvalidInput); use rstest #[case]
variants to drive the three cases so the same assertion logic covers
DepositReceipt (existing), RequestWithdrawReceipt, and ExecuteWithdrawReceipt.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@contract/vault/soroban/shared-types/src/lib.rs`:
- Around line 1369-1375: Test uses identical addresses for
ExecuteWithdrawReceipt::Completed (owner and receiver), which hides layout
errors; update the test fixture so owner and receiver are distinct values (e.g.,
introduce a second fixture function or constant like receipt_address_alt() and
use that for receiver or owner) and prefer using two different accepted StrKey
variants for the pair to ensure the new owner field is actually pinned in the
wire layout and roundtrip serialization/deserialization.

---

Duplicate comments:
In `@contract/proxy-4626-soroban/src/tests.rs`:
- Around line 518-551: Extend test coverage beyond
test_deposit_rejects_receipt_with_trailing_bytes by adding parameterized tests
that assert trailing-byte receipts are rejected for the other decoders:
construct malformed vectors by encoding RequestWithdrawReceipt and
ExecuteWithdrawReceipt (like how DepositReceipt was encoded then extended with
extra bytes), call the matching entrypoints (withdraw, redeem, request_withdraw
for RequestWithdrawReceipt and execute_withdraw for ExecuteWithdrawReceipt) via
the fixture (use Fixture::set_next_receipt to inject the malformed receipt and
call the contract through fixture.env.as_contract), and assert the call returns
Err(ContractError::InvalidInput); use rstest #[case] variants to drive the three
cases so the same assertion logic covers DepositReceipt (existing),
RequestWithdrawReceipt, and ExecuteWithdrawReceipt.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: a436c094-d0b0-421a-9303-560c68d573c8

📥 Commits

Reviewing files that changed from the base of the PR and between dd83754 and d704cbf.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (24)
  • Cargo.toml
  • contract/proxy-4626-soroban/Cargo.toml
  • contract/proxy-4626-soroban/src/contract.rs
  • contract/proxy-4626-soroban/src/lib.rs
  • contract/proxy-4626-soroban/src/tests.rs
  • contract/vault/curator-primitives/src/policy/cap_group/mod.rs
  • contract/vault/curator-primitives/src/policy/cooldown/mod.rs
  • contract/vault/curator-primitives/src/policy/state/mod.rs
  • contract/vault/curator-primitives/src/policy/supply_queue/mod.rs
  • contract/vault/kernel/src/actions/mod.rs
  • contract/vault/soroban/README.md
  • contract/vault/soroban/governance/src/tests.rs
  • contract/vault/soroban/scripts/vault_payload.py
  • contract/vault/soroban/shared-types/src/lib.rs
  • contract/vault/soroban/src/contract/curator_vault.rs
  • contract/vault/soroban/src/contract/entrypoints.rs
  • contract/vault/soroban/src/contract/helpers.rs
  • contract/vault/soroban/src/contract/mod.rs
  • contract/vault/soroban/src/contract/types.rs
  • contract/vault/soroban/src/tests.rs
  • contract/vault/soroban/tests/blend_e2e.rs
  • contract/vault/soroban/tests/integration_tests.rs
  • test-utils/examples/registry_add_version_args.rs
  • test-utils/src/lib.rs

Comment thread contract/vault/soroban/shared-types/src/lib.rs
@carrion256
carrion256 force-pushed the codex/soroban-execute-receipts branch from c61d914 to 82841dc Compare June 8, 2026 09:31
@carrion256
carrion256 force-pushed the codex/soroban-execute-receipts branch from 82841dc to df5f9aa Compare June 8, 2026 10:48

@coderabbitai coderabbitai Bot 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.

Warning

CodeRabbit couldn't request changes on this pull request because it doesn't have sufficient GitHub permissions.

Please grant CodeRabbit Pull requests: Read and write permission and re-run the review.

👉 Steps to fix this

Actionable comments posted: 2

♻️ Duplicate comments (3)
contract/vault/soroban/shared-types/src/lib.rs (2)

1102-1105: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Add a second receipt-address fixture for Completed receipt tests.

Using the same fixture for both owner and receiver means swapped or duplicated fields in the wire layout would still pass. Make them distinct—ideally covering both accepted StrKey variants—to actually pin both fields. Based on learnings, this receipt wire contract is security-relevant.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@contract/vault/soroban/shared-types/src/lib.rs` around lines 1102 - 1105,
Create a second distinct fixture function (e.g., receipt_address_b or
receipt_address_completed) that returns a different ReceiptAddress via
ReceiptAddress::from_str(...).expect(...), using a different valid StrKey string
than the existing receipt_address() so owner and receiver in Completed receipt
tests are unique; update the Completed receipt tests to use receipt_address()
and the new fixture respectively to ensure swapped/duplicated fields fail the
wire-contract checks.

Source: Learnings


22-89: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Add direct accept/reject vectors for validate_address_strkey.

This parser is now a receipt-boundary security check, but the tests only exercise it indirectly through ReceiptAddress and receipt decode. Please add explicit valid G.../C... cases plus bad checksum, alphabet, length, and version-byte cases so validator drift fails fast. Based on learnings, receipt serialization changes in contract/vault/soroban/**/*.rs are security-relevant.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@contract/vault/soroban/shared-types/src/lib.rs` around lines 22 - 89, The
validate_address_strkey function is only exercised indirectly; add direct unit
tests that call validate_address_strkey and cover explicit accept/reject
vectors: include valid G... (ACCOUNT_VERSION) and C... (CONTRACT_VERSION)
base32-encoded 56-byte STRKEY_LEN inputs that decode to BINARY_LEN bytes, and
negative cases for bad checksum (mismatched crc16_xmodem), invalid alphabet
characters, incorrect length, and wrong version byte values; reference the
symbols validate_address_strkey, crc16_xmodem, ACCOUNT_VERSION,
CONTRACT_VERSION, STRKEY_LEN and BINARY_LEN when creating tests so failures map
directly to the parser and will catch receipt-serialization regressions.

Source: Learnings

contract/vault/soroban/tests/blend_e2e.rs (1)

29-43: 🧹 Nitpick | 🔵 Trivial | ⚡ Quick win

Enumerate the supported receipt pairings in execute_command.

The _ branch makes every non-deposit command implicitly “expects I128Receipt”, so a future test addition falls back to a generic decode error instead of an obvious unsupported-command failure. List the actual commands used here (Allocate, RefreshMarkets, etc.) and reject the rest explicitly. As per coding guidelines, prefer parsing over validation and make invalid states unrepresentable.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@contract/vault/soroban/tests/blend_e2e.rs` around lines 29 - 43, The
execute_command function currently treats every non-DepositWithMin as returning
an I128Receipt; update execute_command to explicitly match each VaultCommand
variant and decode to the correct receipt type (e.g., VaultCommand::Allocate =>
I128Receipt, VaultCommand::RefreshMarkets => I128Receipt,
VaultCommand::SomeOtherVariant => SomeOtherReceipt) instead of using the
wildcard arm, and for any unsupported/unknown VaultCommand variants return a
clear templar_soroban_runtime::ContractError::UnsupportedCommand (or a new
explicit UnsupportedCommand error) so invalid/unsupported commands fail with a
descriptive error rather than a generic decode failure; locate the
execute_command function and the VaultCommand enum to map each variant to its
expected receipt decoder and add the explicit error branch for remaining cases.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@contract/proxy-4626-soroban/src/tests.rs`:
- Around line 115-118: The NextReceipt flag is treated as a permanent override
because set_next_receipt writes MockVaultDataKey::NextReceipt but
MockVaultContract::execute never clears it; update the contract to consume the
injected receipt instead of leaving it sticky by removing
MockVaultDataKey::NextReceipt when execute reads it (i.e., read-and-remove from
env.storage().instance() so the injected blob is one-shot), or alternatively
change set_next_receipt/execute semantics so execute explicitly clears the key
after using it to ensure later calls don't reuse the same receipt.

In `@contract/vault/soroban/tests/integration_tests.rs`:
- Around line 1270-1292: Add an assertion that the queued withdrawal was
actually removed after execute_withdraw completes: after calling
proxy.execute_withdraw (which yields ExecuteWithdrawReceipt::Completed) read the
contract state (via env.as_contract / the same contract proxy) and assert the
request queue no longer contains the request_id (or that the queue length is
decremented/empty and any queued entry for request_id is absent); update the
test around execute_withdraw/ExecuteWithdrawReceipt::Completed to fetch the
queue (e.g., call the contract method that returns queued requests or inspect
the same storage item used by withdraw/redeem) and assert the queued request is
drained to prevent a regression where the receipt is returned before state
mutation persists.

---

Duplicate comments:
In `@contract/vault/soroban/shared-types/src/lib.rs`:
- Around line 1102-1105: Create a second distinct fixture function (e.g.,
receipt_address_b or receipt_address_completed) that returns a different
ReceiptAddress via ReceiptAddress::from_str(...).expect(...), using a different
valid StrKey string than the existing receipt_address() so owner and receiver in
Completed receipt tests are unique; update the Completed receipt tests to use
receipt_address() and the new fixture respectively to ensure swapped/duplicated
fields fail the wire-contract checks.
- Around line 22-89: The validate_address_strkey function is only exercised
indirectly; add direct unit tests that call validate_address_strkey and cover
explicit accept/reject vectors: include valid G... (ACCOUNT_VERSION) and C...
(CONTRACT_VERSION) base32-encoded 56-byte STRKEY_LEN inputs that decode to
BINARY_LEN bytes, and negative cases for bad checksum (mismatched crc16_xmodem),
invalid alphabet characters, incorrect length, and wrong version byte values;
reference the symbols validate_address_strkey, crc16_xmodem, ACCOUNT_VERSION,
CONTRACT_VERSION, STRKEY_LEN and BINARY_LEN when creating tests so failures map
directly to the parser and will catch receipt-serialization regressions.

In `@contract/vault/soroban/tests/blend_e2e.rs`:
- Around line 29-43: The execute_command function currently treats every
non-DepositWithMin as returning an I128Receipt; update execute_command to
explicitly match each VaultCommand variant and decode to the correct receipt
type (e.g., VaultCommand::Allocate => I128Receipt, VaultCommand::RefreshMarkets
=> I128Receipt, VaultCommand::SomeOtherVariant => SomeOtherReceipt) instead of
using the wildcard arm, and for any unsupported/unknown VaultCommand variants
return a clear templar_soroban_runtime::ContractError::UnsupportedCommand (or a
new explicit UnsupportedCommand error) so invalid/unsupported commands fail with
a descriptive error rather than a generic decode failure; locate the
execute_command function and the VaultCommand enum to map each variant to its
expected receipt decoder and add the explicit error branch for remaining cases.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: ac2a2557-9a49-4e3b-8528-f614745efec9

📥 Commits

Reviewing files that changed from the base of the PR and between c61d914 and df5f9aa.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (24)
  • Cargo.toml
  • contract/proxy-4626-soroban/Cargo.toml
  • contract/proxy-4626-soroban/src/contract.rs
  • contract/proxy-4626-soroban/src/lib.rs
  • contract/proxy-4626-soroban/src/tests.rs
  • contract/vault/curator-primitives/src/policy/cap_group/mod.rs
  • contract/vault/curator-primitives/src/policy/cooldown/mod.rs
  • contract/vault/curator-primitives/src/policy/state/mod.rs
  • contract/vault/curator-primitives/src/policy/supply_queue/mod.rs
  • contract/vault/kernel/src/actions/mod.rs
  • contract/vault/soroban/README.md
  • contract/vault/soroban/governance/src/tests.rs
  • contract/vault/soroban/scripts/vault_payload.py
  • contract/vault/soroban/shared-types/src/lib.rs
  • contract/vault/soroban/src/contract/curator_vault.rs
  • contract/vault/soroban/src/contract/entrypoints.rs
  • contract/vault/soroban/src/contract/helpers.rs
  • contract/vault/soroban/src/contract/mod.rs
  • contract/vault/soroban/src/contract/types.rs
  • contract/vault/soroban/src/tests.rs
  • contract/vault/soroban/tests/blend_e2e.rs
  • contract/vault/soroban/tests/integration_tests.rs
  • test-utils/examples/registry_add_version_args.rs
  • test-utils/src/lib.rs

Comment thread contract/proxy-4626-soroban/src/tests.rs
Comment thread contract/vault/soroban/tests/integration_tests.rs
@carrion256
carrion256 merged commit 72c8a43 into dev Jun 8, 2026
26 checks passed
@carrion256
carrion256 deleted the codex/soroban-execute-receipts branch June 8, 2026 12:21
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.

1 participant