Adds an on-chain exchange-rate parity check to escrow funding so a multi-token deposit is validated to be worth the agreed job value at deposit time. Closes #659.
The escrow contract accepts multiple funding tokens but never validated that the deposited amount was worth the agreed job value. A client could fund a USD-priced job with a token whose value later dropped, under-paying the freelancer with no on-chain recourse. The agreed value lived only in PostgreSQL.
fund_jobgains two parameters:agreed_value_stroops: i128andmax_slippage_bps: u32. Whenagreed_value_stroops > 0, the contract queries the configured price oracle for a TWAP of the job token (quoted in XLM stroops), computesdeposited_value = amount * twap_price / PRICE_SCALEwith overflow-safe arithmetic, and rejects withEscrowError::InsufficientValueif the value falls belowagreed_value * (10000 - max_slippage_bps) / 10000.agreed_value_stroops = 0bypasses the oracle entirely (native-XLM jobs and the legacy migration path), so existing escrows continue to function.- New errors:
InsufficientValue(41),OracleUnavailable(42),ValueOverflow(43). Oracle unavailability usestry_invoke_contractand returnsOracleUnavailablerather than panicking; a TWAP with fewer thanMIN_TWAP_SAMPLES(10) samples or a non-positive price is treated as unavailable. - A
RateSnapshot { twap_price, samples, agreed_value_stroops, deposited_value, max_slippage_bps, ledger }is persisted per job for audit and exposed viaget_rate_snapshot. set_price_oracle/get_price_oracle(admin) configure the oracle. The oracle must exposetwap(token: Address, quote: Address, sample_ledgers: u32) -> (i128, u32).
test.rs: exact value match, 1 bps under tolerance (passes), 1 bps over tolerance (rejected withInsufficientValue), boundary value, XLM-only bypass, oracle-not-configured and too-few-samples (OracleUnavailable). AMockOraclecontract provides a configurable TWAP.fuzz.rs:fuzz_deposited_value_never_overflowsdrives random wideamount/twap_pricepairs and asserts the value computation never wraps i128 — it either returns the exact checked result orValueOverflow.
ContractService.buildFundJobTxnow passesagreed_value_stroopsandmax_slippage_bps.simulateFundJobpre-flights the parity check so failures surface as structured errors before the user signs.getRateSnapshotreads the stored snapshot.POST /escrow/init-fundderivesagreed_value_stroopsfrom the job budget (bypassOracleopts out), returns422 InsufficientValue/503 OracleUnavailableon a failed pre-flight, and echoes the agreed value + slippage to the client.GET /escrow/:jobId/rate-snapshotreturns the stored TWAP snapshot for UI display.
DepositRateInfoshows the agreed value, TWAP rate, equivalent deposit value, and slippage tolerance on the funding confirmation modal, and warns when the live rate has drifted more than 1% from the quoted snapshot.- The job detail funding flow passes the rate context into the confirmation modal
and surfaces the structured
InsufficientValue/OracleUnavailableerrors.
Jobs funded before this change (or with agreed_value_stroops = 0) have no
RateSnapshot; get_rate_snapshot returns None and the API responds 404 for
those, while all existing escrow operations remain unaffected.