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
2 changes: 1 addition & 1 deletion script/smoke/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ Seven "journeys", run as a whole suite (a single journey can still be run via th
| `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 `MultiplierUpdated` vs Cobalt `UIMultiplierUpdated`). |
| `multiplier` | ERC-8056 scheduled multiplier (AssetV2 @ Cobalt): `setUIMultiplier` scheduling + its guards (`InvalidMultiplier`, `EffectiveAtInPast`, `EffectiveAtTooFar`, `ScheduleOverlap`), `cancelScheduledMultiplier` (+ `NoScheduledMultiplier`), the `updateMultiplier` instant-failsafe V2 event semantics (`UIMultiplierUpdated` + `MultiplierUpdateCancelled`, *not* `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_HOLDER_POLICY` membership gate + `SEIZE_ROLE`, `seizeWithMemo` (`Transfer` -> `Memo` -> `Seized`, supply-preserving), its reject gates (`AccountNotSeizable`, role, `InvalidReceiver`, `ContractPaused`), the admin-op decoupling from the receiver policy on `to`, and the independent `SEIZE` pause vector. **Skips** cleanly on a pre-Cobalt chain (probed via the `SEIZE_HOLDER_POLICY()` getter). Complements `stablecoin`, which covers the legacy burn-based `burnBlocked`. |
| `seize` | Transfer-based seize (AssetV2 @ Cobalt): the `SEIZE_HOLDER_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_HOLDER_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
1 change: 1 addition & 0 deletions script/smoke/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ def _role(name: str) -> bytes:
TRANSFER_EXECUTOR_POLICY = _role("TRANSFER_EXECUTOR_POLICY")
MINT_RECEIVER_POLICY = _role("MINT_RECEIVER_POLICY")
SEIZE_HOLDER_POLICY = _role("SEIZE_HOLDER_POLICY")
SEIZE_RECEIVER_POLICY = _role("SEIZE_RECEIVER_POLICY")


@dataclass(frozen=True)
Expand Down
37 changes: 33 additions & 4 deletions script/smoke/journeys/seize.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@
it is NOT authorized by that policy), `seizeWithMemo` (`Transfer` -> `Memo` -> `Seized`,
supply-preserving because seize is a reassignment, not a burn), and the `SEIZE` pause
vector — plus the gates that must reject (`AccountNotSeizable`, role,
`InvalidReceiver`, `ContractPaused`) and the admin-op decoupling from the receiver
policy on `to`.
`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).
Fork-gated: the whole surface is Cobalt-only. The journey probes the
`SEIZE_HOLDER_POLICY()` getter and cleanly SKIPS on a pre-Cobalt chain (where the
Expand Down Expand Up @@ -56,8 +58,11 @@ def _is_cobalt(c: Chain, tok) -> bool:


def _journey(c: Chain, tok) -> None:
step(1, "getters: SEIZE_HOLDER_POLICY() and SEIZE_ROLE() match the keccak constants")
step(1, "getters: SEIZE_HOLDER_POLICY(), SEIZE_RECEIVER_POLICY() and SEIZE_ROLE() match the keccak constants")
c.assert_eq(tok.functions.SEIZE_HOLDER_POLICY().call(), config.SEIZE_HOLDER_POLICY, "SEIZE_HOLDER_POLICY scope")
c.assert_eq(
tok.functions.SEIZE_RECEIVER_POLICY().call(), config.SEIZE_RECEIVER_POLICY, "SEIZE_RECEIVER_POLICY scope"
)
c.assert_eq(tok.functions.SEIZE_ROLE().call(), config.SEIZE_ROLE, "SEIZE_ROLE id")

step(2, "mint(alice, 1000); mint(deployer, 10)")
Expand Down Expand Up @@ -142,8 +147,30 @@ def _pause(c: Chain, tok) -> None:
c.assert_eq(tok.functions.totalSupply().call(), config.amt(1010, 18), "total supply still unchanged across all seizes")


def _receiver_policy(c: Chain, tok) -> None:
# SEIZE_RECEIVER_POLICY gates `to`, mirroring MINT_RECEIVER_POLICY: unset = allow-any,
# configured = the destination must be authorized. Balances entering here: alice=410, bob=600.
step(10, "SEIZE_RECEIVER_POLICY unset (default): seize to any destination is allowed")
c.assert_eq(tok.functions.SEIZE_RECEIVER_POLICY().call(), config.SEIZE_RECEIVER_POLICY, "receiver scope getter")
# Deployer is not on any allowlist; with the scope unset (ALWAYS_ALLOW) the seize still lands.
c.send(tok.functions.seizeWithMemo(c.ALICE, c.DEPLOYER, config.amt(10, 18), MEMO), c.deployer)
c.assert_eq(tok.functions.balanceOf(c.DEPLOYER).call(), config.amt(10, 18), "deployer received seize (unset receiver policy)")

step(11, "configure SEIZE_RECEIVER_POLICY allowlist(bob): seize to bob (authorized) succeeds")
recv_pid = c.create_policy(c.DEPLOYER, config.POLICY_TYPE_ALLOWLIST)
c.send(tok.functions.updatePolicy(config.SEIZE_RECEIVER_POLICY, recv_pid), c.deployer)
c.send(c.policy.functions.updateAllowlist(recv_pid, True, [c.BOB]), c.deployer)
c.assert_eq(c.policy.functions.isAuthorized(recv_pid, c.BOB).call(), True, "bob authorized as seize receiver")
c.assert_eq(c.policy.functions.isAuthorized(recv_pid, c.DEPLOYER).call(), False, "deployer not an authorized receiver")
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(700, 18), "bob received seize (authorized receiver)")

step(12, "receiver policy forbids an unauthorized destination -> PolicyForbids(SEIZE_RECEIVER_POLICY)")
c.expect_revert("PolicyForbids", tok.functions.seizeWithMemo(c.ALICE, c.DEPLOYER, 1, MEMO), c.DEPLOYER)


def _events(c: Chain) -> None:
step(10, "expected events emitted across the flow")
step(13, "expected events emitted across the flow")
c.assert_events_emitted(
"seize events",
"B20Created(address,uint8,string,string,uint8,bytes)",
Expand All @@ -153,6 +180,7 @@ def _events(c: Chain) -> None:
"Seized(address,address,address,uint256)",
"PolicyCreated(uint64,address,uint8)",
"BlocklistUpdated(uint64,address,bool,address[])",
"AllowlistUpdated(uint64,address,bool,address[])",
"PolicyUpdated(bytes32,uint64,uint64)",
"Paused(address,uint8[])",
"Unpaused(address,uint8[])",
Expand All @@ -169,5 +197,6 @@ def run(c: Chain) -> None:
_edges(c, tok)
_decoupling(c, tok)
_pause(c, tok)
_receiver_policy(c, tok)
_events(c)
log("seize: OK")
Loading