fix(thorname): separate affiliate collectors and correct payout accounting
Summary
Give each THORName its own affiliate collector so names sharing an owner no longer mix earnings or pay each other’s balances to different recipients.
The upgrade automatically moves each existing collector balance into the owner’s named collector when they have exactly one stored THORName whose spelling and length meet current registration rules. Owners with multiple THORNames, no remaining THORNames, or a sole THORName that fails those rules receive their nonzero balance directly in native RUNE, except module-owned balances, which go to Reserve. Their new per-name collectors start at zero. Registrations, paid registration periods, aliases, and preferred assets remain unchanged.
Closes #2119 (closed).
Background and SwapKit’s request
This MR was prompted by SwapKit’s investigation of an IronWallet/FEW payout reported as approximately 33 times the expected amount.
SwapKit registers partner THORNames using an owner address it controls. Although few and tan have different payout addresses, their collector balances were shared because both names have the same owner. A payout triggered by one name could therefore send fees earned through another name to the triggering name’s recipient.
SwapKit reported halting THORChain through its integration and identified 32 affected partners. GiMa explicitly requested a THORNode MR, and the subsequent discussion agreed on separate collectors and an automatic store migration that pays the existing pooled balance to the owner in RUNE.
The shared accounting also exposes accumulated REVSHARE: when names share an owner, one name’s preferred-asset payout can include REVSHARE earned by another name, even if the receiving name has no REVSHARE allocation. This is a general consequence of the old collector design, it does not establish that SwapKit’s own REVSHARE was paid to FEW/TAN.
Collector separation
- Replace owner-address keys with case-insensitive THORName keys under
affcol_name/. - Credit ordinary collected affiliate fees, REVSHARE, and preferred-asset bond-yield payouts to the relevant name.
- Use each name’s collector balance and configured alias for preferred-asset payouts.
- Make
/thorchain/thorname/<name>report that name’s balance rather than the owner’s shared total. - Update collector protobufs, generated code, and genesis handling. Reject invalid collector records and duplicate names, including duplicates differing only by case.
REVSHARE eligibility, attribution, rates, and strict-first-affiliate selection remain unchanged. The change separates where the resulting earnings accumulate.
Automatic migration and existing balances
Module migration 18 -> 19 performs a one-time migration for all existing owner-based collectors. There is no opt-in mode, migration memo, or requirement to change owners.
For each owner:
- If the owner is a module account, transfer its nonzero balance to Reserve.
- Otherwise, if the owner has exactly one stored THORName whose spelling and length meet current registration rules, move the balance into that THORName’s collector without a wallet payment or fee.
- Otherwise, pay the entire nonzero shared collector balance directly to the owner’s THORChain address in one combined native RUNE payment, without an outbound fee. This includes owners whose sole stored THORName fails current spelling rules, such as
thorswap.fi. - Delete the legacy collector record after successful migration.
- Start subsequent accounting in separate per-name collectors.
Names are counted using ownership at the upgrade block, including expired names.
The migration preserves THORName ownership, aliases, preferred assets, and paid registration periods. It emits affiliate_collector_migration events containing the owner and RUNE amount, plus thorname when crediting a named collector.
All reconciliation transfers, named credits, migration payments, and legacy-record deletions commit atomically. A malformed record or failed transfer leaves them unapplied. Legacy key decoding remains confined to the migration. Normal collector code has no owner-based fallback.
Historical earnings cannot be split accurately from the shared collector total. The combined owner payment does not identify each partner’s share. Reconciliation and distribution require a separate off-chain analysis of transaction history and are outside this MR.
Funds already committed to pending swaps are excluded from the migrated available balance and continue through their queues.
Ownership changes and expiry
Before transferring a THORName or re-registering it after expiry, pay its available collector balance to the previous owner in native RUNE. The payment and ownership change commit together - if payment fails, the ownership change fails.
Pending conversions and failed-payout refunds remain attached to the THORName and may credit its collector after ownership changes. They are not tracked separately for former owners.
New registrations and ownership transfers cannot assign module accounts as owners, including configured module accounts that have not yet been created. Updates that leave an existing owner unchanged remain allowed.
Related swap and accounting fixes
The MR also corrects collector handling around execution, settlement, and retries:
- Atomic payout creation: commit the payout voter, queue entry, fund movement, and affiliate collector reset together. A failed trigger cannot leave partial state when its caller logs the error and continues.
- Credit on delivery: credit converted RUNE together with its actual delivery to the affiliate collector module, using the amount received after applicable fees. V1 handles this during execution, V2 handles it during settlement.
- Single delivery path: prevent affiliate collector swaps from also scheduling a generic outbound or entering an unrelated downstream memo handler.
- Correct failed-payout restoration: restore unused preferred-payout RUNE to both the affiliate collector module and the named ledger without charging a generic refund outbound fee.
- Safe retries: commit restoration or delivery together with queue removal. Keep failed restoration available for retry and prevent repeated settlement from paying or restoring funds twice.
- Internal conversion validation: reject external swaps targeting the collector module unless they match the internal affiliate-conversion path.
A separate commit fixes a broader solvency issue found while testing payout failures: completed advanced swaps retained after failed settlement must still count as outstanding liabilities. Their output has already left the pool but remains owed. Solvency now accounts for that output and only the unspent input, including for non-streaming swaps. This prevents retained payouts from appearing as surplus funds and avoids counting consumed deposits twice.
Observed impact over the last 90 days
The activity review covered 98 affiliate THORNames belonging to 84 current owner addresses. Of these owners, four had multiple names used during the period that currently share an owner-based collector and have preferred assets configured.
| Owner / name family | Unique swaps in 90 days | Current payout configuration | Expected effect after migration |
|---|---|---|---|
SwapKit - few, tan, and related names |
1,676 | Multiple preferred assets and payout addresses under one owner | Separates balances and payouts by THORName. SwapKit requested this behaviour. |
Vultisig - v0, va, vi |
729 | Same preferred asset and payout address for all three names | Future preferred-asset payouts retain the same asset and recipient. Independent balances can change payout timing and batch amounts. |
Unknown operator - lake, pg, tape |
2 | All three prefer ETH.USDC, with different payout addresses | Each THORName accrues its own balance for its configured recipient. Only lake and pg were used during the period. |
Unknown operator - bluff, gp, gpm, pencil |
19 | Different preferred assets and payout addresses, gp and pencil share an address |
Each THORName accrues independently. ` |
Validation
Tested Migrate18to19 against an isolated mainnet snapshot at height 27,841,753:
- Migrated all 79 legacy collectors: 51 named collectors and 28 direct payouts.
- Correctly reconciled the 0.84011399 RUNE shortfall from Reserve.
- Verified expected balances, unchanged RUNE supply, and preserved THORNames and queues.
- Confirmed a second invocation makes no changes and results persist after reopening the database.
- All 8 focused migration tests passed.