Skip to content

fee_collector::collect_fee's obvious access-control fix would break its real caller — no allowlist primitive exists anywhere #42

Description

@abayomicornelius

Problem (spike / open-ended)

Open issue #1 in this repository correctly identifies that collect_fee has no access control:

// fee_collector/src/lib.rs:92-96
pub fn collect_fee(
    env: Env,
    token: Address,
    amount: i128,
) -> Result<(), FeeCollectorError> {
    Self::assert_initialized(&env)?;
    // ... no require_auth() of any kind, on anyone, anywhere in this function

The obvious, minimal-looking fix that comes to mind for "add access control" is the same pattern used everywhere else in this contract: fetch the stored admin and call admin.require_auth(), exactly like withdraw (fee_collector/src/lib.rs:138-143) and set_treasury (:165-170) already do. But that fix is architecturally wrong for collect_fee specifically, and shipping it naively would break the contract's actual integration, not just tighten it.

collect_fee is documented as being called by stellar_send, not by fee_collector's own admin:

"Called by StellarSend (or any authorised caller) ... The actual token transfer must have already occurred (StellarSend transfers the fee to this contract's address before calling this)." — fee_collector/src/lib.rs:83-91

stellar_send does not hold fee_collector's admin key, has no reason to, and shouldn't need to — stellar_send's own admin and fee_collector's admin are two entirely separate Address values set independently at each contract's own initialize call (stellar_send/src/lib.rs:113-142 vs fee_collector/src/lib.rs:67-81), with no relationship enforced or even checkable between them anywhere in the code. If collect_fee is "fixed" by adding admin.require_auth() against fee_collector's stored admin, then stellar_send's calls to collect_fee (once the separate, already-open "stellar_send never invokes fee_collector::collect_fee" issue is also fixed) would need fee_collector's admin to co-sign every single payment stellar_send processes — which is obviously not the intended integration and would make the two contracts unusable together.

What collect_fee actually needs is a caller-allowlist primitive — some notion of "which contract address(es) are permitted to report fee collections" — distinct from the admin concept that governs withdraw/set_treasury. That primitive doesn't exist today in any form: not in the real code, and not even as a stub in fee_collector/src/stubs.rs's backlog of planned features (which lists two-step admin rotation, a version query, a per-epoch withdrawal limit, multi-token withdrawal, and several test-coverage items — but nothing resembling a caller allowlist or authorized-reporter concept).

Why this is open-ended rather than a fixed-scope fix

This needs real design work, not just a one-line require_auth() insertion, because of several genuinely unresolved questions: (1) Should the allowlist be a single Address (the one stellar_send deployment this fee_collector instance is meant to serve) set at initialize time, or a general Vec<Address>/Map<Address, bool> supporting multiple authorized callers (relevant if this fee_collector is ever meant to serve more than one stellar_send-like integration, or a future contract that also needs to forward fees)? (2) If it's a settable allowlist rather than a single fixed value, who can add/remove entries — the admin, presumably, but does adding a caller need the SAME two-step-safety treatment as admin rotation itself (see the companion "no admin rotation" issue in this batch), since a wrongly-added caller could inflate fee totals just as effectively as the current fully-open access? (3) Does the allowlisted caller's identity need to be verified as the contract's own invoking address (i.e. env.current_contract_address() as seen from the perspective of a cross-contract call, which Soroban doesn't expose directly to the callee the way require_auth() exposes an authorizing account/contract) — Soroban's authorization model is built around Address::require_auth(), which works naturally for an account authorizing an action, but stellar_send calling collect_fee isn't naturally an "authorization" in that sense, it's a plain cross-contract function call; the right primitive might actually be closer to "verify the caller invoked this via a cross-contract call from a specific, allowlisted contract address," which needs research into what Soroban's SDK actually exposes for verifying the immediate caller of a cross-contract invocation (env.current_contract_address() inside the caller's frame vs. any caller-identity primitive exposed to the callee — this is worth verifying against current soroban-sdk 21.x capabilities before committing to a design, since APIs in this area have evolved across Soroban protocol versions).

Investigation steps

Research what soroban-sdk 21.x actually exposes for a callee to verify "which contract invoked me" (as opposed to "which account/contract authorized this specific action via require_auth"), since these are two different Soroban concepts and the right fix likely depends on which one is actually available and idiomatic for this use case.

Testing strategy

Once a design direction is chosen, the deliverable test is a genuine cross-contract integration test (in the spirit of the already-open "no cross-contract integration tests exist" issue #29, and directly reusable by it) that deploys both fee_collector and a caller contract in the same test Env, wires the caller as the allowlisted reporter, and proves: (a) the allowlisted caller's collect_fee invocation succeeds without needing fee_collector's admin to co-sign, and (b) a non-allowlisted third contract's collect_fee invocation is rejected — this is the concrete artifact proving whichever allowlist design is chosen actually solves both halves of the problem (blocking arbitrary callers per #1, without breaking the real stellar_send integration).

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    GrantFox OSSIssue tracked in GrantFox OSSMaybe RewardedIssue may be eligible for a GrantFox rewardOfficial Campaign | FWC26Campaign: Official Campaign | FWC26Third CampaignCampaign: Third CampaigncontractsSmart contract logicsecuritySecurity concernspikeOpen-ended research/design investigation, not a fixed-scope fixvery hardVery difficult / senior-level bounty issue

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions