Skip to content

feat(proxy-4626): add Soroban ERC-4626 proxy#418

Merged
carrion256 merged 12 commits into
devfrom
spr/refactor/vault-ergonomics/4641dafc
Jun 5, 2026
Merged

feat(proxy-4626): add Soroban ERC-4626 proxy#418
carrion256 merged 12 commits into
devfrom
spr/refactor/vault-ergonomics/4641dafc

Conversation

@carrion256

@carrion256 carrion256 commented Apr 23, 2026

Copy link
Copy Markdown
Collaborator

@coderabbitai

coderabbitai Bot commented Apr 23, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Soroban ERC-4626 Proxy Contract Addition - Security & Correctness Review

Overview

This PR adds contract/proxy-4626-soroban, a Soroban proxy exposing an ERC-4626-like interface over the Templar vault with intentional deviations and async withdrawal semantics. Also includes supportive changes to governance and proxy-oracle contracts for permissionless TTL extension.

Critical Security & Correctness Points for Human Review

🔴 HIGH PRIORITY

  1. Async Withdrawal Model Deviation

    • withdraw/redeem return u64 request_id (not i128 amount as ERC-4626 expects)
    • Assets only transferred after separate execute_withdraw call succeeds
    • execute_withdraw keyed by caller address (not request_id), implying one pending withdrawal per address
    • Risk: Smart contract and protocol integrators will break if they assume standard ERC-4626 synchronous withdrawal semantics
    • Action: Ensure prominent documentation in function comments; flag in integration tests and client SDKs
  2. Zero Slippage in deposit()

    • deposit() method uses min_shares_out = 0, accepting any minted share amount
    • No slippage protection against vault state changes between preview and execution
    • Contrast: mint() appropriately uses preview result as min_shares_out = shares
    • Risk: Users could receive fewer shares than expected if vault conditions worsen
    • Action: Update function documentation to explicitly recommend deposit_with_min() for production; warn in SDK generated docs
  3. Vault Error Code Mapping Incomplete

    • Error handler maps only codes 3 → InvalidInput, 8 → AlreadyInitialized; all other codes → VaultError
    • Action: Cross-verify against vault's actual error discriminants (templar_soroban_kernel_vault); document mapping or risk silent error loss
  4. No Address Validation During Initialization

    • initialize() accepts vault/asset/share addresses without verifying they are callable contracts
    • Addresses encoded via address_to_wire() but never validated before storage
    • Risk: Malformed addresses cause opaque VaultError in later operations instead of fast-fail at init
    • Action: Add basic validation (e.g., verify address string encoding succeeds, consider code presence check)

🟡 MEDIUM PRIORITY

  1. Delegated Operator Rejection

    • withdraw/redeem enforce operator == owner via require_self_operator(), explicitly rejecting delegated operations
    • EIP-7540 / OpenZeppelin-style operators intentionally unsupported pending future design
    • Current allowance check is guard-only; vault auth/transfer determines actual consumption
    • Risk: Routers, custodians, and batched executors cannot use proxy; forces workarounds
    • Action: Document as known limitation in vault integration guide; flag as future UX enhancement priority
  2. Preview-to-Execution Race Window

    • mint/withdraw/redeem call call_proxy_view() separately from invoke_vault_execute() with computed minimums
    • Vault state could change between calls, but min_shares_out/min_assets_out guards protect against unfavorable changes
    • Risk: If vault's slippage enforcement is weak, users could be sandwiched
    • Action: Verify vault's DepositWithMin/RequestWithdraw minimum output validation is airtight
  3. TTL Extension Permissionless Across All Contracts

    • extend_ttl() made permissionless in governance and proxy-oracle adapter (removed caller param and auth check)
    • No authorization prevents spam, but Soroban metering should bound abuse
    • Risk: Malicious actor could trigger repeated extend_ttl() calls to consume executor bandwidth
    • Action: Verify ops infrastructure has monitoring/alerts for unusual extend_ttl() invocation patterns

🟢 LOWER PRIORITY

  1. mint() Return Value Semantic Quirk

    • mint(operator, shares, receiver) returns assets (amount of asset tokens needed), not computed minted_shares
    • Differs from standard ERC-4626 which returns shares minted
    • Action: Document rationale or verify this matches vault's preview behavior; update naming in comments if intentional
  2. Error Type NotImplemented Unused

    • Error enum defines NotImplemented = 6 variant with zero call sites
    • Benign dead code, but indicates incomplete design
    • Action: Remove in next cleanup pass or document intended use
  3. TTL Extension Threshold

    • INSTANCE_TTL_THRESHOLD = 518_400 (6 days) hardcoded
    • Action: Verify alignment with vault archival policy and integration cooldown requirements

Positive Security Observations

Proper Authorization: All mutating methods call require_auth() on authenticated party; initialization properly guards subsequent reads/writes
Error Mapping: External invocation errors (InvokeError::Contract, InvokeError::Abort) caught and normalized to ContractError
Non-negative Validation: All i128 amounts validated as >= 0 before vault operations
Initialization Guard: Enforced one-time initialization; second calls rejected with AlreadyInitialized; reads always call require_initialized()
Storage Consistency: TTL extended on every require_initialized() call; initialization extends TTL before/after write
Test Coverage: Unit tests (mock vault) and integration tests (live vault + curator) cover deposit/mint/withdraw/redeem flows
No Reentrancy: Soroban's explicit contract invocation semantics prevent callback-based reentrancy
Wire Format Safety: Address encoding via address_to_wire() properly validates UTF-8; decoding via postcard safe

Files Requiring Detailed Line-by-Line Review

  1. contract/proxy-4626-soroban/src/contract.rs (637 lines)

    • Focus: deposit() use of min_shares_out = 0, mint() return value semantics, call_proxy_view() preview logic, invoke_vault_execute() error handling
  2. contract/proxy-4626-soroban/src/error.rs (60 lines)

    • Focus: Verify from_vault_error_code() mappings (3→InvalidInput, 8→AlreadyInitialized) against actual vault error discriminants
  3. contract/vault/soroban/shared-types/src/lib.rs (255+ lines)

    • Focus: Verify tuple-to-struct field conversions for ProxyViewFields/ProxyPreviewFields are correctly indexed

Behavioral Incompatibilities to Document in User Guides

  • Explicit Caller Parameter: Mutations require explicit Address parameter (Soroban idiom, not standard ERC-4626)
  • Async Withdrawals: withdraw/redeem return u64 request_id, not amount; finalization requires separate execute_withdraw() call
  • Zero Default Slippage: deposit() offers no slippage protection by default; users must opt into deposit_with_min() for safety
  • Single Pending Withdrawal: One queued withdrawal per address maximum (enforced by vault)
  • No Delegated Operators: Withdrawal/redeem operators must be owners; router/custodian delegation unsupported pending future redesign

Integration Test Coverage Assessment

✅ Tests verify successful initialization, double-init rejection, input validation, vault command encoding, event emission
✅ Integration tests exercise full deposit→request_withdraw→execute_withdraw flow with real vault + curator
⚠️ Gap: No explicit test for deposit() zero-slippage edge case or sandwich vulnerability
⚠️ Gap: No test for delegated operator failure path returning InsufficientAllowance

Walkthrough

This PR adds a new Soroban ERC-4626 proxy crate that delegates previews and executions to a configured vault, introduces error types and shared proxy view schemas, implements full proxy entrypoints (deposit/mint/withdraw/redeem/queued flows), and includes unit and integration tests plus a small build recipe update.

Changes

Soroban ERC-4626 Proxy Contract

Layer / File(s) Summary
Workspace & crate manifest, lib & re-exports
Cargo.toml, contract/proxy-4626-soroban/Cargo.toml, contract/proxy-4626-soroban/src/lib.rs
Adds workspace member, crate manifest for templar-4626-proxy-soroban, and re-exports contract and shared proxy view types.
Shared proxy view types
contract/vault/soroban/shared-types/src/lib.rs
Adds proxy ABI/view tuple aliases, named *Fields structs, and From conversions mapping tuple responses into nested structs; includes a unit test.
Contract implementation and vault integration
contract/proxy-4626-soroban/src/contract.rs
Implements VaultCommand encoding, instance storage keys, proxy entrypoints (deposit/mint/withdraw/redeem/request_withdraw/execute_withdraw/initialize), view methods delegating to vault or share token, vault invoke/preview plumbing, typed result extraction, auth/allowance checks, and deprecated event emitters.
Contract errors
contract/proxy-4626-soroban/src/error.rs
Defines on-chain ContractError, off-chain RuntimeError, conversions from codec errors, and mapping from vault error codes.
Unit tests with mock infrastructure
contract/proxy-4626-soroban/src/tests.rs
Adds mock vault/failing vault/share-token, a test Fixture, and unit tests covering initialization, validation, vault-error propagation, command encoding, preview-driven flows, event emission, query behavior, and auth/allowance enforcement.
Integration tests with end-to-end flows
contract/proxy-4626-soroban/tests/integration_tests.rs
Adds integration harness and end-to-end tests validating deposit minting, view-method consistency with vault previews, queued withdraw/request flows with cooldown and execution, and redeem flows.
Build configuration
contract/vault/soroban/justfile
Updates check-4626-proxy recipe to set SOROBAN_SDK_BUILD_SYSTEM_SUPPORTS_SPEC_SHAKING_V2=1 for cargo check.

Permissionless TTL extension & adapter/governance updates

Layer / File(s) Summary
SEP-40 adapter: extend_ttl and tests
contract/proxy-oracle/soroban/sep40-adapter-contract/src/lib.rs, contract/proxy-oracle/soroban/sep40-adapter-contract/src/tests.rs, contract/proxy-oracle/soroban/README.md
Adds Sep40Adapter::extend_ttl, refreshes instance TTL on reads, updates tests to assert TTL refresh, and documents adapter extend_ttl as permissionless.
Governance storage and API
contract/proxy-oracle/soroban/governance-contract/src/lib.rs, contract/proxy-oracle/soroban/governance-contract/src/storage.rs, contract/proxy-oracle/soroban/governance-contract/src/tests.rs
Makes governance extend_ttl public (removes caller param and admin check), adds extend_active_proposal_ttls to refresh active proposal TTLs, and updates tests to reflect permissionless TTL extension.
Integration tests/docs for TTL
contract/proxy-oracle/soroban/integration-tests/tests/ttl_extend.rs
Replaces admin-only TTL test with public-call test invoking governance.extend_ttl() and updates docs.
Vault runtime docs
contract/vault/soroban/README.md
Adds “Runtime TTL and Keeper Responsibility” section describing VaultCommand::ExtendTtl and which runtime storage components are renewed.

Estimated Code Review Effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Suggested Reviewers

  • royalf00l
  • peer2f00l
  • SmolinPavel
🚥 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 docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch spr/refactor/vault-ergonomics/4641dafc

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

Copy link
Copy Markdown
Collaborator Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Apr 24, 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.

@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. Now we have some codegen room the goal for this proxy contract is to have a cleaner interface into the vaults for depositors, rather than the obtuse, opaque interface.

@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 135-145: The Withdraw event is being emitted when only a
withdrawal request is enqueued (via invoke_vault_execute with
VaultCommand::RequestWithdraw), which is incorrect; remove the
emit_withdraw_event call from the request paths (the call in the block using
expect_u64_result/invoke_vault_execute and the similar call at lines ~159-169)
and instead invoke emit_withdraw_event inside execute_withdraw immediately after
the actual asset transfer completes successfully, keeping the same payload
(caller, receiver, owner, assets, shares) and updating any imports/signatures as
necessary so the event payload still compiles and reflects the real transfer.
- Around line 281-303: The initialize function currently allows any caller to
set vault/asset/share addresses; require authorization by verifying the
transaction signer before writing state: update initialize (and add
ProxyDataKey::Admin) to accept or derive an expected admin and assert that
env.invoker() (or env.signer() per your runtime API) equals that admin,
otherwise return ContractError::Unauthorized; store the admin via
env.storage().instance().set(&ProxyDataKey::Admin, &admin) the first time and on
subsequent calls use is_initialized(&env) combined with an authorization check
(use initialize, is_initialized, ProxyDataKey::Admin,
ProxyDataKey::VaultAddress, ProxyDataKey::AssetToken, ProxyDataKey::ShareToken,
ContractError::Unauthorized to locate code).
- Around line 82-189: Public amount parameters can be negative and must be
rejected at the proxy boundary; add non-negative guards at the start of deposit,
mint, withdraw, redeem, and request_withdraw to validate assets, shares, and
min_assets_out (as applicable) are >= 0, and return a ContractError (e.g., an
InvalidAmount/BadInput variant) when violated. Locate the checks in the
functions deposit (assets), mint (shares), withdraw (assets and derived shares),
redeem (shares and derived assets), and request_withdraw (shares and
min_assets_out) and return the error before any require_auth/allowance or
invoke_vault_execute calls so invalid values never reach invoke_vault_execute,
call_proxy_view, or require_auth_or_allowance.
- Around line 353-367: The proxy is letting the vault's numeric error codes 1–6
be decoded into the local ContractError enum via try_invoke_contract::<Bytes,
ContractError>, bypassing from_vault_error_code; change the invocation to use a
generic raw error type (e.g., try_invoke_contract::<Bytes, u32> or the raw
InvokeError) instead of ContractError, then update the match arms around result
(the block handling Ok(Ok(bytes)), Ok(Err(_)), Err(Ok(...)),
Err(Err(invoke_error))) so that any numeric vault code is received as raw data
and you explicitly pass it through ContractError::from_vault_error_code (or map
InvokeError -> from_vault_error_code) before returning, ensuring all vault codes
are mapped via from_vault_error_code rather than being auto-decoded into the
local enum.
🪄 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: bff2656d-2d68-4c38-b31a-78613d5da1d6

