571c5f9261
Task A, static phase. Ghidra 12.1.2 headless via the repo's own pyghidra harness
over CardsDLL_Win64_retail.dll (13,382 functions). Queries and raw decompiler
output committed under docs/evidence/market-sold-re-2026-08-17/.
RECOVERED FROM THE BINARY
1. No sold token, now EXHAUSTIVELY: both vocabularies dumped to their sentinels
rather than sampled. tradeState is exactly 4 rows; itemState is exactly 12
(invalid/free/WAITING_FOR_GAME/inGame/forSale/offered/activeBadge/
activeHomeKit/activeAwayKit/activeBall/activeStadium/active=255). A sold row
MUST therefore be a combination of existing atoms.
2. What closed does, complete, from the auctionInfo deserializer 0x18013e410:
IS_GLOW = (tradeState==closed) ? bidState != none
: bidState in {outbid, buyNow}
INBOX = bidState in {highest, buyNow}
3. The full record -> Flash map from the publisher 0x1801bf030, superseding the
partial list. The prize: record +0xbf is published as COINS_AWARDED, fed by the
coinsProcessed atom 0x2f4. The corpus had recorded that atom's type and noted
its consumer was never found; it is now traced. DURATION also renders the
localised FUT_AUCTION_EXPIRED when expires underflows.
4. highest vs buyNow on a closed row is UNDECIDABLE from CardsDLL, by proof: both
yield IS_GLOW=1/INBOX=1, bit-identical. But bidState is ALSO published verbatim
as YOURBID alongside STATE and COINS_AWARDED, so the movie does receive the raw
values - the discrimination exists and lives entirely in unread ActionScript.
This retires the question as a static target, and it contradicts the
third-party lore that a seller's sold row is closed+buyNow (the corpus's own
lifecycle table says closed+highest and assigns buyNow to the buyer).
5. The clear-sold verb EXISTS. Builder 0x1801647c0 emits "/sold" when the tradeId
field is zero and "/%lld" otherwise, on route base ut/delete/%s/trade, response
class RS4 FutISRemoveTradeServerResponse. Confirmed by the client's own
request-name table entry RemoveAllSoldFromTradePile. A BULK clear-sold verb only
makes sense if sold rows PERSIST in the seller's pile until cleared, which is
incompatible with our Fix A invariant - so the sold path will require revisiting
it under live validation.
6. The seller's SOLD counter is real, proven end to end with no inference: the hub
tradePile sub-deserializer 0x18013ead0 writes atom sold 0x2c9 to +0x1d8, and the
tile publisher 0x1800b1dc0 renders +0x1d8 as Flash TEXT3 under the localised
caption FUT_TF_SOLD. Siblings: selling -> +0x1d2 -> FUT_TF_SELLING,
count -> +0x1d4 -> FUT_UC_ITEMS, plus FUT_TF_WINNING/FUT_TF_OUTBID on the
Transfer Targets tile. We and the Python oracle both hardcode sold:0, so that
bucket can never fill.
7. Reusable method: an atom id is the INDEX into the alphabetical atom-name pointer
table at base 0x1802d2760. Validated 12/12 against the known auctionInfo atoms
and cross-checked against fifa17-recon/docs/fut_atoms.tsv. Documented gotcha:
resolve a name by the pointer slot INSIDE the table, never by the first matching
string in the binary, or you get confident nonsense.
8. An auction-outcome vocabulary exists (auctionSoldBid 0x39, auctionSoldBuyNow
0x3a, auctionWon*/auctionLost*) but NO deserializer consumes it - every
candidate function was checked for the value-SKIP/atom-loop signature and none
qualifies. Server-side or telemetry only; it does not carry sold state here.
TASK B IS UNDECIDABLE FROM THE CLIENT, and this is a proof of absence: no 0.95 or
0.05 constant of either width, no tax/fee/net/proceeds caption, and no fee
arithmetic anywhere. The client never computes or displays a net, so no experiment
against our own server can measure the rounding - whatever we credit is what it
displays, and there is no oracle. Only an original EA-era seller-balance capture
could settle it. The rule stays an explicit CHOICE (floor the fee, so
fee + proceeds == gross exactly) and is now pinned at the requested boundaries
100/101/119/120/149/150/151/199/200 plus 15,000 and i64::MAX.
Settlement NOT promoted. No production process, port or database was touched.
353 lines
18 KiB
Markdown
353 lines
18 KiB
Markdown
# Transfer-market sold settlement + transaction fee
|
|
|
|
How a completed market sale moves coins and ownership. Written while the
|
|
`forSale` expiry lifecycle test was running, so **nothing here has been deployed
|
|
or validated against a FIFA client** — see the confidence table.
|
|
|
|
Every claim carries one of:
|
|
|
|
| Tag | Meaning |
|
|
|---|---|
|
|
| **CODE** | Read directly out of the current implementation |
|
|
| **TESTED** | Pinned by a unit or integration test that fails if it regresses |
|
|
| **FIFA17-HISTORICAL** | Supported by contemporaneous FIFA 17 knowledge, not by our client binary |
|
|
| **NEEDS-LIVE** | Requires a real FIFA 17 client to confirm; must not be treated as settled |
|
|
|
|
---
|
|
|
|
## 1. What the Buy Now path did before this work
|
|
|
|
Traced end to end, **CODE**:
|
|
|
|
```text
|
|
POST/PUT /ut/game/fifa17/trade/<id>
|
|
-> openfut-utas-host/src/lib.rs route classification (EconomyRoute::MarketBuy)
|
|
-> market.rs::handle_market_buy
|
|
store.get_listing(id) market_store.rs
|
|
bid < buy_now -> echo raised bid, NO coin movement, return
|
|
store.reserve_listing(id) CAS active -> reserved
|
|
econ.balance() pre-check, 461 if short
|
|
econ.purchase_item(price, "market-buy:<id>", listing.card_id)
|
|
-> POST {core}/economy/purchase-item
|
|
-> openfut-core services/economy.rs::purchase_item
|
|
BEGIN IMMEDIATE; debit(club, cost); add_item(club, item, card); COMMIT
|
|
store.complete_sale(id) CAS reserved -> sold
|
|
-> auctionInfo record with tradeState "closed", bidState "highest",
|
|
currentBid = price, itemData.itemState "free"
|
|
```
|
|
|
|
The module header at `openfut-utas-host/src/market.rs:12-16` states it plainly:
|
|
a buy-now *"debits the buyer and MINTS the won card into their club … there is no
|
|
real counterparty and no seller credit"*.
|
|
|
|
### The ten questions, answered from code
|
|
|
|
| # | Question | Answer (**CODE**) |
|
|
|---|---|---|
|
|
| 1 | Where are buyer coins debited? | `services/economy.rs::debit` inside `purchase_item`, one `BEGIN IMMEDIATE` transaction. Host pre-checks with `econ.balance()` only to distinguish 461 from 503. |
|
|
| 2 | Does Core know the seller? | **No.** Every `/economy/*` route resolves ONE club from the `X-OpenFUT-Game` active profile (`routes/economy.rs:23-28`); no request shape carries a counterparty. |
|
|
| 3 | Does the auction persist seller-owned item identity? | **Yes** — `listings.core_item_id` and `wire_item_id` (`market_store.rs:199-203`). The settlement inputs already existed; the buy path just ignored them. |
|
|
| 4 | Transfer or mint? | **Mint.** `purchase_item` INSERTs a new `owned_cards` row keyed `market-buy:<tradeId>`. The seller's row is untouched, so a sold card existed **twice**. |
|
|
| 5 | Is the seller credited? | **No.** Nowhere. |
|
|
| 6 | Any tax calculation? | **No.** Zero occurrences of `*5/100`, `0.95`, `fee`, `tax` in Core, host, adapter *or* the Python oracle. It existed only as prose in `FIFA17_HUB_BEHAVIOR_SPEC.md:884` and `FIFA17_TRANSFER_MARKET_WIRE.md:127-131`, both labelled unimplemented. |
|
|
| 7 | Auction state sequence? | `active -> reserved -> sold`, host-side (`market_store.rs`). On any Core failure `reserved -> active`. |
|
|
| 8 | Is there a `reserved` state? | **Yes**, and it is the concurrency guard, not decoration — `reserve_listing` is a `BEGIN IMMEDIATE` compare-and-swap, so of two concurrent buyers exactly one proceeds to a debit. |
|
|
| 9 | When does the item leave the seller's `/tradePile`? | When the listing stops being `active`: the pile projection emits only real active auctions (the Fix A invariant). `sold`/`cancelled` rows vanish from the pile the moment the CAS lands. |
|
|
| 10 | What makes a retry safe? | The same CAS. A second Buy Now finds the listing non-`active`, so `reserve_listing` returns `false` and the handler returns an empty `auctionInfo` — the client reads "auction gone", and no economy call is made. |
|
|
|
|
### Deficiencies
|
|
|
|
1. **Ownership duplication.** A sold card ends up owned by both parties. Structural, not a race.
|
|
2. **No seller credit** — selling was economically pointless.
|
|
3. **No fee**, so no sink; every synthetic buy was pure coin destruction and every sale would have been pure creation.
|
|
4. **Core could not express a counterparty at all** — the single-club resolver had no seller/buyer distinction to make.
|
|
5. `UPDATE owned_cards SET club_id` did not exist anywhere in Core: there was **no ownership-transfer primitive** to call.
|
|
|
|
---
|
|
|
|
## 2. The settlement transaction
|
|
|
|
`openfut-core/src/services/economy.rs`, alongside the existing tx-scoped
|
|
primitives so it reuses the module's proven atomicity (`pool.acquire()` +
|
|
`BEGIN IMMEDIATE` + `finish()`), **CODE**:
|
|
|
|
```rust
|
|
pub enum SaleBuyer<'a> { Club(&'a str), Outside }
|
|
pub struct SaleTerms { pub gross: i64, pub fee: i64 }
|
|
|
|
pub async fn settle_sale(
|
|
pool: &Pool, item_id: &str, seller_club_id: &str,
|
|
buyer: SaleBuyer<'_>, terms: SaleTerms,
|
|
) -> AppResult<SaleReceipt>
|
|
```
|
|
|
|
One transaction: debit buyer `gross` → evict the item from every squad →
|
|
transfer the existing row → credit seller `gross - fee`. The fee is simply never
|
|
credited anywhere, which is what destroys it.
|
|
|
|
Exposed as `POST /economy/settle-sale`. It is the **only** economy route that
|
|
names clubs explicitly, because a sale has two sides and the active-profile
|
|
resolver can only describe one. Both are optional: an omitted `seller_club_id`
|
|
means the active club (the production shape — the player listed the item), and an
|
|
omitted `buyer_club_id` means `Outside`, *not* the active club, so the route can
|
|
never accidentally settle a club against itself.
|
|
|
|
### Ownership transfer, not duplication
|
|
|
|
```sql
|
|
UPDATE owned_cards SET club_id = ? WHERE id = ? AND club_id = ?
|
|
```
|
|
|
|
No INSERT and no DELETE on the two-party path, so the instance id, its
|
|
`chemistry_style`/`position_override`/`training_bonus` and its `acquired_at` all
|
|
survive and the inventory row count cannot change. Duplication is ruled out
|
|
structurally rather than by an assertion. `acquired_at` is deliberately preserved:
|
|
this is the same instance under new ownership, not a new acquisition.
|
|
|
|
**TESTED** — `settle_sale_transfers_ownership_and_splits_coins` asserts both that
|
|
the item's owner changed and that `SELECT COUNT(*) FROM owned_cards` is still 1.
|
|
|
|
### The seller is pinned, not inferred — and this was a real bug
|
|
|
|
The first implementation derived the seller from *current* ownership, which reads
|
|
naturally ("the seller is whoever owns it") and is wrong. The concurrency test
|
|
caught it immediately: **both** racing buyers succeeded, because after the first
|
|
sale the item was owned by buyer B, so the second call read B as the seller and
|
|
cheerfully chain-sold it B → C. Core has no listing concept, so it had no way to
|
|
notice that this was a replay of one listing rather than a new sale.
|
|
|
|
The fix is that `seller_club_id` is the club the caller **believes** owns the item,
|
|
and every ownership statement is predicated on it. That turns the `UPDATE` into an
|
|
ownership compare-and-swap which is authoritative about "this sale already
|
|
happened", independently of any caller-side listing state. Safety is not weakened:
|
|
a caller still cannot credit a club that never owned the item, because the credit
|
|
only follows a matched CAS.
|
|
|
|
**TESTED** — `two_buyers_racing_one_item_settle_once` (exactly one winner, loser's
|
|
balance untouched, one instance, seller paid once) and
|
|
`settling_the_same_sale_twice_pays_once`.
|
|
|
|
### Squad eviction is mandatory
|
|
|
|
`squad_players.owned_card_id` is a foreign key onto `owned_cards(id)` and the pool
|
|
enables `foreign_keys`. Two consequences, both **CODE**:
|
|
|
|
* an `Outside` sale of a squadded card would fail outright on the FK;
|
|
* a two-party transfer preserves the row id, so a surviving `squad_players` row
|
|
would leave the **previous** owner fielding a card they no longer own.
|
|
|
|
So both paths evict first, and the receipt reports `squad_slots_freed`.
|
|
**TESTED** — `sale_evicts_the_item_from_the_sellers_squad`.
|
|
|
|
> Noticed while reading, NOT fixed here (out of scope, no behaviour changed):
|
|
> `routes/cards.rs:187-192` (quick sell) and `services/market.rs` both DELETE an
|
|
> owned card without clearing `squad_players` and without a transaction, so
|
|
> quick-selling a squadded card should fail on the same FK. Worth a follow-up.
|
|
|
|
---
|
|
|
|
## 3. The fee arithmetic
|
|
|
|
Lives in `openfut-adapter-fifa17/src/fut/economy_policy.rs`, beside `pack_price`
|
|
and `match_reward_total`, because 5% is a **FIFA policy constant** and Core must
|
|
stay game-neutral. Core validates `0 <= fee <= gross` and never computes a rate.
|
|
|
|
```rust
|
|
pub const TRANSFER_MARKET_FEE_PERCENT: i64 = 5;
|
|
pub fn transfer_market_fee(gross: i64) -> i64 { /* floor, i128 intermediate */ }
|
|
pub fn seller_proceeds(gross: i64) -> i64 { gross.max(0) - transfer_market_fee(gross) }
|
|
```
|
|
|
|
Integer only. Floating point is never used for coin settlement: `0.05` is not
|
|
representable in binary, and at large prices a `f64` round trip can create or
|
|
destroy a coin. The multiply widens to `i128`, so overflow is unreachable for any
|
|
`i64` price and no price ceiling has to be assumed.
|
|
|
|
**Rounding is a CHOICE, and it is NEEDS-LIVE.** The fee is floored, so the seller
|
|
keeps the fractional coin. That was chosen because it makes
|
|
|
|
```text
|
|
fee + proceeds == gross
|
|
```
|
|
|
|
hold exactly at every input, which is what the accounting invariant rests on. The
|
|
discriminating case against the alternative (flooring the seller's 95% instead) is
|
|
a gross of **150**: this rule pays **143**, the alternative pays **142**. Nothing
|
|
in the corpus or the client binary settles which the real server did — the client
|
|
is only ever told the gross, and no `tax`/`netPrice`/`sellerProceeds` wire field
|
|
exists. **A future live test should list something at 150 coins and read the
|
|
credited amount.**
|
|
|
|
**TESTED** at `0, 1, 19, 20, 21, 39, 40, 100, 150, 200, 1_000, 15_000,
|
|
15_000_000, i64::MAX`, plus `fee + proceeds == gross` swept over 0..2000 and the
|
|
extremes, plus negative-gross rejection.
|
|
|
|
| gross | fee | proceeds | why it is in the table |
|
|
|---|---|---|---|
|
|
| 19 | 0 | 19 | last fee-free price |
|
|
| 20 | 1 | 19 | first price that pays |
|
|
| 150 | 7 | 143 | **the NEEDS-LIVE discriminator** (alternative: 8 / 142) |
|
|
| 15,000 | 750 | 14,250 | canonical fixture |
|
|
| i64::MAX | MAX/20 | remainder | proves no overflow |
|
|
|
|
---
|
|
|
|
## 4. Accounting invariant
|
|
|
|
For `SaleBuyer::Club`, **TESTED** by `sale_conserves_coins_minus_the_fee`:
|
|
|
|
```text
|
|
buyer_debit == seller_credit + fee
|
|
total modelled coins shrink by EXACTLY the fee
|
|
21,000 -> 20,250 (difference 750)
|
|
```
|
|
|
|
For `SaleBuyer::Outside` the identity deliberately differs and the doc comment
|
|
says so: the counterparty owns no `clubs` row, so nothing is debited and the
|
|
seller's proceeds **enter** the economy from outside. **TESTED** by
|
|
`settle_sale_to_outside_retires_the_item_and_pays_net`, which asserts total coins
|
|
*rise* by the proceeds. Conflating the two would look like a conservation bug.
|
|
|
|
---
|
|
|
|
## 5. Rejected sales
|
|
|
|
All **TESTED** by `invalid_sales_change_nothing`, which re-seeds a fresh fixture
|
|
per case and asserts both balances and the item's owner afterwards:
|
|
|
|
| Case | Outcome |
|
|
|---|---|
|
|
| buyer cannot afford `gross` | rejected, nothing moves |
|
|
| buyer == seller | rejected at the Core domain boundary — not a market path, and it would otherwise debit and credit one club while destroying the fee |
|
|
| item does not exist | rejected |
|
|
| item not owned by the named seller (**includes every replay**) | rejected |
|
|
| buyer club does not exist | rejected |
|
|
| `fee > gross` | rejected |
|
|
| negative fee / negative gross | rejected |
|
|
| ownership changed mid-flight | rejected by the CAS |
|
|
|
|
A zero-price sale is **legal** (a free transfer, no fee) — **TESTED** by
|
|
`zero_price_sale_is_a_free_transfer`.
|
|
|
|
Auction-level states (`expired`, `cancelled`, already-`sold`) are **not** Core's
|
|
business: Core has no listing concept, and the host's `active -> reserved` CAS
|
|
already refuses those. Duplicating that check in Core would create a second
|
|
authority on auction lifecycle.
|
|
|
|
---
|
|
|
|
## 6. Atomicity
|
|
|
|
`services::economy` is the **only** module in Core that is genuinely atomic; it
|
|
uses `pool.acquire()` + raw `BEGIN IMMEDIATE` + `finish()` deliberately, because a
|
|
DEFERRED `pool.begin()` upgrades to a write at first write, where SQLite returns
|
|
`SQLITE_BUSY` immediately instead of honouring `busy_timeout`
|
|
(`services/economy.rs:218-223`). Settlement is built inside that module for
|
|
exactly this reason, so it inherits the guarantee rather than re-deriving it.
|
|
|
|
Handlers receive a pooled clone, never `&mut SqliteConnection`, so the transaction
|
|
boundary is inside the service function. **CODE.** No externally observable state
|
|
exists where the buyer is debited without the item moving, or the seller credited
|
|
without the buyer debited — every step shares one connection inside one
|
|
transaction, and `finish()` rolls back the whole thing on any error.
|
|
|
|
### Rejection precedence, and a test that passed for the wrong reason
|
|
|
|
Inside one transaction the order of the steps cannot change the final state — any
|
|
error rolls everything back. It does change **which reason** a rejection carries,
|
|
and that turned out to matter.
|
|
|
|
The first implementation debited the buyer before touching ownership, on the
|
|
reasonable-sounding grounds that an unaffordable sale should fail before ownership
|
|
moves. The isolated staging harness then showed that a replayed settlement was
|
|
refused with `insufficient balance: have 5000, need 15000` — because the buyer had
|
|
already spent the coins on the sale that succeeded. The affordability guard fired
|
|
first, so **the ownership CAS was never consulted**, and the unit test named
|
|
`settling_the_same_sale_twice_pays_once` was passing without ever exercising the
|
|
replay guard it claimed to test.
|
|
|
|
Two fixes, both **TESTED**:
|
|
|
|
* ownership is now judged before affordability, so a replay is reported as
|
|
`item not owned by club` (404) — the true cause;
|
|
* the replay test now asserts the error is specifically `AppError::NotFound`, and
|
|
adds a deliberately CHEAP replay (gross 100 out of the buyer's remaining 5,000)
|
|
that only ownership can possibly refuse.
|
|
|
|
The lesson generalises: a guard that is merely *shadowed* by an earlier guard is
|
|
untested, and a green assertion on "the second call failed" says nothing about
|
|
which mechanism failed it.
|
|
|
|
---
|
|
|
|
## 6b. Isolated staging harness
|
|
|
|
`scripts/settlement-staging.py` — stdlib-only, exercises the settlement against a
|
|
**real Core over real HTTP** with zero production contact. It picks an ephemeral
|
|
loopback port (production 8099/8199/18080 sit in a hard deny-list checked in three
|
|
places), runs Core once to apply its own migrations, seeds the canonical two-party
|
|
fixture by direct SQL, relaunches Core, then prints BEFORE / PURCHASE / AFTER with
|
|
PASS-FAIL lines and cleans up in a `finally`.
|
|
|
|
Result: **31/31 checks pass**, exit 0.
|
|
|
|
```text
|
|
BEFORE seller 1,000 buyer 20,000 owner club-seller rows 1 total 21,000
|
|
POST /economy/settle-sale {item_id, gross 15000, fee 750, seller, buyer} -> 200
|
|
AFTER seller 15,250 buyer 5,000 owner club-buyer rows 1 total 20,250
|
|
fee destroyed 750 duplicates 1 coins destroyed 750
|
|
RETRY (identical) -> 404 item not owned by club: item-x nothing moved
|
|
REPLAY GUARD (gross 100) -> 404 item not owned by club: item-x nothing moved
|
|
```
|
|
|
|
Two things it found that the unit tests had not:
|
|
|
|
1. the rejection-precedence bug above;
|
|
2. Core's **content preflight** aborts startup when an owned card references a
|
|
`CardDefinitionId` that no loaded pack defines (`content preflight failed: 1
|
|
owned card(s) reference CardDefinitionId(s) not loaded (e.g. ["def-x"])`), so
|
|
the harness writes a one-entry content pack for its synthetic card rather than
|
|
quietly substituting a stock card id.
|
|
|
|
---
|
|
|
|
## 7. What is NOT done, and why
|
|
|
|
* **No production wire change, no deployment.** The host gained the *capability*
|
|
(`CoreEconomy::settle_sale`) but `handle_market_buy` is untouched: the synthetic
|
|
buy path (`core_item_id == None`) has no counterparty, so minting there is
|
|
correct and unchanged.
|
|
* **Nothing decides that a player's listing has sold.** That trigger is the real
|
|
remaining feature, and it needs the seller-facing sold wire representation,
|
|
which is deliberately deferred — `tradeState`/`itemState` for a sold row are
|
|
**NEEDS-LIVE** and must not be guessed. `docs/FIFA17_TRANSFER_MARKET_WIRE.md`
|
|
records that no `sold` token exists in FIFA 17's `tradeState` table
|
|
(`active=1 inactive=2 expired=3 closed=4`).
|
|
* **`/tradePile/counts` semantics unchanged.** Its `sold` field stays as-is; the
|
|
Core facts a correct `sold` count would need now exist, but the route output
|
|
must not change without client evidence.
|
|
* **The 5% rate itself is FIFA17-HISTORICAL**, and the rounding rule is
|
|
**NEEDS-LIVE** (the 150-coin discriminator above).
|
|
|
|
## 8. Confidence summary
|
|
|
|
| Claim | Status |
|
|
|---|---|
|
|
| The old buy path minted a duplicate and never credited a seller | **CODE** |
|
|
| No fee arithmetic existed anywhere in the project | **CODE** |
|
|
| Ownership now transfers as one row, with no duplicate possible | **TESTED** |
|
|
| Buyer debited, seller credited net, fee destroyed, in one transaction | **TESTED** |
|
|
| `buyer_debit == seller_credit + fee` | **TESTED** |
|
|
| Canonical 15,000 fixture: 20,000→5,000 / 1,000→15,250 / fee 750 | **TESTED** |
|
|
| A replayed settlement pays once | **TESTED** |
|
|
| Two racing buyers settle exactly once | **TESTED** |
|
|
| A sold card leaves the seller's lineup | **TESTED** |
|
|
| Invalid sales leave the economy unchanged | **TESTED** |
|
|
| The fee rate is 5% of gross | **FIFA17-HISTORICAL** |
|
|
| The fee is floored (150 → 143, not 142) | **UNDECIDABLE from available evidence** — CardsDLL contains no fee arithmetic, no `0.95`/`0.05` constant and no tax/net caption, so the client never computes or displays a net and there is no oracle to measure against. See `FIFA17_SOLD_WIRE_RE.md` §9. Remains a pinned CHOICE. |
|
|
| The seller-facing sold wire state | **PARTIALLY RECOVERED** from the binary — see `FIFA17_SOLD_WIRE_RE.md`. Proven: no `sold` token in either vocabulary (exhaustive); `coinsProcessed` is published to Flash as `COINS_AWARDED`; a bulk clear-sold verb `DELETE …/trade/sold` exists; the seller's SOLD counter is real (atom `sold` 0x2c9 → `FUT_TF_SOLD`). Still UNDECIDABLE statically: `highest` vs `buyNow`. |
|
|
| The client renders a settled sale correctly | **NEEDS-LIVE** — never exercised |
|
|
|
|
No Core test here establishes anything about the FIFA 17 wire contract. They
|
|
establish the domain transaction only.
|