Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions script/smoke/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,10 +109,10 @@ Seven "journeys", run as a whole suite (a single journey can still be run via th
| Journey | What it exercises |
|---|---|
| `factory` | Deterministic create + address prediction, the `isB20` / `isB20Initialized` query surface, and creation-time reverts (duplicate salt, bad decimals, bad currency, unknown variant). |
| `asset` | Full Asset-variant lifecycle (18 decimals): mint, transfer, `transferWithMemo`, delegated `transferFrom`, `announce` + `batchMint`, rebase via `updateMultiplier`, metadata, burn, then the gates that must reject (supply cap, pause, role, announcement-id reuse). The rebase event is fork-aware: V1 emits `MultiplierUpdated`; Cobalt (AssetV2) emits both `MultiplierUpdated` and `UIMultiplierUpdated`. |
| `asset` | Full Asset-variant lifecycle (18 decimals): mint, transfer, `transferWithMemo`, delegated `transferFrom`, an allowlisted `TRANSFER_EXECUTOR_POLICY` on direct and memo transfers, `announce` + `batchMint`, rebase via `updateMultiplier`, metadata, burn, then the gates that must reject (supply cap, pause, role, announcement-id reuse). The rebase event is fork-aware: V1 emits `MultiplierUpdated`; Cobalt (AssetV2) emits both `MultiplierUpdated` and `UIMultiplierUpdated`. |
| `multiplier` | ERC-8056 scheduled multiplier (AssetV2 @ Cobalt): `updateUIMultiplier` scheduling + its guards (`InvalidMultiplier`, `EffectiveAtInPast`, `EffectiveAtTooFar`, `UIMultiplierUpdateExists`), `cancelUIMultiplierUpdate` (+ `UIMultiplierUpdateDoesNotExist`), the `updateMultiplier` instant-failsafe V2 event semantics (`UIMultiplierUpdated` + `UIMultiplierUpdateCancelled` + the deprecated `MultiplierUpdated`), the read aliases (`uiMultiplier`/`balanceOfUI`/`totalSupplyUI`), and ERC-165 advertisement. **Skips** cleanly on a pre-Cobalt chain (probed via `supportsInterface(0xa60bf13d)`). |
| `stablecoin` | Stablecoin-variant deltas (fixed 6 decimals, immutable currency) plus the regulated freeze-and-seize path (blocklist policy + `burnBlocked`). |
| `seize` | Transfer-based seize (AssetV2 @ Cobalt): the `SEIZE_EXEMPT_POLICY` membership gate + `SEIZE_ROLE`, `seizeWithMemo` (`Transfer` -> `Memo` -> `Seized`, supply-preserving), its reject gates (`AccountNotSeizable`, role, `InvalidReceiver`, `ContractPaused`), the admin-op decoupling from the transfer receiver policy on `to`, the `SEIZE_RECEIVER_POLICY` gate on `to` (unset = allow-any, configured = destination must be authorized, else `PolicyForbids`), and the independent `SEIZE` pause vector. **Skips** cleanly on a pre-Cobalt chain (probed via the `SEIZE_EXEMPT_POLICY()` getter). Complements `stablecoin`, which covers the legacy burn-based `burnBlocked`. |
| `seize` | Transfer-based seize (AssetV2 @ Cobalt): the `SEIZE_EXEMPT_POLICY` membership gate + `SEIZE_ROLE`, `seizeWithMemo` (`Transfer` -> `Memo` -> `Seized`, supply-preserving), its reject gates (`AccountNotSeizable`, role, `InvalidReceiver`, `ContractPaused`), the admin-op decoupling from regular transfer receiver and executor policies, the `SEIZE_RECEIVER_POLICY` gate on `to` (unset = allow-any, configured = destination must be authorized, else `PolicyForbids`), and the independent `SEIZE` pause vector. **Skips** cleanly on a pre-Cobalt chain (probed via the `SEIZE_EXEMPT_POLICY()` getter). Complements `stablecoin`, which covers the legacy burn-based `burnBlocked`. |
| `policy` | Policy creation (both types), membership, built-in sentinels, the two-step admin transfer lifecycle, and a token actually *enforcing* a policy (`PolicyForbids` on transfer + mint). |
| `invariants` | EVM-context invariants a precompile must implement explicitly: payable rejection, unknown-selector revert, strict ABI decode, dirty-bit canonicalization, `STATICCALL` read-only enforcement, returndata fidelity, OOG containment, revert atomicity, and gas independence from a force-fed balance. Uses the `PrecompileProbe` + `ForceFeeder` helpers under `test/lib/`. |

Expand Down
50 changes: 50 additions & 0 deletions script/smoke/journeys/asset_lifecycle.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,52 @@ def _journey(c: Chain, tok) -> None:
c.assert_eq(tok.functions.totalSupply().call(), config.amt(1430, 18), "total supply after burn")


def _executor_policy(c: Chain, tok) -> None:
"""Verify the executor policy gates direct transfer and transferWithMemo initiators."""
step("10b", "allowlist deployer as the only transfer executor")
executor_policy = c.create_policy_with_accounts(
c.DEPLOYER,
config.POLICY_TYPE_ALLOWLIST,
[c.DEPLOYER],
)
c.send(tok.functions.updatePolicy(config.TRANSFER_EXECUTOR_POLICY, executor_policy), c.deployer)
c.assert_eq(
tok.functions.TRANSFER_EXECUTOR_POLICY().call(),
config.TRANSFER_EXECUTOR_POLICY,
"TRANSFER_EXECUTOR_POLICY scope",
)
c.assert_eq(
tok.functions.policyId(config.TRANSFER_EXECUTOR_POLICY).call(),
executor_policy,
"executor policy attached",
)

step("10c", "authorized deployer can use transfer and transferWithMemo")
c.send(tok.functions.transfer(c.ALICE, config.amt(1, 18)), c.deployer)
receipt = c.send(tok.functions.transferWithMemo(c.ALICE, config.amt(1, 18), MEMO), c.deployer)
c.assert_log_order(
receipt,
"Transfer(address,address,uint256)",
"Memo(address,bytes32)",
"authorized transferWithMemo emits Memo immediately after Transfer",
)

step("10d", "unlisted user2 is blocked from direct transfer and transferWithMemo before balance checks")
c.expect_revert("PolicyForbids", tok.functions.transfer(c.ALICE, config.amt(1, 18)), c.USER2)
c.expect_revert(
"PolicyForbids",
tok.functions.transferWithMemo(c.ALICE, config.amt(1, 18), MEMO),
c.USER2,
)

# Later lifecycle edges intentionally assert other transfer guards (allowance and pause), so restore
# the default policy after this focused executor-policy check.
c.send(
tok.functions.updatePolicy(config.TRANSFER_EXECUTOR_POLICY, config.ALWAYS_ALLOW_ID),
c.deployer,
)


def _edges(c: Chain, tok) -> None:
step(11, "supply cap: lower cap to current supply, then mint 1 -> SupplyCapExceeded")
total = tok.functions.totalSupply().call()
Expand Down Expand Up @@ -169,6 +215,9 @@ def _events(c: Chain, v2: bool) -> None:
"B20Created(address,uint8,string,string,uint8,bytes)",
"RoleGranted(bytes32,address,address)",
"SupplyCapUpdated(address,uint256,uint256)",
"PolicyCreated(uint64,address,uint8)",
"AllowlistUpdated(uint64,address,bool,address[])",
"PolicyUpdated(bytes32,uint64,uint64)",
"Transfer(address,address,uint256)",
"Memo(address,bytes32)",
"Approval(address,address,uint256)",
Expand All @@ -192,6 +241,7 @@ def run(c: Chain) -> None:
v2 = c.supports_erc165(tok, config.SCALED_UI_AMOUNT_ID)
log(f"multiplier surface: {'ERC-8056 / AssetV2 (UIMultiplierUpdated)' if v2 else 'V1 (MultiplierUpdated)'}")
_journey(c, tok)
_executor_policy(c, tok)
_edges(c, tok)
_events(c, v2)
log("asset-lifecycle: OK")
30 changes: 26 additions & 4 deletions script/smoke/journeys/seize.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@
`InvalidReceiver`, `ContractPaused`), the admin-op decoupling from the *transfer*
receiver policy on `to`, and the `SEIZE_RECEIVER_POLICY` gate on `to` (mirrors
`MINT_RECEIVER_POLICY`: unset = allow-any, configured = the destination must be
authorized).
authorized). It also verifies that `seizeWithMemo` does not consult the regular
`TRANSFER_EXECUTOR_POLICY` initiator gate.

