> ## Documentation Index
> Fetch the complete documentation index at: https://base-a060aa97-docs-use-case-ia-overhaul.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# B20 invariants and test cases

> Normative invariants the B20 precompiles guarantee, and the conformance test cases that exercise them.

These invariants and conformance test cases are the normative behavioral guarantees of the B20 precompiles. They complement the [B20 specification](/base-chain/specs/reference/b20) and the [errors & events index](/base-chain/specs/reference/b20/errors-and-events).

## Invariants

### Policy Registry

1. `isAuthorized` never reverts for any combination of `policyId` and `account`.
2. A non-existent `BLOCKLIST` policy authorizes everyone. A non-existent `ALLOWLIST` policy denies everyone.
3. After `renounceAdmin(policyId)`, all membership-mutating calls on that policy revert permanently.
4. `ALWAYS_ALLOW` (ID `0`) authorizes every account. `ALWAYS_BLOCK` denies every account. Neither can be created, modified, or renounced.
5. Policy IDs are globally unique and monotonically increasing within each `PolicyType` prefix.

### Roles

6. The last `DEFAULT_ADMIN_ROLE` holder cannot be removed via `renounceRole` or `revokeRole` — only `renounceLastAdmin()`.
7. After `renounceLastAdmin()`, no address can ever hold `DEFAULT_ADMIN_ROLE` again.
8. Roles granted before admin renunciation continue to function.
9. Custom roles have no built-in effect on any B20 operation.

### Transfer Policies

10. `approve` is never policy-gated.
11. `TRANSFER_EXECUTOR_POLICY` is checked only on `transferFrom`, never on `transfer`.
12. `MINT_RECEIVER_POLICY` is always enforced, even during factory `initCalls`.
13. All three transfer-side scopes are bypassed during `initCalls`.
14. Every scope defaults to `ALWAYS_ALLOW` at token creation.

### Supply

15. `totalSupply` can never exceed the supply cap.
16. The supply cap can never be set below the current `totalSupply`.
17. Burns reduce `totalSupply` and create headroom under the cap.

### Pause

18. Each `PausableFeature` is independent — pausing one does not affect the others.
19. `approve` and `permit` are never affected by any pause state.
20. Pause state is never bypassed during factory `initCalls`.

### Memos

21. The `Memo` event is always emitted at exactly `logIndex + 1` relative to its parent `Transfer` event.
22. Memo methods are functionally identical to their non-memo counterparts in all respects except event emission.

### Permit

23. `permit` only accepts ECDSA signatures. ERC-1271 contract signatures always fail.
24. Each successful `permit` increments the owner's nonce by exactly 1.
25. Permits signed before `updateName` fail after the name change.

### Variants

26. Asset decimals are set at creation and immutable. Valid range is 6–18.
27. Stablecoin decimals are always `6`.
28. `OPERATOR_ROLE` exists only on Asset tokens.
29. Announcement IDs are unique across a token's lifetime.
30. The currency code on a Stablecoin is immutable and contains only `A`–`Z` characters.
31. Multiplier updates affect all holders simultaneously.
32. `batchMint` enforces `MINT_RECEIVER_POLICY` for each recipient individually.

### Factory

33. B20 addresses are deterministic: same inputs always produce the same address.
34. The variant byte at address position 10 always matches the deployed variant.
35. Each `(deployer, variant, salt)` tuple produces exactly one address.
36. `initCalls` execute in array order. A revert in any initCall reverts the entire deployment.

## Test cases

### Policy Registry

| # | Scenario                                               | Expected                       |
| - | ------------------------------------------------------ | ------------------------------ |
| 1 | Create `BLOCKLIST` policy, check unblocked account     | `isAuthorized` returns `true`  |
| 2 | Add account to blocklist, check                        | `isAuthorized` returns `false` |
| 3 | Remove account from blocklist, check                   | `isAuthorized` returns `true`  |
| 4 | Create `ALLOWLIST` policy, check unlisted account      | `isAuthorized` returns `false` |
| 5 | `isAuthorized` with non-existent blocklist-prefixed ID | Returns `true`                 |
| 6 | `isAuthorized` with non-existent allowlist-prefixed ID | Returns `false`                |
| 7 | `renounceAdmin`, then `updateBlocklist`                | Reverts                        |
| 8 | `finalizeUpdateAdmin` from non-pending address         | Reverts                        |

### Roles

| #  | Scenario                                                   | Expected                               |
| -- | ---------------------------------------------------------- | -------------------------------------- |
| 9  | Grant `MINT_ROLE`, call `mint`                             | Succeeds                               |
| 10 | Call `mint` without `MINT_ROLE`                            | Reverts                                |
| 11 | One admin remains, call `revokeRole(DEFAULT_ADMIN_ROLE)`   | Reverts with `LastAdminCannotRenounce` |
| 12 | Call `renounceLastAdmin()`                                 | Succeeds — token becomes admin-less    |
| 13 | After `renounceLastAdmin`, `MINT_ROLE` holder calls `mint` | Succeeds — non-admin roles still work  |
| 14 | Deploy with `initialAdmin == address(0)`, call `grantRole` | Reverts                                |

