diff --git a/allways/__init__.py b/allways/__init__.py index 9522109..8d9040a 100644 --- a/allways/__init__.py +++ b/allways/__init__.py @@ -1,3 +1,3 @@ -__version__ = '1.0.6' +__version__ = '1.0.7' version_split = __version__.split('.') __spec_version__ = (1000 * int(version_split[0])) + (10 * int(version_split[1])) + (1 * int(version_split[2])) diff --git a/allways/chains.py b/allways/chains.py index 56dc645..d30d816 100644 --- a/allways/chains.py +++ b/allways/chains.py @@ -21,6 +21,9 @@ class ChainDefinition: env_prefix: str # .env variable prefix (e.g. 'BTC' -> BTC_RPC_URL) seconds_per_block: int = 12 # Average block time on this chain min_confirmations: int = 1 # Minimum confirmations before accepting a transaction + # Smallest amount that can actually exist/move on-chain, in native units + # (BTC dust floor, TAO existential deposit). 1 = no floor. + min_onchain_amount: int = 1 # ─── Supported Chains ──────────────────────────────────── @@ -32,6 +35,8 @@ class ChainDefinition: env_prefix='BTC', seconds_per_block=600, min_confirmations=2, + # 1000 sat, not the bare 546 P2PKH dust line: margin vs higher dustrelayfee / wallet quirks, and a tighter executable-rate ceiling. + min_onchain_amount=1000, ) CHAIN_TAO = ChainDefinition( id='tao', @@ -41,6 +46,8 @@ class ChainDefinition: env_prefix='TAO', seconds_per_block=12, min_confirmations=6, + # Existential deposit: accounts below this are reaped. + min_onchain_amount=500, ) SUPPORTED_CHAINS = { diff --git a/allways/utils/rate.py b/allways/utils/rate.py index 38f3af6..42ea538 100644 --- a/allways/utils/rate.py +++ b/allways/utils/rate.py @@ -127,19 +127,20 @@ def is_executable_rate( min_swap_rao: int, max_swap_rao: int, ) -> bool: - """True iff the rate is integer-routable in its declared direction. + """True iff the rate is fundably routable in its declared direction. - Crown-eligibility gate against sentinel rates that no user can route: + Crown-eligibility gate against rates that no user can route. Routable means + a source >= the source chain's ``min_onchain_amount`` (dust / existential + deposit) maps a TAO leg into ``[min_swap_rao, max_swap_rao]`` — a rate whose + only in-bounds source is sub-dust (e.g. 1 sat -> 0.5 TAO at 5e7 TAO/BTC) is + unfundable, so unexecutable. - * BTC→TAO: high-side sentinels (``1e10``, ``1.797e308`` TAO/BTC) — the - smallest positive sat already maps above ``max_swap_rao``, so no - positive integer source produces an in-bounds TAO leg. - * TAO→BTC: low-side sentinels (``1e-8`` TAO/BTC) — the TAO leg IS the - source amount, so it trivially fits any bounds, but the destination - payout implied by the rate is absurd. Catch this by the symmetric - check on the inverse rate: if treating ``1/rate`` as a BTC→TAO rate - has no integer-routable source either, the original rate is at an - extreme of the executable spectrum and a sentinel by symmetry. + * BTC→TAO: high-side rates — even the smallest fundable sat maps above + ``max_swap_rao``, so no fundable source produces an in-bounds TAO leg. + * TAO→BTC: low-side rates — the TAO leg IS the source, so it trivially fits + any bounds, but the destination payout is absurd. Caught by the symmetric + check on ``1/rate``: if the inverse direction has no fundable source, the + original rate is at an extreme of the executable spectrum. A bound at ``0`` is the contract's "unset" sentinel and disables that side; both at 0 → permissive (no on-chain bounds yet). @@ -151,15 +152,18 @@ def is_executable_rate( def _has_integer_routable_source(forward_rate: float, src_chain: str) -> bool: # For a "src → tao" direction at ``forward_rate`` (tao per src), is - # there a positive integer src amount whose TAO leg lands in bounds? - src_decimals = get_chain(src_chain).decimals - decimal_factor = 10 ** (get_chain('tao').decimals - src_decimals) + # there an src amount that is fundable on-chain (>= the chain's + # min_onchain_amount) whose TAO leg lands in bounds? + src = get_chain(src_chain) + decimal_factor = 10 ** (get_chain('tao').decimals - src.decimals) denom = forward_rate * decimal_factor if not math.isfinite(denom) or denom <= 0: # rate × decimal_factor overflowed (e.g. 1.797e308 × 10) → smallest # positive integer source already maps above any finite max bound. return False - min_source = max(1, math.ceil(max(1, min_swap_rao) / denom)) + # Floor at the source chain's dust/existential minimum: a rate whose only + # in-bounds source is below it (e.g. 1 sat) is unfundable, so unexecutable. + min_source = max(src.min_onchain_amount, math.ceil(max(1, min_swap_rao) / denom)) if max_swap_rao <= 0: return True max_source = math.floor(max_swap_rao / denom) diff --git a/pyproject.toml b/pyproject.toml index 1991816..7bbe497 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "allways" -version = "1.0.6" +version = "1.0.7" description = "Allways - Universal Transaction Layer: Trustless cross-chain swaps on Bittensor Subnet 7" license = "MIT" requires-python = ">=3.10,<3.15" diff --git a/tests/test_rate.py b/tests/test_rate.py index 372e195..4682551 100644 --- a/tests/test_rate.py +++ b/tests/test_rate.py @@ -2,6 +2,7 @@ from decimal import Decimal +from allways.chains import get_chain from allways.constants import BTC_TO_SAT, RATE_PRECISION, TAO_TO_RAO from allways.utils.rate import apply_fee_deduction, calculate_to_amount, is_executable_rate, normalize_rate @@ -326,30 +327,38 @@ def test_tao_to_btc_lowball_rate_rejected(self): max_swap on the TAO leg, so the original rate is sentinel-low.""" assert is_executable_rate(1e-8, 'tao', 'btc', self.MIN, self.MAX) is False - def test_boundary_rate_executable_at_one_sat(self): - """At max_swap/10 = 50_000_000 rao per sat, the smallest integer sat - produces exactly max_swap — should be executable.""" - rate = self.MAX / 10 # TAO leg at 1 sat == max_swap - assert is_executable_rate(rate, 'btc', 'tao', self.MIN, self.MAX) is True + DUST = get_chain('btc').min_onchain_amount # smallest fundable BTC source - def test_just_past_boundary_rate_rejected(self): - """A rate that maps 1 sat just above max_swap → no executable sat.""" - rate = (self.MAX / 10) * 1.0001 + def test_sub_dust_boundary_rate_rejected(self): + """At max_swap/10, the only in-bounds source is 1 sat — below the BTC + dust floor, so unfundable. Rejected (the crown-squat rate).""" + rate = self.MAX / 10 # TAO leg at 1 sat == max_swap; 1 sat < dust assert is_executable_rate(rate, 'btc', 'tao', self.MIN, self.MAX) is False - def test_tao_to_btc_boundary_rate_executable_at_one_sat(self): - """Symmetric boundary: at inverse=max_swap/10 (i.e. r = 10/max_swap), - treating 1/r as btc→tao maps 1 sat to exactly max_swap on the TAO leg. - Just executable.""" - rate = 10 / self.MAX # 1/r * 10 == max_swap; 1 sat at inverse hits max - assert is_executable_rate(rate, 'tao', 'btc', self.MIN, self.MAX) is True + def test_dust_floor_boundary_rate_executable(self): + """At the rate where the dust floor maps exactly to max_swap, the + smallest fundable source is in-bounds — just executable.""" + rate = self.MAX / (10 * self.DUST) # DUST sat → max_swap on the TAO leg + assert is_executable_rate(rate, 'btc', 'tao', self.MIN, self.MAX) is True - def test_tao_to_btc_just_past_boundary_rate_rejected(self): - """One ULP below the boundary: the inverse rate's 1 sat overshoots - max_swap, so no integer source routes by symmetry.""" - rate = (10 / self.MAX) * 0.9999 + def test_just_past_dust_floor_boundary_rejected(self): + """Just above the boundary, even the dust floor overshoots max_swap → + no fundable source routes.""" + rate = (self.MAX / (10 * self.DUST)) * 1.0001 + assert is_executable_rate(rate, 'btc', 'tao', self.MIN, self.MAX) is False + + def test_tao_to_btc_sub_dust_boundary_rate_rejected(self): + """Symmetric: r = 10/max_swap maps 1 sat (sub-dust) to max_swap on the + inverse leg — rejected (the swap-1670 tao→btc crown-squat rate).""" + rate = 10 / self.MAX assert is_executable_rate(rate, 'tao', 'btc', self.MIN, self.MAX) is False + def test_tao_to_btc_dust_floor_boundary_executable(self): + """Symmetric boundary at the dust floor: the dust-clearing inverse + source maps in-bounds — just executable.""" + rate = (10 * self.DUST) / self.MAX + assert is_executable_rate(rate, 'tao', 'btc', self.MIN, self.MAX) is True + def test_tao_to_btc_sentinel_unset_bounds_still_permissive(self): """Unset bounds disable the gate in both directions — keeps the legacy "no on-chain bounds yet" path permissive.""" diff --git a/uv.lock b/uv.lock index 7a3cfd0..f6427db 100644 --- a/uv.lock +++ b/uv.lock @@ -164,7 +164,7 @@ wheels = [ [[package]] name = "allways" -version = "1.0.6" +version = "1.0.7" source = { editable = "." } dependencies = [ { name = "base58" },