Fork-gated: the whole surface is Cobalt-only. The journey probes the
`SEIZE_EXEMPT_POLICY()` getter and cleanly SKIPS on a pre-Cobalt chain (where the
Expand Down Expand Up @@ -125,16 +126,37 @@ def _edges(c: Chain, tok) -> None:


def _decoupling(c: Chain, tok) -> None:
step(10, "seize ignores the receiver policy on `to`: block bob on TRANSFER_RECEIVER_POLICY, seize still lands")
step(
10,
"seize ignores transfer receiver and executor policies: block bob as receiver and every transfer executor",
)
recv_pid = c.create_policy(c.DEPLOYER, config.POLICY_TYPE_BLOCKLIST)
c.send(tok.functions.updatePolicy(config.TRANSFER_RECEIVER_POLICY, recv_pid), c.deployer)
c.send(c.policy.functions.updateBlocklist(recv_pid, True, [c.BOB]), c.deployer)
c.assert_eq(c.policy.functions.isAuthorized(recv_pid, c.BOB).call(), False, "bob blocked as a receiver")
# A normal transfer to bob would revert PolicyForbids; seize is an admin op and does not consult it.
c.send(
tok.functions.updatePolicy(config.TRANSFER_EXECUTOR_POLICY, config.ALWAYS_BLOCK_ID),
c.deployer,
)
c.assert_eq(
tok.functions.policyId(config.TRANSFER_EXECUTOR_POLICY).call(),
config.ALWAYS_BLOCK_ID,
"every transfer executor blocked",
)
# A normal transfer to bob would revert PolicyForbids. Seize is an admin operation: it consults
# neither the regular transfer receiver policy nor the regular transfer executor policy.
c.expect_revert("PolicyForbids", tok.functions.transfer(c.BOB, 1), c.DEPLOYER)
c.send(tok.functions.seizeWithMemo(c.ALICE, c.BOB, config.amt(100, 18), MEMO), c.deployer)
c.assert_eq(tok.functions.balanceOf(c.BOB).call(), config.amt(500, 18), "bob received the seize despite receiver policy")
c.assert_eq(
tok.functions.balanceOf(c.BOB).call(),
config.amt(500, 18),
"bob received the seize despite transfer policies",
)
c.assert_eq(tok.functions.balanceOf(c.ALICE).call(), config.amt(500, 18), "alice debited")
c.send(
tok.functions.updatePolicy(config.TRANSFER_EXECUTOR_POLICY, config.ALWAYS_ALLOW_ID),
c.deployer,
)


def _pause(c: Chain, tok) -> None:
Expand Down
Loading