### Transfer Policies

| #  | Scenario                                                                 | Expected                     |
| -- | ------------------------------------------------------------------------ | ---------------------------- |
| 15 | Sender on blocklist calls `transfer`                                     | Reverts with `PolicyForbids` |
| 16 | Sender on blocklist calls `approve`                                      | Succeeds                     |
| 17 | `transferFrom` where executor is on executor blocklist                   | Reverts                      |
| 18 | Direct `transfer` by sender on executor blocklist (not sender blocklist) | Succeeds                     |
| 19 | During `initCalls`, transfer from blocklisted sender                     | Succeeds — bypass            |
| 20 | During `initCalls`, mint to receiver on mint-receiver blocklist          | Reverts — never bypassed     |

### Mint and Supply Cap

| #  | Scenario                                      | Expected                         |
| -- | --------------------------------------------- | -------------------------------- |
| 21 | Mint that would push `totalSupply` above cap  | Reverts with `SupplyCapExceeded` |
| 22 | Mint exactly to cap                           | Succeeds                         |
| 23 | `updateSupplyCap` below current `totalSupply` | Reverts with `InvalidSupplyCap`  |
| 24 | Burn tokens, then mint up to cap              | Succeeds                         |
| 25 | Mint while `MINT` is paused                   | Reverts                          |

### Burn and Seize

| #  | Scenario                                                         | Expected |
| -- | ---------------------------------------------------------------- | -------- |
| 26 | `burnBlocked` on frozen account                                  | Succeeds |
| 27 | `burnBlocked` on non-frozen account                              | Reverts  |
| 28 | `burnBlocked` by holder of `BURN_ROLE` (not `BURN_BLOCKED_ROLE`) | Reverts  |
| 29 | Freeze, seize full balance, re-mint to recovery                  | Succeeds |
| 30 | Burn while `BURN` is paused                                      | Reverts  |

### Pause

| #  | Scenario                                      | Expected |
| -- | --------------------------------------------- | -------- |
| 31 | Pause `TRANSFER`, call `transfer`             | Reverts  |
| 32 | Pause `TRANSFER`, call `mint`                 | Succeeds |
| 33 | Pause `TRANSFER`, call `approve`              | Succeeds |
| 34 | Pauser calls `unpause` without `UNPAUSE_ROLE` | Reverts  |

### Memos

| #  | Scenario                      | Expected                                                |
| -- | ----------------------------- | ------------------------------------------------------- |
| 35 | `transferWithMemo`            | Emits `Transfer` then `Memo` at consecutive log indices |
| 36 | `transferFromWithMemo`        | `Memo.caller` is `msg.sender`, not `from`               |
| 37 | `transfer` (non-memo variant) | No `Memo` event                                         |

### Permit

| #  | Scenario                                             | Expected |
| -- | ---------------------------------------------------- | -------- |
| 38 | Valid permit with correct signature, nonce, deadline | Succeeds |
| 39 | Permit with expired deadline                         | Reverts  |
| 40 | Replay used permit signature                         | Reverts  |
| 41 | Permit signed before `updateName`, submitted after   | Reverts  |
| 42 | Contract wallet signature (ERC-1271)                 | Reverts  |
| 43 | Permit while `TRANSFER` is paused                    | Succeeds |

### Factory

| #  | Scenario                                                          | Expected         |
| -- | ----------------------------------------------------------------- | ---------------- |
| 44 | `getB20Address` then deploy with same params                      | Addresses match  |
| 45 | Inspect byte 10 of deployed Asset address                         | Returns `0x00`   |
| 46 | Deploy same `(deployer, variant, salt)` twice                     | Second reverts   |
| 47 | Deploy when variant feature not activated                         | Reverts          |
| 48 | `initCalls` that pause `TRANSFER`, then transfer in next initCall | Transfer reverts |

### Variants

| #  | Scenario                                                           | Expected                               |
| -- | ------------------------------------------------------------------ | -------------------------------------- |
| 49 | Deploy Asset with `decimals = 5`                                   | Reverts                                |
| 50 | Update multiplier to `2e18`, check `balanceOf` for raw balance 100 | Returns 200                            |
| 51 | Reuse announcement ID                                              | Reverts with `DuplicateAnnouncementId` |
| 52 | `batchMint` where one recipient is not on allowlist                | Reverts                                |
| 53 | Deploy Stablecoin with `currency = "usd"`                          | Reverts — `A`–`Z` only                 |
