From 7558493cabf4ef1dc2d0c9164a88d608be002ca8 Mon Sep 17 00:00:00 2001 From: Rayyan Alam Date: Thu, 17 Sep 2026 10:36:46 -0400 Subject: [PATCH] fix(smoke): update AbiDecodeFailed shape and drop stale burnBlocked ABI fragment (#226) AbiDecodeFailed reverts now carry a bare function selector with no message, so the harness's length check and docs were tightened to match. Separately, PR #193 restored burnBlocked to the IB20 interface after #186 removed it, so the stablecoin journey's hand-built ABI fragment for it now collides with the real entry under a stricter web3.py ABI validator; call it directly through the normal token binding instead. Co-authored-by: Claude (cherry picked from commit f39eea1d11f634697c21e5a868342aa6b9662bc4) --- script/smoke/chain.py | 8 +++---- script/smoke/journeys/stablecoin_lifecycle.py | 22 ++----------------- 2 files changed, 6 insertions(+), 24 deletions(-) diff --git a/script/smoke/chain.py b/script/smoke/chain.py index cfdbd5d3..b4755f70 100644 --- a/script/smoke/chain.py +++ b/script/smoke/chain.py @@ -331,8 +331,8 @@ def _revert_bytes(exc: ContractLogicError) -> bytes | None: def expect_abi_decode_failed(self, desc: str, fn, frm: ChecksumAddress) -> None: """Simulate `fn` via eth_call; assert Rust precompile AbiDecodeFailed revert shape. - Dispatch decode failures encode as `function_selector || utf8_error`, not a typed - custom error such as InvalidVariant(). + Dispatch decode failures encode as a bare `function_selector` (no message; the error + string was dropped), not a typed custom error such as InvalidVariant(). """ fn_selector = bytes(HexBytes(fn.selector)) try: @@ -361,7 +361,7 @@ def _assert_abi_decode_revert( if raw is None: self._diagnose(f"expected ABI decode failure: {desc}", repro_fn, repro_overrides, repro_call) die(f"expected ABI decode failure for {desc} but revert had no data") - if len(raw) <= 4: + if len(raw) < 4: self._diagnose(f"expected ABI decode failure: {desc}", repro_fn, repro_overrides, repro_call) die(f"expected ABI decode failure for {desc} but revert was only {len(raw)} byte(s): 0x{raw.hex()}") if raw[:4] != fn_selector: @@ -382,7 +382,7 @@ def expect_raw_abi_decode_failed( value: int = 0, frm: ChecksumAddress | None = None, ) -> None: - """Assert hand-built calldata reverts with AbiDecodeFailed (selector || utf8).""" + """Assert hand-built calldata reverts with AbiDecodeFailed (bare selector, no message).""" if len(data) < 4: die(f"expected ABI decode failure for {desc} but calldata is shorter than 4 bytes") fn_selector = data[:4] diff --git a/script/smoke/journeys/stablecoin_lifecycle.py b/script/smoke/journeys/stablecoin_lifecycle.py index 456db595..e15f7635 100644 --- a/script/smoke/journeys/stablecoin_lifecycle.py +++ b/script/smoke/journeys/stablecoin_lifecycle.py @@ -8,27 +8,9 @@ from __future__ import annotations from .. import config -from ..abis import STABLECOIN_ABI from ..chain import Chain, log, step from ..codec import StablecoinCreateParams, init_call -# `burnBlocked` is a deprecated precompile selector that base-std #186 removed from the IB20 -# interface, so it is absent from STABLECOIN_ABI. The selector still dispatches on the precompile -# (retained for back-compat), so bind it via an explicit fragment to keep exercising the legacy -# burn-based freeze-and-seize path. The transfer-based seize surface is covered by the `seize` journey. -_BURN_BLOCKED_FRAGMENT = { - "type": "function", - "name": "burnBlocked", - "stateMutability": "nonpayable", - "inputs": [{"name": "from", "type": "address"}, {"name": "amount", "type": "uint256"}], - "outputs": [], -} - - -def _burn_blocked_at(c: Chain, tok): - """A token binding including the deprecated `burnBlocked` selector (absent from STABLECOIN_ABI).""" - return c.w3.eth.contract(address=tok.address, abi=[*STABLECOIN_ABI, _BURN_BLOCKED_FRAGMENT]) - def _setup(c: Chain): salt = c.cfg.salt_for("stablecoin") @@ -73,7 +55,7 @@ def _journey(c: Chain, tok) -> None: c.assert_eq(c.policy.functions.isAuthorized(pid, c.ALICE).call(), False, "alice blocked") step(5, "seize: burnBlocked(alice, 400); Transfer then BurnedBlocked") - receipt = c.send(_burn_blocked_at(c, tok).functions.burnBlocked(c.ALICE, config.amt(400, 6)), c.deployer) + receipt = c.send(tok.functions.burnBlocked(c.ALICE, config.amt(400, 6)), c.deployer) c.assert_eq(tok.functions.balanceOf(c.ALICE).call(), config.amt(600, 6), "alice balance after seize") c.assert_eq(tok.functions.totalSupply().call(), config.amt(1100, 6), "total supply after seize") c.assert_log_order( @@ -86,7 +68,7 @@ def _journey(c: Chain, tok) -> None: def _edges(c: Chain, tok) -> None: step(6, "seize an unblocked account -> AccountNotBlocked") - c.expect_revert("AccountNotBlocked", _burn_blocked_at(c, tok).functions.burnBlocked(c.BOB, 1), c.DEPLOYER) + c.expect_revert("AccountNotBlocked", tok.functions.burnBlocked(c.BOB, 1), c.DEPLOYER) step(7, "role gate: user2 mint -> AccessControlUnauthorizedAccount") c.expect_revert("AccessControlUnauthorizedAccount", tok.functions.mint(c.ALICE, 1), c.USER2)