📥 Commits

Reviewing files that changed from the base of the PR and between 4f964ca and 6b3f2d0.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (8)
  • .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

Comment thread contract/proxy-4626-soroban/src/contract.rs
Comment thread contract/proxy-4626-soroban/src/contract.rs
Comment thread contract/proxy-4626-soroban/src/contract.rs
Comment thread contract/proxy-4626-soroban/src/contract.rs Outdated
@coderabbitai

coderabbitai Bot commented Apr 24, 2026

Copy link
Copy Markdown
Contributor

Tip

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

🧩 Analysis chain

Let me explore the codebase to give you a thorough description and design critique.


🏁 Script executed:

#!/bin/bash
# Find all files related to proxy-4626 or erc4626
fd --type f | grep -i "4626\|proxy" | head -60

Length of output: 1354


🏁 Script executed:

#!/bin/bash
# Read the main contract files
cat contract/proxy-4626-soroban/src/lib.rs
echo "=== CONTRACT ==="
cat contract/proxy-4626-soroban/src/contract.rs
echo "=== ERROR ==="
cat contract/proxy-4626-soroban/src/error.rs
echo "=== CARGO ==="
cat contract/proxy-4626-soroban/Cargo.toml

Length of output: 19756


🏁 Script executed:

#!/bin/bash
echo "=== TESTS ==="
cat contract/proxy-4626-soroban/src/tests.rs
echo "=== INTEGRATION TESTS ==="
cat contract/proxy-4626-soroban/tests/integration_tests.rs
echo "=== SHARED TYPES (for VaultCommand/VaultCommandResult) ==="
fd --type f | grep -i "shared.types\|shared_types" | head -20

Length of output: 30283


🏁 Script executed:

#!/bin/bash
echo "=== SHARED TYPES LIB ==="
cat contract/vault/soroban/shared-types/src/lib.rs
echo "=== VAULT README ==="
cat contract/vault/README.md 2>/dev/null | head -100
echo "=== VAULT PROXY VIEW (search in vault soroban) ==="
rg -n "proxy_view\|ProxyView" --type rust -A5 contract/vault/soroban/ | head -80

Length of output: 22205


🏁 Script executed:

#!/bin/bash
echo "=== VAULT SOROBAN proxy_view impl ==="
rg -n "proxy_view" --type rust -A20 | head -80
echo "=== CHECK FOR ERC4626 STANDARD COMPLIANCE ISSUES ==="
# Check withdraw/redeem return types against ERC-4626 standard
rg -n "fn withdraw\|fn redeem\|fn deposit\|fn mint" --type rust contract/proxy-4626-soroban/src/contract.rs
echo "=== CHECK FOR MISSING 4626 METHODS ==="
# ERC-4626 standard requires: deposit, mint, withdraw, redeem, asset, totalAssets, totalSupply,
# balanceOf, convertToShares, convertToAssets, previewDeposit, previewMint, previewWithdraw,
# previewRedeem, maxDeposit, maxMint, maxWithdraw, maxRedeem
rg -n "pub fn " contract/proxy-4626-soroban/src/contract.rs

Length of output: 8714


Now I have enough to give a thorough description and design critique. Let me also search the ERC-4626 standard to confirm key interface deviations.

@carrion256 — here is a full description and usability critique for your human reviewers.


PR #418 — Soroban ERC-4626 Proxy: Description & Design Critique

What this PR does

This PR introduces contract/proxy-4626-soroban, a thin adapter contract that wraps the existing Templar Soroban vault behind a familiar ERC-4626 tokenized vault standard interface. The motivation is exactly as stated: the underlying vault's API is deliberately low-level for gas and cross-chain reasons — mutations go through a single binary-encoded execute(Bytes) entrypoint with a hand-rolled VaultCommand codec, and reads go through proxy_view(owner, assets, shares) which returns a deeply-nested anonymous tuple (ProxyCoreView, ProxyPolicyView, ProxyPreviewView) where ProxyPreviewView is literally an 8-tuple of i128 values accessed by position (e.g., preview.6, preview.7). This proxy hides all of that from depositors.

The proxy exposes the complete ERC-4626 surface:

  • Mutations: deposit, mint, withdraw, redeem
  • Views: asset, total_assets, total_supply, balance_of, convert_to_shares, convert_to_assets
  • Preview methods: preview_deposit, preview_mint, preview_withdraw, preview_redeem
  • Cap methods: max_deposit, max_mint, max_withdraw, max_redeem
  • Token metadata: decimals, name, symbol
  • Vault-specific: request_withdraw, execute_withdraw

It is initialized once with (vault_address, asset_token, share_token) and stores those in instance storage. The proxy itself is stateless beyond that config — all accounting lives in the vault.


Usability Critique

✅ What works well

  1. The main goal is achieved. The entire VaultCommand encode/decode path and the tuple-indexed proxy_view response are fully encapsulated. External callers never touch binary payloads or anonymous indices.
  2. Full view surface. All eight ERC-4626 preview/max/convert methods are implemented and route through proxy_view, so codegen tools and frontends get a consistent query interface.
  3. Delegated withdraw. withdraw and redeem support a caller ≠ owner flow by reading the share token allowance before dispatching, which is correct behavior for operator use cases.
  4. Deposit slippage (on mint). mint correctly calls preview_mint first to discover how many assets are needed, then passes min_shares_out: shares to the vault — ensuring the user gets at least the requested shares.
  5. Events. Deposit and Withdraw events are emitted as required by the ERC-4626 specification when tokens are deposited into or withdrawn from the vault.
  6. Test coverage. Both a mock-vault unit test suite and a real integration test suite with SorobanVaultContract are included and cover the main deposit/withdraw/redeem flows and event emission.

⚠️ Departures from ERC-4626 that integrators must know

1. withdraw and redeem return a request_id (u64), not shares/assets.

withdraw(uint256 assets, address receiver, address owner) in the standard returns uint256 shares, and redeem(uint256 shares, address receiver, address owner) returns assets — it redeems a specific number of shares from owner and sends assets of underlying token from the vault to receiver. Here both return a u64 withdrawal request ID. This is an intentional, necessary deviation given the vault's async cooldown model, but any codegen or aggregator assuming ERC-4626 return types will break. Recommendation: add a prominent comment on both functions explaining this deviation.

