Skip to content

[Bug] TAO->BTC reserve viability can approve arbitrary BTC payouts because collateral/slash exposure only tracks the TAO leg #396

@JSONbored

Description

@JSONbored

Summary

For TAO->BTC swaps, the protocol checks reserve viability, collateral, min/max bounds, confirm fees, and timeout slash against tao_amount only. The non-TAO destination amount (to_amount) can be arbitrarily large if a miner posts a near-zero TAO/BTC rate.

That means a route can be marked viable and accepted as an exact quote even when it promises an impossible BTC payout. If it times out, the miner's slash is still only the TAO leg, not the BTC payout it advertised.

This is related to #392/#393, but it is a narrower protocol/economic invariant: destination-side payout exposure is not bounded or collateral-backed for reverse swaps.

Live evidence

Current live quote:

uv run alw swap quote --from tao --to btc --amount 0.1

#  UID  Rate    You Receive          Collateral   Status
1  253  1000    0.00009900 BTC       0.5153 TAO   available
2  14   999     0.00009910 BTC       0.5060 TAO   available
3  65   340.82  0.00029048 BTC       0.5782 TAO   available
4  189  340.32  0.00029091 BTC       0.4792 TAO   available
5  193  1e-08   9900000.00000000 BTC 0.5998 TAO   available

The UID 193 row says a user sending 0.1 TAO would receive 9,900,000 BTC, and the CLI marks it available because the TAO leg is within bounds and below the miner's TAO collateral.

Local reserve-path proof

Using the same shared math as CLI, validator, and miner:

from_chain=tao
to_chain=btc
from_amount=0.1 TAO
rate=1e-08
collateral=0.5998 TAO
min_swap=0.1 TAO
max_swap=0.5 TAO

Local proof output:

canonical_pair=btc->tao is_reverse=True
send_tao=0.1 rate=1e-08
raw_to_amount_sats=1000000000000000
user_receives_btc=9900000
tao_leg=0.1 collateral_tao=0.5998
check_swap_viability=True reason=ok
quote_within_slippage_exact=True
confirm_fee_tao=0.001
timeout_slash_tao=0.1
promised_btc_to_timeout_slash_ratio=9.900000E+7 BTC_per_TAO_slashed

So the exact reserve quote passes:

  • quote_within_slippage(to_amount, to_amount, 200) == True
  • check_swap_viability(0.1 TAO, 0.5998 TAO collateral, 0.1-0.5 TAO bounds) == True

But the destination payout is 9,900,000 BTC.

Code path

The validator reserve path recomputes to_amount from the miner commitment and checks exact/slippage consistency, but the bounds and collateral gates only compare synapse.tao_amount:

allways/validator/axon_handlers.py:366-385
expected_to_amount = recompute_reserve_amounts(...)
expected_tao_amount = derive_tao_leg(...)
if synapse.tao_amount != expected_tao_amount: reject
if not quote_within_slippage(synapse.to_amount, expected_to_amount, slippage_bps): reject

allways/validator/axon_handlers.py:407-422
if synapse.tao_amount > collateral: reject
if synapse.tao_amount < min_swap: reject
if synapse.tao_amount > max_swap: reject

The shared viability helper mirrors the same invariant:

allways/utils/rate.py:122-141
def check_swap_viability(tao_amount_rao, miner_collateral_rao, min_swap_rao, max_swap_rao):
    if tao_amount_rao < min_swap_rao: ...
    if tao_amount_rao > max_swap_rao: ...
    if tao_amount_rao > miner_collateral_rao: ...
    return True

The contract stores to_amount, but reserve and initiate also only gate the TAO leg against bounds/collateral:

smart-contracts/ink/lib.rs:574-590
if tao_amount < min_swap_amount: AmountBelowMinimum
if tao_amount > max_swap_amount: AmountAboveMaximum
if miner_collateral < min_collateral: InsufficientCollateral

smart-contracts/ink/lib.rs:855-872
if tao_amount/from_amount/to_amount != reservation: InvalidAmount
if tao_amount > miner_collateral: InsufficientCollateral

On timeout, the slash is also only swap.tao_amount:

smart-contracts/ink/lib.rs:1026-1029
let actual_slash = this.apply_collateral_penalty(swap.miner, swap.tao_amount);

On success, the protocol fee charged to miner collateral is also only based on swap.tao_amount:

smart-contracts/ink/lib.rs:976-982
let fee = swap.tao_amount.saturating_div(FEE_DIVISOR);
let actual_fee = this.apply_collateral_penalty(swap.miner, fee);

Why this matters

For TAO->BTC, a miner can post a tiny TAO/BTC rate that implies an arbitrary BTC payout. The system treats the quote as viable because the user source is a small TAO amount inside the contract bounds.

The miner is never required to collateralize the promised BTC output. If it fails, timeout slash is only the TAO source leg. If it somehow confirms, the fee is only 1% of the TAO source leg.

That is not a safe market invariant. The protocol is treating tao_amount as the universal exposure metric even when the actual user-facing promise is a non-TAO destination amount.

Duplicate check

Raw authenticated GraphQL duplicate sweep checked:

payout collateral
destination collateral
BTC payout collateral
non-TAO destination collateral
TAO->BTC collateral
1e-08
9900000
absurd BTC
economically collateral
collateral-backed
output amount
to_amount collateral
to_amount bounds
destination payout
vote_reserve to_amount
tao_amount/from_amount/to_amount
slash exposure
timeout slash
quote available
expected_to_amount
derive_tao_leg

Closest related items:

No existing open/closed issue or PR was found for non-TAO destination payouts being unbounded relative to collateral/slash exposure.

Expected direction

The protocol needs a direction-aware exposure invariant. Possible fixes:

  • Reject non-TAO destination amounts that exceed a sane value derived from miner collateral, current bounds, and/or rate sanity.
  • Add a destination-side payout cap for non-TAO outputs.
  • Make crown/reserve eligibility require that the posted quote is economically collateral-backed, not only TAO-leg-valid.
  • For CLI, do not mark TAO->BTC routes as available when the destination output is nonsensical relative to collateral/exposure.

The key invariant: a route should not be considered reservable if the miner's maximum penalty is a small TAO amount while the quoted destination payout is effectively unbounded.

Validation

Commands run:

uv run alw swap quote --from tao --to btc --amount 0.1
uv run pytest tests/test_rate.py tests/test_axon_handlers.py -q

Result:

79 passed

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions