Skip to content

fix(soroban): bind adapters by market identity#426

Draft
carrion256 wants to merge 2 commits into
spr/refactor/vault-ergonomics/4f330057from
audit/adapter-identity-and-governance-a001
Draft

fix(soroban): bind adapters by market identity#426
carrion256 wants to merge 2 commits into
spr/refactor/vault-ergonomics/4f330057from
audit/adapter-identity-and-governance-a001

Conversation

@carrion256

@carrion256 carrion256 commented Apr 30, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • replace positional supply-queue adapter lookup with immutable market-keyed adapter bindings
  • create missing market -> adapter bindings atomically from supply-queue governance proposals via aligned accounts
  • reject missing adapters for new markets and reject attempts to rebind existing markets
  • keep pure queue reorders adapter-free and preserve bindings when markets are removed
  • update Blend e2e setup to follow the production binding flow

Audit Context

  • Cluster: adapter-identity-and-governance
  • Finding: A-001 positional adapter mapping can silently remap markets when the supply queue is reordered
  • Base: PR 417 branch spr/refactor/vault-ergonomics/4f330057

Verification

  • cargo test -p templar-soroban-runtime --lib -- --nocapture — 111 passed
  • cargo test -p templar-soroban-runtime --test blend_e2e -- --nocapture — 1 passed
  • cargo test -p templar-soroban-runtime -- --nocapture — passed
  • post-commit Soroban size-budget-check — passed, 94149 bytes

Notes

  • AllowedAdapters remains an allowlist/config surface, but no longer creates or rewrites market identity.
  • adapter_for_market now returns InvalidInput unless an explicit keyed binding exists.

Halborn Finding IDs

Included for Halborn SSC GitHub remediation detection:

  • A-001 / Finding ID ced39f48-e0fe-4218-80c3-d631ff6c565e — Positional adapter mapping causes silent market remap on supply queue reorder

This change is Reviewable

@coderabbitai

coderabbitai Bot commented Apr 30, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 1a4ddfdb-b91a-437c-9332-ff25de5c19c6

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

The vault adapter binding system is refactored to use a stored Map<u32, SdkAddress> keyed by market target ID, replacing indexed adapter lookup from the supply queue policy. Governance now validates and maintains these bindings during supply-queue policy execution, with adapter resolution simplified to direct map access. Tests and E2E scenarios are updated accordingly.

Changes

Cohort / File(s) Summary
Adapter Binding Storage and Resolution
contract/vault/soroban/src/contract/entrypoints.rs, contract/vault/soroban/src/contract/helpers.rs, contract/vault/soroban/src/contract/types.rs
Introduces VaultDataKey::AdapterBindings constant and refactors apply_supply_queue_policy to validate, derive, and store adapter bindings in a keyed map. Changes adapter_for_market helper to fetch adapters directly from stored bindings instead of policy state indexing.
Governance and Unit Test Coverage
contract/vault/soroban/src/tests.rs
Adds governance-policy tests validating adapter binding initialization, rejection of unbound markets, prevention of adapter rebinding, and persistence of bindings across supply queue modifications. Includes helper utilities for constructing policy state and supply queues.
E2E Test Configuration
contract/vault/soroban/tests/blend_e2e.rs
Reorders governance calls to set GOVERNANCE_CONFIG_KIND_ALLOWED_ADAPTERS before GOVERNANCE_POLICY_KIND_SUPPLY_QUEUE, and updates supply-queue policy to include adapter account constraints.

Sequence Diagram

sequenceDiagram
    participant Gov as Governance System
    participant Vault as Vault Contract
    participant Storage as Instance Storage

    Gov->>Vault: apply_supply_queue_policy(accounts, target_ids)
    Vault->>Vault: Validate accounts.len() == target_ids.len()
    
    loop For each market in target_ids
        Vault->>Vault: Derive proposed adapter by index
        Vault->>Storage: Check existing AdapterBindings
        alt Binding exists & mismatched
            Vault->>Gov: Reject (InvalidInput)
        else Binding missing
            Vault->>Storage: Insert market → adapter binding
        end
    end
    
    Vault->>Storage: Update supply queue
    Vault->>Storage: Write AdapterBindings map
    Vault->>Gov: Return Ok(())
    
    Note over Gov,Storage: Later: adapter lookup uses stored map
    Gov->>Vault: adapter_for_market(market_id)
    Vault->>Storage: Fetch AdapterBindings[market_id]
    Storage-->>Vault: SdkAddress or InvalidInput error
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 56.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main change: replacing positional adapter lookup with market-keyed adapter bindings, which directly addresses the audit finding A-001.
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.
Description check ✅ Passed The PR description comprehensively describes the changeset, covering the shift from positional to market-keyed adapter bindings, atomic creation from governance proposals, rejection of missing/duplicate adapters, and preservation of bindings during queue reorders.

✏️ 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 audit/adapter-identity-and-governance-a001

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

@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

Tip

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

🧩 Analysis chain

Let me gather the relevant source files to provide a thorough description and design critique.


🏁 Script executed:

#!/bin/bash
# Find relevant files changed in this PR area
fd -e rs . contract/vault/soroban/runtime/src | head -60

echo "---GOVERNANCE---"
fd -e rs . contract/vault/soroban/governance/src | head -40

echo "---BLEND ADAPTER---"
fd -e rs . contract/vault/soroban/blend_adapter | head -40

Length of output: 422


🏁 Script executed:

#!/bin/bash
# Look at adapter-related structures and bindings
rg -n "adapter_for_market\|AdapterBinding\|AllowedAdapters\|market_adapter\|adapter_key\|SupplyQueue\|supply_queue" --type rs -C 3 | head -200

Length of output: 27


🏁 Script executed:

#!/bin/bash
# Look at the governance proposal handling for adapter bindings
rg -n "accounts\|binding\|rebind\|create.*adapter\|adapter.*creat" --type rs -C 3 | head -150

Length of output: 27


🏁 Script executed:

#!/bin/bash
# Find all .rs files in the soroban vault area
fd "\.rs$" contract/vault/soroban/ | sort | head -80

Length of output: 1309


🏁 Script executed:

#!/bin/bash
# Find adapter-related files
fd "adapter" contract/ | sort

Length of output: 131


🏁 Script executed:

#!/bin/bash
# Use rg without type flag
rg -n "adapter_for_market|AdapterBinding|AllowedAdapters|SupplyQueue" -C 3 | head -200

Length of output: 16342


🏁 Script executed:

#!/bin/bash
cat contract/vault/soroban/src/contract/helpers.rs

Length of output: 16825


🏁 Script executed:

#!/bin/bash
cat contract/vault/soroban/src/contract/entrypoints.rs

Length of output: 36111


🏁 Script executed:

#!/bin/bash
cat contract/vault/soroban/src/contract/curator_vault.rs

Length of output: 40876


🏁 Script executed:

#!/bin/bash
# Get the SetSupplyQueue/accounts handling in governance lib.rs
sed -n '1200,1260p' contract/vault/soroban/governance/src/lib.rs

Length of output: 2458


🏁 Script executed:

#!/bin/bash
# Read the blend e2e test
cat contract/vault/soroban/tests/blend_e2e.rs | head -300

Length of output: 9289


🏁 Script executed:

#!/bin/bash
# Check the GovernanceCommand SetGovernancePolicy struct - accounts field
rg -n "GovernanceCommand\|accounts\|SetGovernancePolicy" contract/vault/soroban/shared-types/src/lib.rs | head -60
cat contract/vault/soroban/shared-types/src/lib.rs | head -150

Length of output: 3937


@carrion256: Here is a description and design critique for human reviewers.


PR Description

Problem Addressed (Audit Finding A-001)

Previously, the vault routed supply/withdraw allocation calls to adapters using positional lookup: the adapter at index i in the AllowedAdapters list was assumed to serve the market at position i in the supply queue. A governance reorder of the supply queue (e.g., swapping market A and market B) would silently remap their adapters, potentially routing funds through the wrong protocol.