2. Withdrawals are not atomic — a two-step pattern is required.

Strategies with lockups or withdrawal buffers should reflect that in previewWithdraw/previewRedeem and events, and document potential delays. Here withdraw and redeem queue a request (shares are escrowed in the vault but assets are not yet transferred), and the user must wait for the cooldown period and then call execute_withdraw to claim assets. The max_withdraw / max_redeem methods correctly return 0 when the vault is not in an idle state, which is the right signal to UIs — but a depositor expecting withdraw to behave atomically (as in ERC-4626) will be surprised when their assets don't arrive immediately.

3. Extra caller parameter on all mutation methods.

Standard ERC-4626 signatures are deposit(assets, receiver) and mint(shares, receiver). The proxy adds an explicit caller as the first argument: deposit(caller, assets, receiver). This is a Soroban platform necessity (require_auth() must be called on a concrete address), but it means any toolchain that generates bindings from the ERC-4626 ABI will produce the wrong call. This should be clearly noted in the README/docs alongside the proxy.

4. deposit has no slippage protection.

deposit calls DepositWithMin { ..., min_shares_out: 0 }. This is fine for the atomic Soroban execution model (no MEV), but integrators accustomed to EVM environments may expect a min-out parameter. If the vault's exchange rate moves between preview and execution (e.g., during a fee accrual checkpoint), the depositor gets whatever shares they get. A note or optional min_shares_out parameter override would be helpful.


🔍 Internal design observations

5. ProxyPreviewView is still an anonymous 8-tuple internally.

Inside the proxy, preview.0 through preview.7 are accessed by index across multiple methods. If the vault's proxy_view response layout ever changes (e.g., a field is inserted), these index accesses silently produce wrong values. A named struct or at minimum named constants (PREVIEW_CONVERT_TO_SHARES: usize = 0, etc.) would make the mapping auditable.

6. Allowance is read but not decremented in the delegated withdrawal path.

In require_auth_or_allowance, the proxy reads allowance(owner, proxy) to gate delegated withdraw/redeem — but never calls approve to decrease it. The vault's own RequestWithdraw handler transfers shares from owner to itself, which presumably consumes the owner's on-chain Soroban auth for that transfer. The proxy's allowance check is therefore a guard, not a spend — its effect depends on whether the vault's auth machinery independently enforces the owner's consent. This coupling should be explicitly documented (or tested with a case where mock_all_auths is not used).

7. NotImplemented error variant is defined but never used.

ContractError::NotImplemented = 6 exists in the error enum with no call sites. This implies some methods were originally planned as stubs. Either remove it or add a comment explaining what it's reserved for.

8. execute_withdraw is keyed by caller, not by request_id.

execute_withdraw(caller) dispatches ExecuteWithdraw { caller } to the vault — the vault finalizes whatever pending withdrawal is associated with that address, not a specific request by ID. This means (a) a user can only have one queued withdrawal at a time, and (b) anyone who has caller's auth can finalize it. The return type is (), so the caller gets no confirmation of how many assets were released. For depositor UX, it would be worth documenting that the request_id returned by withdraw/redeem cannot be used as a handle in execute_withdraw.


Summary

This proxy is a correct and well-structured translation layer. The vault's internal complexity is properly hidden for the deposit and view paths. The main usability risk for integrators is the async withdrawal modelwithdraw and redeem are queuing operations, not instant redemptions, and their return type of u64 (request ID) rather than the ERC-4626-mandated amounts is a silent compatibility break. This should be prominently documented at the contract and README level so that downstream codegen and protocol integrations don't silently misuse it.


🧠 Learnings used
Learnt from: CR
Repo: Templar-Protocol/contracts PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-03-19T05:08:26.156Z
Learning: Read `contract/vault/README.md` and `contract/vault/STRIDE.md` before changing the vault contract

@carrion256
carrion256 force-pushed the spr/refactor/vault-ergonomics/4f330057 branch from 4f964ca to 71ecf80 Compare April 24, 2026 15:03
@carrion256
carrion256 force-pushed the spr/refactor/vault-ergonomics/4641dafc branch 3 times, most recently from 9b352cf to 57e1f69 Compare April 28, 2026 12:08
@carrion256
carrion256 force-pushed the spr/refactor/vault-ergonomics/4f330057 branch 2 times, most recently from 43a9e56 to 559c13f Compare April 28, 2026 13:48
Comment thread contract/proxy-4626-soroban/src/contract.rs
@carrion256
carrion256 force-pushed the spr/refactor/vault-ergonomics/4641dafc branch 2 times, most recently from 91fcfc6 to 6db2fc4 Compare April 30, 2026 15:11
@carrion256
carrion256 changed the base branch from spr/refactor/vault-ergonomics/4f330057 to dev June 5, 2026 08:23
@carrion256
carrion256 force-pushed the spr/refactor/vault-ergonomics/4641dafc branch from 6db2fc4 to 3f4224c Compare June 5, 2026 08:38
@codecov

codecov Bot commented Jun 5, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 72.37113% with 134 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
contract/proxy-4626-soroban/src/contract.rs 73.94% 117 Missing ⚠️
contract/proxy-4626-soroban/src/error.rs 19.04% 17 Missing ⚠️

📢 Thoughts on this report? Let us know!

@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

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

⚠️ Outside diff range comments (1)
contract/proxy-4626-soroban/src/contract.rs (1)

97-105: 🧹 Nitpick | 🔵 Trivial | ⚡ Quick win

Deposit has no slippage protection.

min_shares_out: 0 accepts any share amount, exposing depositors to sandwich attacks. Unlike mint() which uses the requested shares as a minimum, deposit() offers no guarantee on the exchange rate.

Consider adding a min_shares_out parameter or computing a floor from preview (e.g., preview_shares * 99 / 100).

