Skip to content

referral_storage::set_tier_config writes TierConfig to storage before validating it, then writes it again after #621

Description

@abayomicornelius

Problem

referral_storage::set_tier_config (contracts/referral_storage/src/lib.rs:237-266) writes the caller-supplied TierConfig to persistent storage before validating it, then validates it, then writes the identical value to the same key a second time:

pub fn set_tier_config(env: Env, admin: Address, tier: u32, config: TierConfig) {
    admin.require_auth();
    // ... admin check ...
    if tier > 2 {
        panic_with_error!(&env, Error::InvalidTier);
    }
    let tier_key = ReferralKey::TierConfig(tier);
    env.storage().persistent().set(&tier_key, &config);   // <-- write #1, unvalidated
    env.storage().persistent().extend_ttl(&tier_key, MIN_BUMP_THRESHOLD, PERSISTENT_BUMP_TARGET);
    // Validate config parameters
    let discount_bps = ((config.total_rebate_bps as u64) * (config.discount_share_bps as u64) / 10000) as u32;
    let rebate_bps = if config.total_rebate_bps >= discount_bps {
        config.total_rebate_bps - discount_bps
    } else {
        panic_with_error!(&env, Error::InvalidTierConfig);
    };
    if discount_bps > 10000 || rebate_bps > 10000 || config.total_rebate_bps > 10000 || config.discount_share_bps > 10000 {
        panic_with_error!(&env, Error::InvalidTierConfig);
    }
    env.storage()
        .persistent()
        .set(&ReferralKey::TierConfig(tier), &config);   // <-- write #2, redundant
}

Because Soroban transactions are atomic, the early unvalidated write never actually persists on the panic path (the whole call reverts), so this isn't independently exploitable. But it is a genuine defect: every successful call performs two identical persistent writes (and two extend_ttl calls implied by the surrounding code) instead of one, wasting storage-write resource budget on every admin config update, and the validation-after-mutation ordering inverts the checks-then-act pattern used everywhere else in this file (set_referrer_tier, set_tier_upgrade_threshold, etc. all validate before writing).

Why it matters

Beyond the wasted resources, the ordering is confusing to read and easy to regress: a future edit to the validation block could accidentally rely on config already being in storage, or a future refactor could drop the second write and silently leave the invalid-looking-but-actually-fine first write as the only one, without anyone noticing since both paths currently produce the same end state.

Suggested fix

Move the entire validation block (the discount_bps/rebate_bps computation and both bound checks) above the first env.storage().persistent().set(...) call, and delete the now-redundant second write and its preceding extend_ttl. This makes set_tier_config validate-then-write like its siblings, with a single write per call.

Activity

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

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions