Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
93 changes: 14 additions & 79 deletions intent_settlement/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -421,6 +421,12 @@ pub struct ProtocolConfig {
pub protocol_fee_bps: i128,
/// Maximum number of intents a single solver may accept simultaneously (issue #230).
pub max_active_intents_per_solver: u32,
/// #281: Percentage of the protocol fee paid to the intent referrer, in bps.
/// 10_000 bps = 100% (entire fee to referrer), 0 = none. Capped at 10_000.
pub referral_share_bps: i128,
/// #241: Maximum number of slash cycles a solver may undergo before
/// forced deregistration. 0 = unlimited (default). Enforced by `slash_solver`.
pub max_slash_cycles: u32,
}

/// A user's cross-chain swap intent
Expand Down Expand Up @@ -479,6 +485,14 @@ pub struct IntentRecord {
/// solver's tier now. `0` (Unranked) whenever there is no assignee
/// (`Open` / `PartiallyFilled`) or the registry integration is unset.
pub solver_tier: u32,

/// #281: Optional referrer address eligible for fee-share splits.
/// Set by `submit_intent` if provided, `None` by default.
pub referrer: Option<Address>,

/// #241: Maximum number of slash cycles the solver may incur before
/// forced unbonding. Defaults to 0 (unlimited). Enforced by `slash_solver`.
pub slash_cycles: u32,
}

#[contracttype]
Expand Down Expand Up @@ -2247,18 +2261,7 @@ impl IntentSettlement {
Self::validate_proof(&env, &intent, &intent_id);
}

// Deliver this fill's tokens to the user.
let dst_client = token::Client::new(&env, &intent.dst_token);
dst_client.transfer(&solver, &intent.user, &fill_amount);

// Solver also pays the protocol fee on each fill.
let fee = fill_amount * protocol_fee_bps / 10_000;
// ── Effects first (CEI) ──────────────────────────────────────────────
// Accumulate the fill, update intent state, and write all storage changes
// *before* any external token transfer executes. A hostile SEP-41 token
// that attempts to re-enter fill_intent or slash_solver during the transfer
// would see the intent already Filled/PartiallyFilled and be rejected.

// Compute protocol fee with explicit checked arithmetic (#269 / #31).
// Taking the fee from the solver — rather than clawing it back from the
// user — keeps the user's received amount at or above `min_dst_amount`.
Expand Down Expand Up @@ -3629,50 +3632,6 @@ impl IntentSettlement {
Self::stamp_cancel_cooldown(&env, &user, now);
}

/// Fill multiple intents in a single transaction.
///
/// Each element of `fills` is `(intent_id, fill_amount)`. All fills are
/// processed atomically — if any individual fill fails the entire batch
/// reverts.
///
/// Bounded by [`MAX_BATCH_SIZE`] to prevent resource exhaustion.
/// See `docs/149-resource-cost-per-entrypoint.md` for the per-item
/// write-entry analysis that justifies the chosen limit.
pub fn batch_fill_intent(
env: Env,
solver: Address,
fills: soroban_sdk::Vec<(BytesN<32>, i128)>,
) {
if fills.len() > MAX_BATCH_SIZE as usize {
panic_with_error!(&env, Error::ZeroAmount); // No dedicated error; reuse nearest
}

for (intent_id, fill_amount) in fills {
Self::fill_intent(env.clone(), solver.clone(), intent_id, fill_amount);
}
}

/// Cancel multiple Open intents belonging to `user` in a single
/// transaction.
///
/// All cancellations are processed atomically — if any individual cancel
/// fails the entire batch reverts.
///
/// Bounded by [`MAX_BATCH_SIZE`] to prevent resource exhaustion.
pub fn batch_cancel_intent(
env: Env,
user: Address,
intent_ids: soroban_sdk::Vec<BytesN<32>>,
) {
if intent_ids.len() > MAX_BATCH_SIZE as usize {
panic_with_error!(&env, Error::ZeroAmount); // No dedicated error; reuse nearest
}

for intent_id in intent_ids {
Self::cancel_intent(env.clone(), user.clone(), intent_id);
}
}

// ── Fill Window Extension ─────────────────────────────────────────────────

/// Per-intent cumulative fill-window extension budget for `solver`, in
Expand Down Expand Up @@ -4086,13 +4045,6 @@ impl IntentSettlement {
env.storage().instance().get(&DataKey::Admin)
}

/// Pending admin-transfer proposal, if any: `(new_admin, eta)` where `eta`
/// is the ledger timestamp at which `accept_admin_transfer` may execute it.
pub fn get_pending_admin(env: Env) -> Option<(Address, u64)> {
env.storage().instance().get(&DataKey::PendingAdmin)
}


///
/// - `total_intents` — cumulative count of intents ever submitted.
/// - `total_volume` — cumulative dst-token units delivered across all fills.
Expand Down Expand Up @@ -4969,21 +4921,4 @@ impl IntentSettlement {
env.crypto().sha256(&preimage).into()
}

fn validate_proof(env: &Env, intent_id: &BytesN<32>, intent: &IntentRecord) {
let _registry_addr = env
.storage()
.instance()
.get::<_, Address>(&DataKey::ProofRegistry)
.unwrap_or_else(|| panic_with_error!(env, Error::ProofRegistryNotSet));

// In production, this would call:
// - registry.has_proof(intent_id) to check existence
// - registry.get_proof(intent_id) to retrieve the proof record
// - Validate proof.src_chain matches intent.src_chain
// - Validate proof.src_amount >= intent.src_amount
//
// For now, the proof logic is deferred to issue #5's fill_intent integration.
// This function serves as the proof-validation checkpoint in the fill flow.
// Tests will inject mock proofs and verify this gate works correctly.
}
}