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.
68 lines
2.4 KiB
Python
68 lines
2.4 KiB
Python
"""Q4 — is the atom ID the index into the alphabetical atom-name pointer table?
|
|
|
|
Validate against the twelve KNOWN auctionInfo atoms. If all twelve agree on one
|
|
base, the table is the atom dictionary and we can read the ID of any name.
|
|
"""
|
|
import struct
|
|
|
|
KNOWN = { # atom id -> name, from the confirmed auctionInfo deserializer
|
|
0x57: "bidState", 0x65: "buyNowPrice", 0xC1: "currentBid", 0x116: "expires",
|
|
0x16B: "itemData", 0x2B6: "sellerEstablished", 0x2B7: "sellerName",
|
|
0x2E6: "startingBid", 0x2F4: "coinsProcessed", 0x331: "tradeId",
|
|
0x335: "tradeState", 0x380: "watched",
|
|
}
|
|
|
|
|
|
def str_addr(name):
|
|
"""Address of the exact NUL-terminated string `name`."""
|
|
for h in find_all(name.encode() + b"\x00", blocks=(".rdata", ".data")):
|
|
return h
|
|
return None
|
|
|
|
|
|
def ptr_addr(sa):
|
|
hits = find_all(struct.pack("<Q", sa), blocks=(".rdata", ".data"))
|
|
return hits[0] if hits else None
|
|
|
|
|
|
print("=" * 78)
|
|
print("== solve for the table base using the twelve known atoms")
|
|
print("=" * 78)
|
|
bases = {}
|
|
for aid, name in sorted(KNOWN.items()):
|
|
sa = str_addr(name)
|
|
pa = ptr_addr(sa) if sa else None
|
|
if pa is None:
|
|
print(f" 0x{aid:03x} {name:20s} string=0x{sa or 0:x} pointer=NOT FOUND")
|
|
continue
|
|
base = pa - 8 * aid
|
|
bases[base] = bases.get(base, 0) + 1
|
|
print(f" 0x{aid:03x} {name:20s} str=0x{sa:x} ptr=0x{pa:x} -> base 0x{base:x}")
|
|
|
|
print()
|
|
print(" base histogram:", {hex(b): n for b, n in sorted(bases.items(), key=lambda kv: -kv[1])})
|
|
if not bases:
|
|
raise SystemExit("no bases resolved")
|
|
BASE = max(bases, key=bases.get)
|
|
print(f" CONSENSUS BASE = 0x{BASE:x} ({bases[BASE]}/{len(KNOWN)} atoms agree)")
|
|
|
|
print()
|
|
print("=" * 78)
|
|
print("== read the atom id of every sold/counts-related name")
|
|
print("=" * 78)
|
|
for name in ("sold", "selling", "offered", "count", "maxAuctionsAllowed",
|
|
"auctionSoldBid", "auctionSoldBuyNow", "auctionWonBid",
|
|
"auctionWonBuyNow", "auctionLostOutbid", "auctionLostOutbidSelf",
|
|
"auctionLostBidRejected", "credits", "coins", "tradeIdStr",
|
|
"itemState", "offers", "bids", "watched", "expires"):
|
|
sa = str_addr(name)
|
|
pa = ptr_addr(sa) if sa else None
|
|
if pa is None:
|
|
print(f" {name:24s} ABSENT from the table")
|
|
continue
|
|
off = pa - BASE
|
|
if off % 8:
|
|
print(f" {name:24s} ptr=0x{pa:x} misaligned (off {off})")
|
|
continue
|
|
print(f" {name:24s} ptr=0x{pa:x} ATOM ID = 0x{off // 8:x} ({off // 8})")
|