What Changed

The PR introduces a dedicated AdapterBindings storage key (Map<u32 (market_id), Address>), replacing the positional lookup with an identity-keyed binding:

AdapterBindings: { market_id_0 → adapter_address_0, market_id_1 → adapter_address_1, ... }

adapter_for_market (in helpers.rs) now directly queries this map by market ID and returns InvalidInput if no binding exists, making supply/withdraw/refresh calls fail fast and loudly for unbound markets.

apply_supply_queue_policy (in entrypoints.rs) is the creation site for bindings. When a SetGovernancePolicy call for GOVERNANCE_POLICY_KIND_SUPPLY_QUEUE is executed:

  • The target_ids field carries the ordered list of market IDs.
  • The optional accounts field carries parallel adapter addresses for new markets only.
  • For each market ID in target_ids:
    • If the market is new (no existing binding): an adapter must be provided via accounts, or the call is rejected.
    • If the market is existing: the provided adapter (if any) must match the existing binding exactly, or the call is rejected — rebinding is prohibited.
    • If the market is existing and no adapter is provided: pure reorder is allowed without touching bindings.

Bindings survive RemoveMarket — the map entry is not cleared when a market is removed, so reintroducing a market reuses its original binding automatically.

AllowedAdapters is retained as a configuration surface (e.g., for access control or tooling), but it no longer drives allocation routing.


Design Critique

1. The timelocked governance proposal path cannot create bindings

GovernanceAction::SetSupplyQueue(Vec<u32>) in the governance contract serializes to GovernanceCommand::SetGovernancePolicy { accounts: None, ... }. This means any proposal submitted through the governance timelock (the production governance flow) will always have accounts: None and therefore cannot introduce bindings for new markets — it will be rejected by apply_supply_queue_policy if any target ID is new.

