You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
What the user signs is built from onchainId alone -- frontend/script.js:268 passes marketId: market.onchainId into placeBet, which puts nativeToScVal(BigInt(marketId)) into the
operation. The title, the close date, and the resolution criteria never leave the browser.
get_market is never called anywhere in this codebase. Confirmed by grep: the only contract
call the frontend makes is place_bet. So the app has no idea what on-chain market 3 actually is.
Impact
The binding between the displayed question and the signed transaction is a hardcoded integer that
nobody validates. That breaks in ordinary operation, not just under attack:
A redeploy renumbers markets.scripts/deploy-testnet.sh deploys fresh contracts and scripts/create-mainnet-markets.sh creates markets in sequence. After any redeploy, on-chain
market 3 is a different question -- but script.js still advertises the XLM question against id 3.
The user reads one question and stakes XLM on another.
A resolved or cancelled market still accepts a click. The UI has no idea market 3 closed. The
user only learns when simulation returns error 5 MarketExpired, 7 MarketResolved, or
8 MarketCancelled -- after they have committed to the flow.
The close date is decoration. "Sep 30, 2026" is a string; the contract's real close ledger is
never read, so the countdown a user makes their decision on may be wrong.
The odds are decoration too.yes: 50 is a literal, not derived from the YES/NO pools, so the
implied payout shown before signing has no relationship to the actual payout.
The user is being asked to approve a financial transaction on the basis of information that the
application has never verified and cannot verify in its current shape.
What to do
Fetch the market with get_market(market_id) and render the question, close time, and
resolution status from the contract, so the displayed market and the signed market are the
same object by construction
Re-read market state immediately before building the transaction, and abort with a clear
message if it is resolved, cancelled, frozen, or past close
Derive the implied probability and the estimated payout from the on-chain YES/NO pools and the
contract's fee configuration, not from literals
Show the user, in the confirmation step, the exact market id and question the transaction
targets, before they are handed to the wallet
Remove onchainId as a hand-maintained field -- the on-chain id should come from enumeration,
never from a value typed into a source file
Surface the minimum stake and the fee from get_config rather than discovering them through
error 10 BetTooSmall after submission
Why this is critical
This is a what-you-see-is-what-you-sign failure on a money path. The current design can take a real
XLM stake for a market the user never agreed to, through nothing more exotic than a routine
contract redeploy -- and the user has no way to detect it, because the only place the real question
exists is on chain, and the app never looks there.
Problem
The market question a user reads and the market their money goes to are two unrelated values, and
no code checks that they agree.
What the user reads comes from a JavaScript literal,
frontend/script.js:10:What the user signs is built from
onchainIdalone --frontend/script.js:268passesmarketId: market.onchainIdintoplaceBet, which putsnativeToScVal(BigInt(marketId))into theoperation. The title, the close date, and the resolution criteria never leave the browser.
get_marketis never called anywhere in this codebase. Confirmed by grep: the only contractcall the frontend makes is
place_bet. So the app has no idea what on-chain market 3 actually is.Impact
The binding between the displayed question and the signed transaction is a hardcoded integer that
nobody validates. That breaks in ordinary operation, not just under attack:
scripts/deploy-testnet.shdeploys fresh contracts andscripts/create-mainnet-markets.shcreates markets in sequence. After any redeploy, on-chainmarket 3 is a different question -- but
script.jsstill advertises the XLM question against id 3.The user reads one question and stakes XLM on another.
user only learns when simulation returns error 5
MarketExpired, 7MarketResolved, or8
MarketCancelled-- after they have committed to the flow.never read, so the countdown a user makes their decision on may be wrong.
yes: 50is a literal, not derived from the YES/NO pools, so theimplied payout shown before signing has no relationship to the actual payout.
The user is being asked to approve a financial transaction on the basis of information that the
application has never verified and cannot verify in its current shape.
What to do
get_market(market_id)and render the question, close time, andresolution status from the contract, so the displayed market and the signed market are the
same object by construction
message if it is resolved, cancelled, frozen, or past close
contract's fee configuration, not from literals
targets, before they are handed to the wallet
onchainIdas a hand-maintained field -- the on-chain id should come from enumeration,never from a value typed into a source file
get_configrather than discovering them througherror 10
BetTooSmallafter submissionWhy this is critical
This is a what-you-see-is-what-you-sign failure on a money path. The current design can take a real
XLM stake for a market the user never agreed to, through nothing more exotic than a routine
contract redeploy -- and the user has no way to detect it, because the only place the real question
exists is on chain, and the app never looks there.