Option A: Add parameter
     pub fn deposit(
         env: Env,
         caller: Address,
         assets: i128,
+        min_shares_out: i128,
         receiver: Address,
     ) -> Result<i128, ContractError> {
         require_non_negative(assets)?;
+        require_non_negative(min_shares_out)?;
         caller.require_auth();
         let shares = expect_i128_result(invoke_vault_execute(
             &env,
             VaultCommand::DepositWithMin {
                 owner: caller.clone(),
                 receiver: receiver.clone(),
                 assets,
-                min_shares_out: 0,
+                min_shares_out,
             },
         )?)?;
🤖 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 97 - 105, The
deposit call currently passes min_shares_out: 0 to VaultCommand::DepositWithMin
(via invoke_vault_execute and expect_i128_result), allowing unlimited slippage;
update the deposit logic to require a min_shares_out and use it instead of 0
(add a min_shares_out: i128 parameter to the deposit function and pass it into
VaultCommand::DepositWithMin), or if you prefer a default safety floor compute
preview_shares first (call the vault preview endpoint with invoke_vault_execute
to get preview_shares) and set min_shares_out = preview_shares * 99 / 100 (or
similar) before calling VaultCommand::DepositWithMin so deposits are protected
from sandwich/slippage attacks.
🤖 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.

Outside diff comments:
In `@contract/proxy-4626-soroban/src/contract.rs`:
- Around line 97-105: The deposit call currently passes min_shares_out: 0 to
VaultCommand::DepositWithMin (via invoke_vault_execute and expect_i128_result),
allowing unlimited slippage; update the deposit logic to require a
min_shares_out and use it instead of 0 (add a min_shares_out: i128 parameter to
the deposit function and pass it into VaultCommand::DepositWithMin), or if you
prefer a default safety floor compute preview_shares first (call the vault
preview endpoint with invoke_vault_execute to get preview_shares) and set
min_shares_out = preview_shares * 99 / 100 (or similar) before calling
VaultCommand::DepositWithMin so deposits are protected from sandwich/slippage
attacks.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 748bdd6c-8415-491e-b13f-b56b36cf6c95

📥 Commits

Reviewing files that changed from the base of the PR and between 7428d71 and 66d92a1.

📒 Files selected for processing (1)
  • contract/proxy-4626-soroban/src/contract.rs

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/proxy-4626-soroban/src/contract.rs (1)

275-300: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Reject negative inputs on the preview/view entrypoints too.

convert_to_shares, convert_to_assets, preview_mint, and preview_withdraw still forward signed negatives into the vault view API. That leaves impossible ERC-4626 states reachable on the read path and makes the proxy return vault-specific behavior instead of a consistent InvalidInput.

Suggested fix
 pub fn convert_to_shares(env: Env, assets: i128) -> Result<i128, ContractError> {
+    require_non_negative(assets)?;
     let preview = call_proxy_view(&env, &env.current_contract_address(), assets, 0)?;
     Ok(preview.convert_to_shares)
 }

 pub fn convert_to_assets(env: Env, shares: i128) -> Result<i128, ContractError> {
+    require_non_negative(shares)?;
     let preview = call_proxy_view(&env, &env.current_contract_address(), 0, shares)?;
     Ok(preview.convert_to_assets)
 }
@@
 pub fn preview_mint(env: Env, shares: i128) -> Result<i128, ContractError> {
+    require_non_negative(shares)?;
     let preview = call_proxy_view(&env, &env.current_contract_address(), 0, shares)?;
     Ok(preview.preview_mint_assets)
 }
@@
 pub fn preview_withdraw(env: Env, assets: i128) -> Result<i128, ContractError> {
+    require_non_negative(assets)?;
     let preview = call_proxy_view(&env, &env.current_contract_address(), assets, 0)?;
     Ok(preview.preview_withdraw_shares)
 }

As per coding guidelines, "**/*.rs: Prefer parsing over validation in Rust; 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/proxy-4626-soroban/src/contract.rs` around lines 275 - 300, The
preview/view entrypoints (convert_to_shares, convert_to_assets, preview_mint,
preview_withdraw, and the delegating preview_deposit/preview_redeem paths)
currently forward negative i128 inputs into call_proxy_view; add explicit
validation at the start of each function to reject negative inputs and return
Err(ContractError::InvalidInput) (or the contract's equivalent) before calling
call_proxy_view, referencing the functions convert_to_shares, convert_to_assets,
preview_mint, preview_withdraw and the helper call_proxy_view so invalid signed
values are caught early and impossible states are made unrepresentable.
🤖 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 129-139: The current path calls
invoke_vault_execute(VaultCommand::DepositWithMin...) and discards the actual
minted shares returned by expect_i128_result, then emits and returns the
requested shares variable; capture the actual minted share amount from
expect_i128_result(invoke_vault_execute(...)) into a local (e.g. minted_shares)
and use that value when calling emit_deposit_event(&env, &operator, &receiver,
assets, minted_shares) and when returning Ok(...) so the event and return
reflect the real fill instead of the requested `shares`.
- Line 174: The RedeemRequest event call is passing the payout recipient
(`receiver`) as the indexed controller topic; change the first argument of
`emit_redeem_request_event` to the actual controller (`operator`) so the
signature matches `(controller, owner, request_id, sender, shares)` — update the
call at the shown site (and the analogous calls around lines 210 and 239) to
pass `operator` (not `receiver`) along with `owner`, `request_id`, `sender`, and
`shares` so indexers see the correct controller.

---

Outside diff comments:
In `@contract/proxy-4626-soroban/src/contract.rs`:
- Around line 275-300: The preview/view entrypoints (convert_to_shares,
convert_to_assets, preview_mint, preview_withdraw, and the delegating
preview_deposit/preview_redeem paths) currently forward negative i128 inputs
into call_proxy_view; add explicit validation at the start of each function to
reject negative inputs and return Err(ContractError::InvalidInput) (or the
contract's equivalent) before calling call_proxy_view, referencing the functions
convert_to_shares, convert_to_assets, preview_mint, preview_withdraw and the
helper call_proxy_view so invalid signed values are caught early and impossible
states are made unrepresentable.
🪄 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: fbaa71ca-9591-4a9e-8e8a-93f2098e07e6

📥 Commits

Reviewing files that changed from the base of the PR and between 66d92a1 and 3f8a6ed.

📒 Files selected for processing (2)
  • contract/proxy-4626-soroban/src/contract.rs
  • contract/proxy-4626-soroban/src/tests.rs

Comment thread contract/proxy-4626-soroban/src/contract.rs Outdated
Comment thread contract/proxy-4626-soroban/src/contract.rs
@carrion256

Copy link
Copy Markdown
Collaborator Author

Tip

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

Now I have enough to give a thorough description and design critique. Let me also search the ERC-4626 standard to confirm key interface deviations.

@carrion256 — here is a full description and usability critique for your human reviewers.

PR #418 — Soroban ERC-4626 Proxy: Description & Design Critique

What this PR does

This PR introduces contract/proxy-4626-soroban, a thin adapter contract that wraps the existing Templar Soroban vault behind a familiar ERC-4626 tokenized vault standard interface. The motivation is exactly as stated: the underlying vault's API is deliberately low-level for gas and cross-chain reasons — mutations go through a single binary-encoded execute(Bytes) entrypoint with a hand-rolled VaultCommand codec, and reads go through proxy_view(owner, assets, shares) which returns a deeply-nested anonymous tuple (ProxyCoreView, ProxyPolicyView, ProxyPreviewView) where ProxyPreviewView is literally an 8-tuple of i128 values accessed by position (e.g., preview.6, preview.7). This proxy hides all of that from depositors.

The proxy exposes the complete ERC-4626 surface:

* Mutations: `deposit`, `mint`, `withdraw`, `redeem`

* Views: `asset`, `total_assets`, `total_supply`, `balance_of`, `convert_to_shares`, `convert_to_assets`

* Preview methods: `preview_deposit`, `preview_mint`, `preview_withdraw`, `preview_redeem`

* Cap methods: `max_deposit`, `max_mint`, `max_withdraw`, `max_redeem`

* Token metadata: `decimals`, `name`, `symbol`

* Vault-specific: `request_withdraw`, `execute_withdraw`

It is initialized once with (vault_address, asset_token, share_token) and stores those in instance storage. The proxy itself is stateless beyond that config — all accounting lives in the vault.

Usability Critique

✅ What works well

1. **The main goal is achieved.** The entire `VaultCommand` encode/decode path and the tuple-indexed `proxy_view` response are fully encapsulated. External callers never touch binary payloads or anonymous indices.

2. **Full view surface.** All eight ERC-4626 preview/max/convert methods are implemented and route through `proxy_view`, so codegen tools and frontends get a consistent query interface.

3. **Delegated withdraw.** `withdraw` and `redeem` support a `caller ≠ owner` flow by reading the share token allowance before dispatching, which is correct behavior for operator use cases.

4. **Deposit slippage (on mint).** `mint` correctly calls `preview_mint` first to discover how many assets are needed, then passes `min_shares_out: shares` to the vault — ensuring the user gets at least the requested shares.

5. **Events.** `Deposit` and `Withdraw` events are emitted as required by the ERC-4626 specification when tokens are deposited into or withdrawn from the vault.

6. **Test coverage.** Both a mock-vault unit test suite and a real integration test suite with `SorobanVaultContract` are included and cover the main deposit/withdraw/redeem flows and event emission.

⚠️ Departures from ERC-4626 that integrators must know

1. withdraw and redeem return a request_id (u64), not shares/assets.

withdraw(uint256 assets, address receiver, address owner) in the standard returns uint256 shares, and redeem(uint256 shares, address receiver, address owner) returns assets — it redeems a specific number of shares from owner and sends assets of underlying token from the vault to receiver. Here both return a u64 withdrawal request ID. This is an intentional, necessary deviation given the vault's async cooldown model, but any codegen or aggregator assuming ERC-4626 return types will break. Recommendation: add a prominent comment on both functions explaining this deviation.

2. Withdrawals are not atomic — a two-step pattern is required.

Strategies with lockups or withdrawal buffers should reflect that in previewWithdraw/previewRedeem and events, and document potential delays. Here withdraw and redeem queue a request (shares are escrowed in the vault but assets are not yet transferred), and the user must wait for the cooldown period and then call execute_withdraw to claim assets. The max_withdraw / max_redeem methods correctly return 0 when the vault is not in an idle state, which is the right signal to UIs — but a depositor expecting withdraw to behave atomically (as in ERC-4626) will be surprised when their assets don't arrive immediately.

3. Extra caller parameter on all mutation methods.

Standard ERC-4626 signatures are deposit(assets, receiver) and mint(shares, receiver). The proxy adds an explicit caller as the first argument: deposit(caller, assets, receiver). This is a Soroban platform necessity (require_auth() must be called on a concrete address), but it means any toolchain that generates bindings from the ERC-4626 ABI will produce the wrong call. This should be clearly noted in the README/docs alongside the proxy.

4. deposit has no slippage protection.

deposit calls DepositWithMin { ..., min_shares_out: 0 }. This is fine for the atomic Soroban execution model (no MEV), but integrators accustomed to EVM environments may expect a min-out parameter. If the vault's exchange rate moves between preview and execution (e.g., during a fee accrual checkpoint), the depositor gets whatever shares they get. A note or optional min_shares_out parameter override would be helpful.

🔍 Internal design observations

5. ProxyPreviewView is still an anonymous 8-tuple internally.

Inside the proxy, preview.0 through preview.7 are accessed by index across multiple methods. If the vault's proxy_view response layout ever changes (e.g., a field is inserted), these index accesses silently produce wrong values. A named struct or at minimum named constants (PREVIEW_CONVERT_TO_SHARES: usize = 0, etc.) would make the mapping auditable.

6. Allowance is read but not decremented in the delegated withdrawal path.

In require_auth_or_allowance, the proxy reads allowance(owner, proxy) to gate delegated withdraw/redeem — but never calls approve to decrease it. The vault's own RequestWithdraw handler transfers shares from owner to itself, which presumably consumes the owner's on-chain Soroban auth for that transfer. The proxy's allowance check is therefore a guard, not a spend — its effect depends on whether the vault's auth machinery independently enforces the owner's consent. This coupling should be explicitly documented (or tested with a case where mock_all_auths is not used).

7. NotImplemented error variant is defined but never used.

ContractError::NotImplemented = 6 exists in the error enum with no call sites. This implies some methods were originally planned as stubs. Either remove it or add a comment explaining what it's reserved for.

8. execute_withdraw is keyed by caller, not by request_id.

execute_withdraw(caller) dispatches ExecuteWithdraw { caller } to the vault — the vault finalizes whatever pending withdrawal is associated with that address, not a specific request by ID. This means (a) a user can only have one queued withdrawal at a time, and (b) anyone who has caller's auth can finalize it. The return type is (), so the caller gets no confirmation of how many assets were released. For depositor UX, it would be worth documenting that the request_id returned by withdraw/redeem cannot be used as a handle in execute_withdraw.

Summary

This proxy is a correct and well-structured translation layer. The vault's internal complexity is properly hidden for the deposit and view paths. The main usability risk for integrators is the async withdrawal modelwithdraw and redeem are queuing operations, not instant redemptions, and their return type of u64 (request ID) rather than the ERC-4626-mandated amounts is a silent compatibility break. This should be prominently documented at the contract and README level so that downstream codegen and protocol integrations don't silently misuse it.
🧠 Learnings used

  • “Delegated withdraw works” is no longer true. It is explicitly rejected.
  • “ProxyPreviewView is accessed by anonymous indices inside the proxy” is stale. It is mapped to named fields in shared types: /data/tmp/contracts-pr418-rebase/contract/vault/soroban/shared-types/src/lib.rs:81.
  • “Withdraw events are emitted as required by ERC-4626” is too generous. We emit RedeemRequest, which is more honest for async redemption.

@carrion256
carrion256 force-pushed the spr/refactor/vault-ergonomics/4641dafc branch from 960a594 to 6cd183a Compare June 5, 2026 10:36
@carrion256
carrion256 enabled auto-merge (rebase) June 5, 2026 10:38
@carrion256

Copy link
Copy Markdown
Collaborator Author

@coderabbitai incremental review (resolved your comments)

@coderabbitai

coderabbitai Bot commented Jun 5, 2026

Copy link
Copy Markdown
Contributor

@carrion256 — thanks for the corrections. I've noted all three:

  1. Delegated withdraw is rejected, not supported — the operator allowance path was removed.
  2. ProxyPreviewView is mapped to named ProxyPreviewFields in contract/vault/soroban/shared-types/src/lib.rs — no more anonymous index access inside the proxy.
  3. Events are RedeemRequest for async redemption flows, not generic ERC-4626 Withdraw events — which is the more honest representation of the queued model.

Kicking off a fresh incremental review now.


✏️ Learnings added
Learnt from: carrion256
URL: https://github.com/Templar-Protocol/contracts/pull/418

Timestamp: 2026-06-05T10:40:47.074Z
Learning: In `contract/proxy-4626-soroban/src/contract.rs` (Templar-Protocol/contracts, Soroban ERC-4626 proxy), delegated withdrawal operators are explicitly rejected. The `withdraw` and `redeem` entrypoints enforce `operator == owner` and return `ContractError::InsufficientAllowance` when a different caller attempts to act on behalf of an owner. Do not flag missing allowance-decrement logic or suggest supporting delegated operators — the design intentionally prohibits delegation.

Learnt from: carrion256
URL: https://github.com/Templar-Protocol/contracts/pull/418

Timestamp: 2026-06-05T10:40:47.074Z
Learning: In `contract/proxy-4626-soroban/src/contract.rs` (Templar-Protocol/contracts, Soroban ERC-4626 proxy), `ProxyPreviewView` (the 8-tuple returned by `proxy_view`) is mapped to the named struct `ProxyPreviewFields` defined in `contract/vault/soroban/shared-types/src/lib.rs`. Access inside the proxy uses named fields (e.g., `preview.convert_to_shares`), not anonymous tuple indices (`.0`–`.7`). Do not flag anonymous index access on `ProxyPreviewView` in this proxy as a maintenance risk.

Learnt from: carrion256
URL: https://github.com/Templar-Protocol/contracts/pull/418

Timestamp: 2026-06-05T10:40:47.074Z
Learning: In `contract/proxy-4626-soroban/src/contract.rs` (Templar-Protocol/contracts, Soroban ERC-4626 proxy), async redemption flows (`withdraw`, `redeem`, `request_withdraw`) emit a `RedeemRequest` event rather than a generic ERC-4626 `Withdraw` event. This is intentional — it correctly reflects that the operation queues a withdrawal rather than immediately transferring assets. Do not flag the absence of a standard `Withdraw` event on these entrypoints as a compliance gap.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


🧠 Learnings used
Learnt from: CR
Repo: Templar-Protocol/contracts PR: 0
File: contract/vault/soroban/AGENTS.md:0-0
Timestamp: 2026-06-04T16:07:43.185Z
Learning: Applies to contract/vault/soroban/**/*.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: carrion256
Repo: Templar-Protocol/contracts PR: 0
File: :0-0
Timestamp: 2026-06-04T13:47:21.372Z
Learning: In Templar-Protocol/contracts, when re-reviewing a PR, only comment on issues introduced by the changes in that specific PR. Do not surface pre-existing issues or out-of-scope concerns from the broader codebase. Incremental review is expected.

Learnt from: carrion256
Repo: Templar-Protocol/contracts PR: 418
File: contract/proxy-4626-soroban/src/contract.rs:281-303
Timestamp: 2026-04-24T14:18:52.424Z
Learning: In `contract/proxy-4626-soroban/src/contract.rs` (Templar-Protocol/contracts), the `initialize` function on `Soroban4626ProxyContract` intentionally has no caller-authorization check. The deployer is expected to call `initialize` atomically with contract deployment (Soroban's `create_contract` + init in a single transaction), which eliminates any front-running window. Do not flag the missing auth check on `initialize` in this contract as a security issue.

Learnt from: CR
Repo: Templar-Protocol/contracts PR: 0
File: contract/vault/soroban/AGENTS.md:0-0
Timestamp: 2026-06-04T16:07:43.185Z
Learning: Applies to contract/vault/soroban/**/*.rs : Changes to postcard serialization, versioning, migration, or storage keys are security-relevant and require careful review

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.

Learnt from: CR
Repo: Templar-Protocol/contracts PR: 0
File: contract/vault/soroban/AGENTS.md:0-0
Timestamp: 2026-06-04T16:07:43.185Z
Learning: Applies to contract/vault/soroban/**/*.rs : Review auth on every privileged Soroban entrypoint. Do not rely on outer routing alone

Learnt from: CR
Repo: Templar-Protocol/contracts PR: 0
File: contract/vault/soroban/AGENTS.md:0-0
Timestamp: 2026-06-04T16:07:43.185Z
Learning: Read `contract/vault/soroban/README.md`, `contract/vault/soroban/STRIDE.md`, `contract/vault/soroban/SIZE_BUDGET.md`, `contract/vault/soroban/src/contract/entrypoints.rs`, `contract/vault/soroban/src/effects/mod.rs`, `contract/vault/soroban/src/storage/mod.rs`, and the related shared-type and contract library files before making non-trivial changes to the Soroban executor layer

Learnt from: CR
Repo: Templar-Protocol/contracts PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-04-24T14:44:16.597Z
Learning: Read crate-local documentation before changing high-impact areas, especially `contract/vault/README.md`, `contract/vault/STRIDE.md`, `contract/universal-account/README.md`, and `service/relayer/README.md`

Learnt from: peer2f00l
Repo: Templar-Protocol/contracts PR: 411
File: contract/market/examples/config/v1/linear-usdt/proxy-collateral.json:21-23
Timestamp: 2026-04-20T03:51:24.169Z
Learning: In `contract/market/examples/config/v1/linear-usdt/proxy-collateral.json`, the `ft_price` method on `linear-protocol.near` is a valid on-chain view method. Do not flag `method_name: "ft_price"` on `account_id: "linear-protocol.near"` as non-existent in future reviews. The method exists on the deployed contract (verified via nearblocks.io) even though it is not prominently documented in the LiNEAR GitHub README.

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-06-04T16:07:43.185Z
Learning: Applies to contract/vault/soroban/**/*.rs : `RemoveMarket`, skim recipient changes, and share-token authority changes can all become authority drift or asset-loss bugs if altered casually

Learnt from: peer2f00l
Repo: Templar-Protocol/contracts PR: 456
File: contract/proxy-oracle/soroban/governance-contract/src/engine.rs:220-243
Timestamp: 2026-06-04T02:52:14.504Z
Learning: In `contract/proxy-oracle/soroban/governance-contract/src/engine.rs` (Templar-Protocol/contracts, Soroban governance contract), `invoke_runtime_call` intentionally collapses all `ContractError` variants from the proxy sub-invocation into a single `GovernanceError::RuntimeFailed` unit variant. This is because Soroban `#[contracterror]` enums are `#[repr(u32)]` unit variants only — they cannot carry payloads — so per-variant mapping of `ContractError` into `GovernanceError` would bloat the on-chain error ABI without benefit. Do not suggest adding payload-carrying error variants or per-variant ContractError mapping to GovernanceError in this or any other Soroban contract in this repo.

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: peer2f00l
Repo: Templar-Protocol/contracts PR: 404
File: contract/vault/kernel/src/actions/mod.rs:1869-1871
Timestamp: 2026-04-14T07:35:09.869Z
Learning: In `contract/vault/kernel/src/actions/mod.rs` (Templar-Protocol/contracts, Rust), the vault kernel crate is compiled for Soroban (wasm32 target). Extracting helper functions from large dispatcher functions like `dispatch::apply_action` risks increasing wasm binary size due to additional call frames and reduced compiler inlining. Therefore, `#[allow(clippy::too_many_lines)]` suppressions on large dispatch/entrypoint functions in this file are intentional and should not be flagged as needing refactoring.
✅ Action performed

Review finished.

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 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

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/governance/src/lib.rs (1)

676-680: 🧹 Nitpick | 🔵 Trivial | 💤 Low value

Consider simplifying the return type.

Both extend_instance_ttl(&env) and extend_pending_queue_ttl(&env) are infallible, so this method can no longer fail. The Result<(), GovernanceError> return type is now unnecessary and could be simplified to ().

♻️ Simplified signature
-    pub fn extend_ttl(env: Env) -> Result<(), GovernanceError> {
+    pub fn extend_ttl(env: Env) {
         extend_instance_ttl(&env);
         extend_pending_queue_ttl(&env);
-        Ok(())
     }
🤖 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/governance/src/lib.rs` around lines 676 - 680, The
function extend_ttl currently returns Result<(), GovernanceError> but both
called helpers extend_instance_ttl and extend_pending_queue_ttl are infallible,
so change extend_ttl's signature to return () (remove Result and
GovernanceError), remove the trailing Ok(()), and update any call sites that
expect a Result to use the new void return; keep the same body calls to
extend_instance_ttl(&env) and extend_pending_queue_ttl(&env).
♻️ Duplicate comments (1)
contract/proxy-4626-soroban/src/contract.rs (1)

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

Pass the request owner as controller, not the payout receiver.

All three emit_redeem_request_event calls pass &receiver as the controller parameter, but the controller should be the address that owns and controls the request (&owner in all three cases). Passing the receiver misattributes the request to the asset payout destination and will break off-chain indexers, wallet UIs, and any system querying "requests by controller."

ERC-7540 semantics: controller is the address that controls the request lifecycle, not the address receiving withdrawn assets.

🔧 Proposed fix
 // In withdraw (line 194)
-        emit_redeem_request_event(&env, &receiver, &owner, request_id, &operator, shares);
+        emit_redeem_request_event(&env, &owner, &owner, request_id, &operator, shares);

 // In redeem (line 230)
-        emit_redeem_request_event(&env, &receiver, &owner, request_id, &operator, shares);
+        emit_redeem_request_event(&env, &owner, &owner, request_id, &operator, shares);

 // In request_withdraw (line 259)
-        emit_redeem_request_event(&env, &receiver, &owner, request_id, &owner, shares);
+        emit_redeem_request_event(&env, &owner, &owner, request_id, &owner, shares);

Also applies to: 230-230, 259-259

🤖 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` at line 194, The three calls to
emit_redeem_request_event are incorrectly passing &receiver as the controller;
update each call to pass &owner (the request owner/controller) instead of
&receiver so the event's controller field reflects the address that controls the
request lifecycle—locate the calls to emit_redeem_request_event (the instances
using variables receiver, owner, request_id, operator, shares) and replace the
controller argument to use owner in all occurrences.
🤖 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-oracle/soroban/sep40-adapter-contract/src/lib.rs`:
- Around line 216-219: Removing the unintended side-effect of reads extending
instance TTL: delete the call to extend_instance_ttl(env) from load_config and
instead invoke extend_instance_ttl only in the contract's mutating/maintenance
entrypoints (e.g., the keeper/update functions and any setters) so pure read
methods (the SEP-40 trait methods base, assets, decimals, resolution, price,
prices, lastprice) call load_config without changing TTL; if you do want
permissionless keepers to extend TTL, add an explicit public keeper method that
calls extend_instance_ttl and is invoked by off-chain actors rather than
embedding it in load_config.

---

Outside diff comments:
In `@contract/vault/soroban/governance/src/lib.rs`:
- Around line 676-680: The function extend_ttl currently returns Result<(),
GovernanceError> but both called helpers extend_instance_ttl and
extend_pending_queue_ttl are infallible, so change extend_ttl's signature to
return () (remove Result and GovernanceError), remove the trailing Ok(()), and
update any call sites that expect a Result to use the new void return; keep the
same body calls to extend_instance_ttl(&env) and extend_pending_queue_ttl(&env).

---

Duplicate comments:
In `@contract/proxy-4626-soroban/src/contract.rs`:
- Line 194: The three calls to emit_redeem_request_event are incorrectly passing
&receiver as the controller; update each call to pass &owner (the request
owner/controller) instead of &receiver so the event's controller field reflects
the address that controls the request lifecycle—locate the calls to
emit_redeem_request_event (the instances using variables receiver, owner,
request_id, operator, shares) and replace the controller argument to use owner
in all occurrences.
🪄 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: ce3ef2b8-6a5b-40d6-849e-ee38c2d41336

📥 Commits

Reviewing files that changed from the base of the PR and between 66d92a1 and 6cd183a.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (18)
  • 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/proxy-oracle/soroban/README.md
  • contract/proxy-oracle/soroban/governance-contract/src/lib.rs
  • contract/proxy-oracle/soroban/governance-contract/src/storage.rs
  • contract/proxy-oracle/soroban/governance-contract/src/tests.rs
  • contract/proxy-oracle/soroban/integration-tests/tests/ttl_extend.rs
  • contract/proxy-oracle/soroban/sep40-adapter-contract/src/lib.rs
  • contract/proxy-oracle/soroban/sep40-adapter-contract/src/tests.rs
  • contract/vault/soroban/README.md
  • contract/vault/soroban/governance/src/lib.rs
  • contract/vault/soroban/justfile
  • contract/vault/soroban/shared-types/src/lib.rs

Comment thread contract/proxy-oracle/soroban/sep40-adapter-contract/src/lib.rs
@carrion256
carrion256 merged commit f697b3b into dev Jun 5, 2026
26 checks passed
@carrion256
carrion256 deleted the spr/refactor/vault-ergonomics/4641dafc branch June 5, 2026 10:50
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