Bittensor SN7 · Allways · Incident report
Between 06:22 and 07:10 UTC on 20 October 2026, four swaps against three unrelated miners resolved TIMED_OUT and paid the taker 38.71 SOL and 5.48 TAO out of miner collateral. In every case the miner's destination payment is on-chain, confirmed, and for the exact required amount — settled roughly ten minutes before the timeout verdict was cast.
One taker, 3ZXhCercm2A3…, opened every one of these swaps.
| Time UTC | Miner | Lane | Size | Fulfilled after | Verdict after | Ext. | Penalty to taker |
|---|---|---|---|---|---|---|---|
| 06:22:24 | 4V4pPw72XBaJ… | sol→solusdc | $1,252.24 | 55 s | 666 s | 0 | 13.189 SOL |
| 06:34:01 | FneiDSnQEUpu… | sol→solusdc | $1,242.84 | 2 s | 611 s | 0 | 13.090 SOL |
| 06:48:21 | 8XaVJcizxHg3… uid 148 | sol→tao | $1,319.84 | 20 s | 614 s | 0 | 5.4809 TAO |
| 06:59:15 | 8XaVJcizxHg3… uid 148 | sol→solusdc | $1,182.09 | 2 s | 606 s | 0 | 12.430 SOL |
The penalty is 110% of the swap: a 100% refund to the taker plus a 10% miner penalty. Because the miner had already delivered, the taker collected both the goods and a refund for those same goods.
Two of the four killed uid 148 outright. Its SOL purse fell 13.2 → 0.312 and its TAO bond 5.5 → 0.017, dropping both below the 1.0 minimum and deactivating the miner.
Each track runs from swap initiation to the timeout verdict. Green marks when the miner's payment was on-chain and mark_fulfilled succeeded; red marks when the validator voted to slash.
mark_fulfilled accepted
TimeoutSwap executed, collateral paid to taker
Every destination payment is on-chain, succeeded with no error, and credits the taker's own receiving account with exactly the required amount. "Required" is 99% of the pinned to_amount; the 1% is the protocol fee, and all 93 completed fills by uid 148 show the identical 1.000% deduction.
The taker's TAO address 5ExiT5xqmamUwRaFwYXbwEM5A9EheWsBrUYrCKRHJMMqJiRU currently holds 10.3135 TAO:
4.9328 TAO the goods uid 148 delivered (tx 0x55def5b2…, 7s after discovery)
5.4809 TAO the refund the protocol paid for those same goods
──────────
10.4137 TAO (10.3135 after fees) = the balance standing in that account
The taker was paid twice for one swap, out of the miner's bond.
uid 148's logs show a single clean fulfilment — no retry, no duplicate send.
06:59:31.553 Discovered swap c638c39c: sol -> solusdc, status=Active
06:59:31.703 source funds verified (11300000000 from 3ZXhCercm2...)
06:59:31.703 initiating dest send of 1171726380 to 3ZXhCercm2... on solusdc
06:59:32.429 sent 1171726380 (tx: 3CCTAXE9..., block: 459139383)
06:59:33.037 marked as fulfilled <- SUCCESS, on-chain
07:09:55.222 TIMED OUT - collateral slashed (failed_swaps=2)
On-chain the miner emitted exactly one SubmitSwapClaim, one VoteInitiate and one MarkFulfilled, all under swap_key c638c39c. Across the whole episode uid 148 sent 5,905.22 USDC in five deliveries plus one TAO delivery — one payment per swap, no duplicates.
For comparison, the swap immediately before it (884c8f51, 06:51:49, same taker, same lane, same size band) received its ConfirmSwap 23 seconds after MarkFulfilled. The two slashed swaps never received one at all.
We have not seen validator logs, so the per-swap reject reason is yours to pull. But in allways/validator/solana_swap_loop.py there is exactly one path that turns a settled delivery into a slash, and the forensics point straight at it.
_decide_fulfilled verifies the destination leg through _fetch_leg, which collapses three very different situations into two verdicts:
try:
info = provider.verify_transaction(...)
except ProviderUnreachableError:
return ('down', None) # never slashes
if info is None:
return ('no', None) # slash-eligible
if not info.confirmed:
return ('pending', info) # extendable
return ('ok', info)
The safe 'down' verdict is reached only when the provider raises a connection error. A node that answers normally but has not yet indexed the delivery returns None, which becomes 'no' — byte-for-byte the same verdict as a miner who never paid at all. From there the overdue branch slashes:
return SwapAction(SwapDecision.TIMEOUT, reason=f'{why} + overdue — dest unverifiable, slashing')
The loop already has the right machinery for a delivery it cannot yet see: 'pending' routes to EXTEND_TIMEOUT, which slides timeout_at instead of slashing. All four swaps record timeoutExtensionsUsed = 0. The extension path was never entered, so the destination leg was not read as unconfirmed — it was read as absent, for transactions that had been final on-chain for roughly ten minutes.
MarkFulfilled writes the destination tx hash onto the swap PDA; the chain knew 3CCTAXE9… the whole time. The timeout path ignores it and pays the taker a full refund anyway. The code says so plainly:
TIMEOUT an overdue swap whose dest is absent/mismatched (the contract slashes Fulfilled too).
So a swap sitting in Fulfilled state, carrying a valid delivery hash, can still be refunded in full with nothing asking — did the miner already pay?
The miner neither signs it nor can block it. For uid 148's 06:59 swap, transaction 3MwqCU2ccuDDysrNvC59be… at 07:09:36:
Instruction: TimeoutSwap
Signer: AuRf6YidcPWGBt4PALPxW42VyuRRpje9pSGRvmv4bMsm (validator)
miner a signer? no
8qdd5wmUrxzm34p6miHNAX4eLLjWN6frXE4bTzMGn8qW -12.4300 SOL (miner collateral PDA)
3ZXhCercm2A3F3zCVJg4QiaMKAjNBGLja5Wif56yciaM +12.4300 SOL (the taker)
The same instruction paid the taker 13.189 SOL at 06:33:30 and 13.090 SOL at 06:44:38 for the other two miners.
Why this is exploitable, not merely unlucky
The taker's downside on a normal, completing swap is only the 1% protocol fee — about $12 on a $1,200 fill. Their upside when the false negative fires is the miner's entire posted collateral, because the penalty is 110% of the swap and a miner at full capacity must post 1.1× the maximum fill.
Risking roughly $12 to win roughly $1,300 is worth spamming, and the larger the fill, the larger the prize. That matches what we observed: maximum-size fills aimed at the deepest crown holders, repeated until one failed to confirm.
3ZXhCercm2A3F3zCVJg4QiaMKAjNBGLja5Wif56yciaM was created at 00:16 UTC on the morning of the attack and funded with 28.405 SOL from 6wWVbLeHDVHvCCsLprB85jkbU1JhS9VezABT2ZZFJ5BB. That funder holds about 2,799 SOL and moves through 1,000+ transactions a day across hundreds of counterparties — an exchange hot wallet, so the ownership trail stops there by design.
It deposited 69.59 SOL into uid 148 across six swaps while never holding more than about 28 SOL, because the delivered USDC was swapped straight back into SOL on Jupiter between fills:
| Time UTC | Instruction | Credit to taker |
|---|---|---|
| 06:28:56 | RouteV2 | +23.82 SOL |
| 06:36:21 | Fill | +11.79 SOL |
| 06:44:47 | Fill | +21.81 SOL |
Three miners were drained largely with the same capital going round, topped up each time by a refunded collateral purse.
From 07:01, minutes after uid 148 went down, the same taker wallet began filling miner 2NX58Qr3N59RuHnNHWBZsDq6FCL1oLn5NLwUSBfVm1AE — uid 174, hotkey 5FjuPi5gAFnM2dWsjRd3rki2ffKLMcw2oMh3uQgwgRQazDfk. What we can show:
We are not asserting common ownership
The exchange hop breaks the funding trail, and we found no direct link between uid 174 and the taker's funder. Two readings fit the data: the same operator clearing the deep crown holders and then keeping its own purses alive, or an unrelated small miner that happens to be the taker's next counterparty.
One detail cuts both ways. uid 174 itself took a TimeoutSwap of 0.99 SOL from the same taker at 07:25:22 — either a genuine fourth victim, or cheap camouflage, since a self-dealt slash costs the operator only the 10% penalty. You have the registration and payment records to settle it. We are flagging the pattern, not the verdict.
1 · Reimbursement
For uid 148 the two penalties were 12.430 SOL and 5.4809 TAO. The true economic loss is smaller than the collateral drain, and we would rather be precise than over-claim:
| Component | Amount |
|---|---|
| The 10% penalty itself (1.13 SOL + 0.498 TAO) | −$242 |
| Goods delivered to the taker and never paid for | −$2,431 |
| Margin earned on the four fills that did complete | +$48 |
| Net loss to uid 148 | −$2,625 |
The 100% refund portion is not itself a loss — that money was the taker's deposit, which we still hold. The loss is that we shipped the goods and refunded them. We would accept reimbursement of the penalties in kind (12.430 SOL + 5.4809 TAO) and leave the arithmetic above as the audit trail.
Miners 4V4pPw72… (13.189 SOL) and FneiDSnQ… (13.090 SOL) were hit by the same taker in the same window and are presumably owed the same treatment. We are not acting for them and have not contacted them.
2 · Reset the failed_swaps counters
This matters as much as the money. uid 148 now sits at failed 2 of 3; one more false negative is a permanent eligibility lockout for an identity that has never actually missed a delivery. Both strikes came from deliveries that are final on-chain. Please roll them back for all affected miners, or the bug quietly converts into a permanent ban.
3 · Two missing guards
_fetch_leg, a provider that answers but has not indexed the transaction should not produce the same verdict as a provider that confirms the transaction is absent. Returning 'down' or 'pending' there routes to the EXTEND_TIMEOUT machinery that already exists and was never used here. A cross-check against a second RPC, or a quorum requirement before a slash, would also close it.MarkFulfilled. A final verification pass against that hash immediately before TimeoutSwap executes would have prevented all four of these. As it stands the contract refunds a Fulfilled swap without ever consulting the delivery it is holding.A cheaper partial mitigation, if the verification change is slow to land: for a swap in Fulfilled state carrying a destination hash, cap the penalty at the 10% rather than 110%. That alone takes the jackpot out of the attack.
4 · Everything here is independently verifiable
All transaction hashes, swap keys and timings above come from the public Solana and Bittensor chains and from api.all-ways.io/swaps. Nothing rests on our own logs except the miner-side trace, which is corroborated by the on-chain SubmitSwapClaim / VoteInitiate / MarkFulfilled sequence.