The only way to create a binding today is via a direct execute_governance call (bypassing the governance contract's timelock), which is the path used in the e2e test. If the intent is to require timelocked proposals for supply queue changes (since SetSupplyQueue is TimelockDecision::Timelocked), this creates an operational dead-end: any proposal that adds a new market will fail on execution.

GovernanceAction and its codec would need a SetSupplyQueueWithAdapters variant (or the SetSupplyQueue variant extended to carry adapter addresses) for the timelocked path to be viable for new-market introductions.

2. AllowedAdapters and AdapterBindings are now semantically disconnected

AllowedAdapters is still written and read by apply_allowed_adapters_config, including a length-parity check against the supply queue. However, adapter_for_market does not consult AllowedAdapters at all. Nothing in apply_supply_queue_policy validates that a proposed binding address is on the AllowedAdapters list. The two surfaces can freely diverge, making the purpose of AllowedAdapters unclear: is it an enforced allowlist or a dead configuration remnant?

3. Orphaned bindings accumulate after RemoveMarket

The decision to preserve bindings on market removal avoids migration risk, but it means the AdapterBindings map grows monotonically and can contain entries for markets that no longer exist in the policy state. If a market is removed and later reintroduced with a different adapter (e.g., after a protocol migration), the old binding will silently take precedence and the new binding attempt will be rejected as a "rebind." There is currently no privileged path to replace a binding short of a vault upgrade.

4. No upgrade path for adapter replacement on live markets

Repointing an existing market to a new adapter address (e.g., migrating from one Blend pool to another) is explicitly rejected by the rebind guard. The intended migration path would be: set cap to zero → remove market (requires zero principal) → re-add with new adapter. But the cap-zero + remove-market cycle requires a zero-balance position, making this operationally heavy. It would be worth documenting this explicitly or providing a guarded rebind path (e.g., governance-only, timelocked).

5. accounts field is semantically overloaded in SetGovernancePolicy

The accounts field in GovernanceCommand::SetGovernancePolicy already carries fee recipients (for GOVERNANCE_POLICY_KIND_FEES) and restriction accounts (for GOVERNANCE_POLICY_KIND_RESTRICTIONS). For supply queue proposals it now carries adapter addresses. The parallel ordering between target_ids and accounts for the supply queue case is implicit and unchecked by the shared codec — callers must get the ordering right with no schema-level enforcement.

6. Opaque error codes on lookup failure

adapter_for_market returns ContractError::InvalidInput for both "no bindings map exists at all" and "market has no binding entry." These are meaningfully different operational states (vault never had bindings configured vs. market was never bound), and distinguishing them would help operators diagnose misconfiguration.


🧠 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: Preserve the existing contract split: runtime, governance, share-token, and blend adapter each have distinct authority boundaries

Learnt from: carrion256
Repo: Templar-Protocol/contracts PR: 417
File: contract/vault/near/src/policy.rs:167-180
Timestamp: 2026-04-27T09:35:55.454Z
Learning: In `contract/vault/near/` (the NEAR vault contract, Templar-Protocol/contracts), this is a greenfield deployment with no existing persisted on-chain state. Therefore, `from_markets` in `contract/vault/near/src/policy.rs` constructing synthetic lease ownership (via fabricated `op_id` and `LeaseOwner`) does not pose a migration risk. If a future migration must handle active legacy locks, it should either reject migration while locks exist or carry enough owner/token metadata to reconstruct leases safely.

Learnt from: CR
Repo: Templar-Protocol/contracts PR: 0
File: contract/market/AGENTS.md:0-0
Timestamp: 2026-03-19T05:08:46.663Z
Learning: Read market contract files together before making non-trivial changes: `contract/market/src/lib.rs`, `contract/market/src/impl_helper.rs`, `contract/market/src/impl_market_external.rs`, `contract/market/src/impl_token_receiver.rs`, `common/src/market/impl.rs`, `common/src/borrow.rs`, `common/src/supply.rs`, `common/src/event.rs`

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/** : RemoveMarket, skim recipient changes, and share-token authority changes can all become authority drift or asset-loss bugs if altered casually

Learnt from: CR
Repo: Templar-Protocol/contracts PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-04-24T14:44:16.597Z
Learning: Applies to contract/market/src/**/*.rs : Read and inspect `contract/market/src/lib.rs`, `contract/market/src/impl_market_external.rs`, and the corresponding logic in `templar_common::market` before making changes to the market 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/** : 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: contract/vault/soroban/AGENTS.md:0-0
Timestamp: 2026-04-24T14:44:42.034Z
Learning: Applies to contract/vault/soroban/src/effects/** : The kernel-to-Soroban address mapping is critical for effect routing. Treat changes there as high impact

Learnt from: carrion256
Repo: Templar-Protocol/contracts PR: 417
File: contract/vault/soroban/governance/src/types.rs:160-174
Timestamp: 2026-04-27T09:36:09.179Z
Learning: In `contract/vault/soroban/governance/src/types.rs` (Templar-Protocol/contracts), the `RestrictionMode` enum's `from_u32`/`as_u32` mapping uses `0=None, 1=Blacklist, 2=Whitelist`. The old `Paused` variant (previously mapped to `1`) has been removed entirely; pause state is now handled separately via `Gate.paused`. This is a greenfield Soroban governance ABI with no legacy clients/relayers, so recycling code `1` for `Blacklist` is intentional and safe. Do not flag this numeric remapping as a migration risk in future reviews.

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

@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/entrypoints.rs (1)

559-563: 🧹 Nitpick | 🔵 Trivial | 🏗️ Heavy lift

accounts is now carrying adapter bindings, not accounts.

For GOVERNANCE_POLICY_KIND_SUPPLY_QUEUE, the Soroban ABI still exposes a generic bag of optionals and repurposes accounts to mean “adapter list aligned with target_ids”. That keeps the proxy interface opaque for curators and tooling. If codegen budget allows, a dedicated supply-queue payload or dedicated adapter_bindings field would make this much harder to misuse.

As per coding guidelines, prefer stronger types over loosely constrained values in Rust: enums over well-known strings, sets over vectors when uniqueness matters, dedicated newtypes over primitive obsession, and structured state machines over ad hoc flags.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@contract/vault/soroban/src/contract/entrypoints.rs` around lines 559 - 563,
GOVERNANCE_POLICY_KIND_SUPPLY_QUEUE is passing a loosely-typed `accounts` bag
into apply_supply_queue_policy even though those values are adapter bindings,
not account objects; change the call and the apply_supply_queue_policy signature
to accept a strongly-typed adapter_bindings/newtype (e.g., AdapterBindings or
SupplyQueueAdapterBindings) instead of `accounts`, validate that
adapter_bindings length and alignment match `target_ids` and that each binding
conforms to expected adapter format, update any internal uses in
apply_supply_queue_policy to treat inputs as adapter bindings (not accounts),
and update other callers and docs/comments to use the new type so the proxy ABI
remains opaque but the Rust side enforces correctness.
🤖 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/vault/soroban/src/contract/entrypoints.rs`:
- Around line 130-153: The code currently allows writing proposed_adapter into
AdapterBindings without checking the allowlist; before calling
bindings.set(target_id, adapter) in the entrypoint that iterates target_ids
(where proposed_adapter is computed), check that the adapter is a member of
VaultDataKey::AllowedAdapters (or call the existing membership helper used
elsewhere) and return ContractError::InvalidInput if not allowed; specifically
update the branch in entrypoints.rs that handles the else (no existing_adapter)
to validate adapter membership against AllowedAdapters prior to calling
bindings.set, so allocate_impl cannot later route funds to an unapproved
adapter.
- Around line 136-153: The loop that binds adapters must reject non-contract
addresses: call require_contract_address() on any proposed adapter (the value
produced from address_from_alloc_string() stored in proposed_adapter) before
comparing with existing_adapter or calling bindings.set; if
require_contract_address() returns an error, propagate
ContractError::InvalidInput. Update the logic in the for loop over target_ids
(around proposed_adapter, existing_adapter, and bindings.set) to validate
proposed_adapter via require_contract_address() and only proceed to compare or
set after that check.

---

Outside diff comments:
In `@contract/vault/soroban/src/contract/entrypoints.rs`:
- Around line 559-563: GOVERNANCE_POLICY_KIND_SUPPLY_QUEUE is passing a
loosely-typed `accounts` bag into apply_supply_queue_policy even though those
values are adapter bindings, not account objects; change the call and the
apply_supply_queue_policy signature to accept a strongly-typed
adapter_bindings/newtype (e.g., AdapterBindings or SupplyQueueAdapterBindings)
instead of `accounts`, validate that adapter_bindings length and alignment match
`target_ids` and that each binding conforms to expected adapter format, update
any internal uses in apply_supply_queue_policy to treat inputs as adapter
bindings (not accounts), and update other callers and docs/comments to use the
new type so the proxy ABI remains opaque but the Rust side enforces correctness.
🪄 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: 79f0ef35-3479-457d-a7bf-23346a761d43

📥 Commits

Reviewing files that changed from the base of the PR and between a657f92 and 48c7f4c.

📒 Files selected for processing (5)
  • contract/vault/soroban/src/contract/entrypoints.rs
  • contract/vault/soroban/src/contract/helpers.rs
  • contract/vault/soroban/src/contract/types.rs
  • contract/vault/soroban/src/tests.rs
  • contract/vault/soroban/tests/blend_e2e.rs

Comment thread contract/vault/soroban/src/contract/entrypoints.rs
Comment thread contract/vault/soroban/src/contract/entrypoints.rs
@carrion256 carrion256 force-pushed the audit/adapter-identity-and-governance-a001 branch from 48c7f4c to cee35b1 Compare May 5, 2026 10:37
@carrion256 carrion256 force-pushed the audit/adapter-identity-and-governance-a001 branch from cee35b1 to d2ea128 Compare May 18, 2026 14:43
@carrion256

Copy link
Copy Markdown
Collaborator Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented May 18, 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 carrion256 force-pushed the audit/adapter-identity-and-governance-a001 branch from d2ea128 to 6a6a9a9 Compare May 19, 2026 08:28
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