diff --git a/docs/AGENTS.md b/docs/AGENTS.md index ee0c2c875..919bb44de 100644 --- a/docs/AGENTS.md +++ b/docs/AGENTS.md @@ -77,7 +77,7 @@ npx skills add base/base-skills |SDKs & APIs/Base Verify API:sdks/base-verify/overview,sdks/base-verify/verify-social-accounts,sdks/base-verify/verify-users-onchain |SDKs & APIs/Migrated Documentation:sdks/migrated-products |Upgrades/Overview:upgrades/overview,base-chain/network-information/configuration-changelog -|Upgrades/Denim:upgrades/denim/overview,upgrades/denim/200ms-blocks,upgrades/denim/migrate-from-flashblocks +|Upgrades/Denim:upgrades/denim/overview,upgrades/denim/200ms-blocks,upgrades/denim/migrate-from-flashblocks,base-chain/specs/reference/b20/changelog/03-denim-b20-token-receiver,base-chain/specs/reference/b20/changelog/03-denim-b20-transfer-executor-enforcement,base-chain/specs/reference/b20/changelog/03-denim-policyregistry-not-policy |Upgrades/Cobalt:upgrades/cobalt/overview,upgrades/cobalt/dynamic-upgrades,base-chain/specs/reference/b20/changelog/02-cobalt-b20asset-multiplier,base-chain/specs/reference/b20/changelog/02-cobalt-b20-seize,base-chain/specs/reference/b20/changelog/02-cobalt-policyregistry-composite-policy,upgrades/cobalt/validity-transactions |Upgrades/Beryl:upgrades/beryl/overview,upgrades/beryl/reth-v2,upgrades/beryl/reducing-canonical-withdrawal-delay,upgrades/beryl/b20 |Upgrades/Azul:upgrades/azul/overview,upgrades/azul/node-upgrade,upgrades/azul/exec-engine,upgrades/azul/proofs diff --git a/docs/base-chain/specs/reference/b20/changelog/02-cobalt-b20-seize.mdx b/docs/base-chain/specs/reference/b20/changelog/02-cobalt-b20-seize.mdx index 4c632817f..5c12f9649 100644 --- a/docs/base-chain/specs/reference/b20/changelog/02-cobalt-b20-seize.mdx +++ b/docs/base-chain/specs/reference/b20/changelog/02-cobalt-b20-seize.mdx @@ -1,5 +1,5 @@ --- -title: "B20: Seize Surface and burnBlocked Deprecation" +title: "B20: Seize Functionality" description: "The B20 seize surface at Cobalt and the deprecation of burnBlocked. Migration notes for teams integrated against the Beryl seize path." --- diff --git a/docs/base-chain/specs/reference/b20/changelog/02-cobalt-b20asset-multiplier.mdx b/docs/base-chain/specs/reference/b20/changelog/02-cobalt-b20asset-multiplier.mdx index 987ab3ca7..19b40c7a1 100644 --- a/docs/base-chain/specs/reference/b20/changelog/02-cobalt-b20asset-multiplier.mdx +++ b/docs/base-chain/specs/reference/b20/changelog/02-cobalt-b20asset-multiplier.mdx @@ -1,5 +1,5 @@ --- -title: "B20: Beryl to Cobalt Migration" +title: "B20: ERC-8056 Conformant Multiplier" description: "B20 Asset multiplier becomes ERC-8056 conformant at Cobalt, with a scheduled multiplier setter for corporate actions. Every Beryl selector stays dialable." --- @@ -13,9 +13,11 @@ conformant and gains a scheduled multiplier setter for corporate actions. Nothin breaks: every Beryl selector, event topic, and error keeps its exact 4-byte selector or topic0 and stays dialable at Cobalt. The deprecations below are advisory, not enforced. -To migrate, adopt the canonical ERC-8056 names (`uiMultiplier`, `toUIAmount`/`fromUIAmount`, -`balanceOfUI`, `totalSupplyUI`), and move routine multiplier changes from the instant -`updateMultiplier(uint256)` to the scheduled `updateUIMultiplier(uint256,uint256)`. +To migrate, adopt the ERC-8056 names (`uiMultiplier`, `toUIAmount`/`fromUIAmount`, `balanceOfUI`, +`totalSupplyUI`), and move routine multiplier changes from the instant `updateMultiplier(uint256)` to +the scheduled `updateUIMultiplier(uint256,uint256)`. `multiplier()` and `scaledBalanceOf(address)` +remain the canonical B20 names; `uiMultiplier()` and `balanceOfUI(address)` are their ERC-8056 +aliases. Cobalt hasn't gone live yet. Until it activates, only the Beryl surface exists on-chain. @@ -26,12 +28,12 @@ The selectors and topic0s below are the real values from the frozen ABIs: `abi/v ### Functions -| Beryl symbol (selector) | Cobalt canonical (selector) | Status | Why | +| Beryl symbol (selector) | Cobalt ERC-8056 symbol (selector) | Status | Why | | --- | --- | --- | --- | -| `multiplier()` `0x1b3ed722` | `uiMultiplier()` `0xa60bf13d` | deprecated-name-kept / new alias | ERC-8056 core naming. Both return the same effective multiplier. `multiplier()` stays. | +| `multiplier()` `0x1b3ed722` | `uiMultiplier()` `0xa60bf13d` | unchanged (canonical name) / new alias | ERC-8056 core naming. Both return the same effective multiplier. | | `toScaledBalance(uint256)` `0x04f04c99` | `toUIAmount(uint256)` `0x3248d4ff` | deprecated-dialable / new | ERC-8056 Conversion extension. Byte-identical behavior. | | `toRawBalance(uint256)` `0x0ca06c44` | `fromUIAmount(uint256)` `0x65cd9b3c` | deprecated-dialable / new | ERC-8056 Conversion extension. Byte-identical behavior. | -| `scaledBalanceOf(address)` `0x1da24f3e` | `balanceOfUI(address)` `0x437a9958` | deprecated-name-kept / new alias | ERC-8056 Balances extension. Alias, same value. | +| `scaledBalanceOf(address)` `0x1da24f3e` | `balanceOfUI(address)` `0x437a9958` | unchanged (canonical name) / new alias | ERC-8056 Balances extension. Alias, same value. | | `updateMultiplier(uint256)` `0x5ffe6146` | `updateUIMultiplier(uint256,uint256)` `0x628e600f` | deprecated-dialable / new (not 1:1) | The canonical path is now the scheduled setter. The instant setter remains as an emergency failsafe. | | — | `newUIMultiplier()` `0xdc767007` | new | ERC-8056 pending-schedule read. | | — | `effectiveAt()` `0x97a4064f` | new | ERC-8056 pending-schedule read (flip timestamp). | diff --git a/docs/base-chain/specs/reference/b20/changelog/03-denim-b20-token-receiver.mdx b/docs/base-chain/specs/reference/b20/changelog/03-denim-b20-token-receiver.mdx new file mode 100644 index 000000000..27dd2b6be --- /dev/null +++ b/docs/base-chain/specs/reference/b20/changelog/03-denim-b20-token-receiver.mdx @@ -0,0 +1,132 @@ +--- +title: "B20: Reject the Token Itself as a Credit Recipient" +description: "Denim reverts InvalidReceiver when a transfer, mint, or seize credits the B20 token's own address, so a mistyped recipient fails instead of locking funds." +--- + +## Abstract + +[Denim](/upgrades/denim/overview) adds `address(this)` as a second trigger of the existing +`InvalidReceiver(address receiver)` error. `transfer`, `transferFrom`, their memo variants, `mint`, +`mintWithMemo`, `batchMint`, and `seizeWithMemo` revert `InvalidReceiver(to)` when `to` is the +token's own address. The guard covers the `IB20` transfer, mint, and seize paths plus +`IB20Asset.batchMint`, so it applies to both B20 Asset and B20 Stablecoin. + +A holder sending to themselves (`from == to`) still succeeds. An issuer can still recover tokens +already credited to the token address: `seizeWithMemo` with `from == address(token)` succeeds. + +This change is breaking for any integration that sends to the token address today. It adds no new +functions, events, errors, or selectors. + +## Motivation + +Users often paste the token address instead of the recipient's address. A B20 token is a precompile +with no holder key, so it cannot call `transfer` on itself. After a credit lands at the token +address, the sender cannot recover it; only the issuer can, through `seizeWithMemo`. + +There is no valid use case for a B20 token to hold its own tokens. Denim reverts on that destination +so the mistaken send fails instead of locking the funds. + +## What Changed + +### Receiver Guard + +`InvalidReceiver` already fires for `address(0)` (ERC-6093). Denim extends the same guard, at the +same position in the revert order: + +```solidity Before (Cobalt) +if (to == address(0)) revert InvalidReceiver(to); +``` + +```solidity After (Denim) +if (to == address(0) || to == address(this)) revert InvalidReceiver(to); +``` + +| Symbol | Selector | Status | Behavior | +| --- | --- | --- | --- | +| `InvalidReceiver(address)` | `0x9cfea583` | Extended | Now also fires when `to == address(this)`. | + +### Revert Order + +The check runs at the existing invalid-receiver step. Canonical order is otherwise unchanged: + +| Function | Check order | +| --- | --- | +| `transfer` / `transferWithMemo` | pause → **invalid-receiver** → zero-sender → executor policy → sender policy → receiver policy → balance | +| `transferFrom` / `transferFromWithMemo` | pause → **invalid-receiver** → zero-sender → allowance → executor policy → sender policy → receiver policy → balance | +| `mint` / `mintWithMemo` | pause → role → **invalid-receiver** → mint-receiver policy → supply cap | +| `batchMint` | pause → role → length / empty → per-element **invalid-receiver** → `_mint` body | +| `seizeWithMemo` | pause → role → **invalid-receiver** → zero-sender → self-seize (`from == to`) → seizable → seize-receiver policy → balance | + +The guard checks `to` only. `from` may equal `address(this)`, so a seize that drains the token +address into a treasury still succeeds. + +## Examples + +A holder transfer to the token reverts: + +```solidity Transfer to Token Address +vm.prank(alice); +token.transfer({to: address(token), amount: amount}); // reverts InvalidReceiver(address(token)) +``` + +Mint and seize to the token address revert the same way: + +```solidity Mint or Seize to Token Address lines expandable wrap +token.mint({to: address(token), amount: amount}); // reverts InvalidReceiver(address(token)) + +token.seizeWithMemo({ + from: alice, to: address(token), amount: amount, memo: memo +}); // reverts InvalidReceiver(address(token)) +``` + +A self-send and a recovery seize both still succeed: + +```solidity Unaffected Paths lines expandable wrap +vm.prank(alice); +token.transfer({to: alice, amount: amount}); // succeeds; balance and totalSupply unchanged + +token.seizeWithMemo({ + from: address(token), to: treasury, amount: amount, memo: memo +}); // succeeds +``` + +## Design Decisions and Alternatives Considered + +Denim reuses `InvalidReceiver` and compares `to` against `address(this)` on every credit path. The +destination is invalid for the same reason `address(0)` is: no holder can spend the credited units. +Wallets that already treat `InvalidReceiver` as "do not send here" keep the same revert handling. + +### Reject Any B20-Prefix Address + +A prefix check cannot tell a B20 token from a user-controlled account in the same address space, +such as a multisig. Rejecting the whole prefix would revert valid transfers, so Denim checks +`address(this)` only. Sends to other B20 tokens still succeed. + +### Call `isB20Initialized(to)` + +This would reject only live tokens, but it adds a factory call on every credit path. + +### New `SelfSend(address)` Error + +A dedicated error would read more clearly in traces, but it adds ABI surface for a condition +`InvalidReceiver` already describes. + +### Also Reject `from == address(this)` + +Blocking spends from the token address would close the only recovery path for balances already +sitting there. + +## Migration + +1. Treat the token's own address as an invalid recipient in wallets, custodians, and indexers, the + same way you treat `address(0)`. +2. After Denim activates, expect `InvalidReceiver` from any transfer, mint, or seize to + `address(token)` that succeeded before. +3. Recover a balance credited to the token address before activation with + `seizeWithMemo(address(token), treasury, amount, memo)`. The caller must hold `SEIZE_ROLE`, and + the token must be seizable under `SEIZE_EXEMPT_POLICY`. +4. No change is needed for self-transfers, approvals, burns, or sends to other B20 tokens. + +The [`seizeWithMemo`](/specifications/b20/reference/interfaces/ib20/seize-with-memo) and +[`transfer`](/specifications/b20/reference/interfaces/ib20/transfer) reference pages describe +behavior before Denim activates. diff --git a/docs/base-chain/specs/reference/b20/changelog/03-denim-b20-transfer-executor-enforcement.mdx b/docs/base-chain/specs/reference/b20/changelog/03-denim-b20-transfer-executor-enforcement.mdx new file mode 100644 index 000000000..498869141 --- /dev/null +++ b/docs/base-chain/specs/reference/b20/changelog/03-denim-b20-transfer-executor-enforcement.mdx @@ -0,0 +1,142 @@ +--- +title: "B20: Transfer Executor Policy Enforcement" +description: "Denim enforces TRANSFER_EXECUTOR_POLICY on msg.sender for every transfer path, closing the transfer and self-transferFrom bypasses." +--- + +## Abstract + +[Denim](/upgrades/denim/overview) applies `TRANSFER_EXECUTOR_POLICY` to every transfer path. The +executor gate now checks `msg.sender` on `transfer`, `transferFrom`, `transferWithMemo`, and +`transferFromWithMemo`, including when `msg.sender == from`. Before Denim, the check ran only on +the `transferFrom` paths, and only when `msg.sender != from`. + +This change is breaking for a token that already set a restrictive `TRANSFER_EXECUTOR_POLICY`: +holders who moved their own tokens with `transfer` or self-`transferFrom` must now be authorized as +initiators. A token that never set the policy keeps the unset always-allow default and is +unaffected. The change adds no new selectors, events, errors, or storage. + +## Motivation + +An issuer of a restricted security token may require every transfer to go through a registered +transfer agent. Holders approve the agent, and only the agent calls `transferFrom`. +`TRANSFER_EXECUTOR_POLICY` is the initiator allowlist for that pattern, but before Denim it had two +gaps: + +1. **`transfer` never consulted the executor policy.** A holder could always move their own tokens + through `transfer`, regardless of the allowlist. +2. **`transferFrom` skipped the check when `msg.sender == from`.** A holder could call + `transferFrom(self, to, amount)` to reach the same unchecked path. + +Both gaps let a non-allowlisted holder move tokens by picking a different entrypoint. Denim brings +`TRANSFER_EXECUTOR_POLICY` to parity with `TRANSFER_SENDER_POLICY` and `TRANSFER_RECEIVER_POLICY`, +which already run on every transfer path. + +## What Changed + +### Transfer-Side Scopes + +All three transfer-side scopes now run inside the shared `_transfer` helper that backs `transfer`, +`transferFrom`, and their memo variants: + +| Scope | Account checked | Before Denim | Denim | +| --- | --- | --- | --- | +| `TRANSFER_SENDER_POLICY` | `from` | Every transfer path | Every transfer path | +| `TRANSFER_RECEIVER_POLICY` | `to` | Every transfer path | Every transfer path | +| `TRANSFER_EXECUTOR_POLICY` | `msg.sender` | `transferFrom` paths, only when `msg.sender != from` | Every transfer path | + +All three scopes are still bypassed during the factory bootstrap window (`_isPrivileged()`), so a +token's `initCalls` can move newly minted supply without pre-authorizing the factory. + +### Revert Order + +Pause, zero-actor, and allowance checks stay in the entrypoints. The executor check moves into +`_transfer`, where it runs first, before the sender and receiver checks: + +| Function | Before Denim | Denim | +| --- | --- | --- | +| `transfer` / `transferWithMemo` | pause → zero-receiver → zero-sender → sender policy → receiver policy → balance | pause → invalid-receiver → zero-sender → **executor policy** → sender policy → receiver policy → balance | +| `transferFrom` / `transferFromWithMemo` | pause → zero-receiver → zero-sender → allowance → executor policy (skipped if `msg.sender == from`) → sender policy → receiver policy → balance | pause → invalid-receiver → zero-sender → allowance → **executor policy** → sender policy → receiver policy → balance | + +At Denim, the invalid-receiver step also rejects the token's own address; see +[Reject the Token Itself as a Credit Recipient](/base-chain/specs/reference/b20/changelog/03-denim-b20-token-receiver). +When more than one check would fail, the caller sees the first revert in that order. + +### Gas + +`_transfer` reads all three transfer-side policy IDs from the existing packed slot in one `SLOAD`. +On `transferFrom`, this removes the second (warm) read the entrypoint used to make. + +On `transfer`, the executor lookup is new. When `TRANSFER_EXECUTOR_POLICY` equals +`TRANSFER_SENDER_POLICY` and `from == msg.sender` (including both slots unset, `ALWAYS_ALLOW_ID`), +`_transfer` reuses the executor result and skips the sender `isAuthorized` call. A default +`transfer` therefore still makes two `isAuthorized` calls. + +## Examples + +A holder moving their own tokens is now gated by the executor policy: + +```solidity Holder Transfer Blocked by Executor Policy +token.updatePolicy(TRANSFER_EXECUTOR_POLICY, ALWAYS_BLOCK_ID); + +vm.prank(alice); +token.transfer(bob, amount); // reverts PolicyForbids(TRANSFER_EXECUTOR_POLICY, ALWAYS_BLOCK_ID) +``` + +An executor allowlist restricts initiation to a transfer agent: + +```solidity Transfer Agent Allowlist lines expandable wrap highlight={6-7,9-10} +address[] memory agents = new address[](1); +agents[0] = transferAgent; +uint64 executorAllowlist = + policyRegistry.createPolicyWithAccounts(admin, IPolicyRegistry.PolicyType.ALLOWLIST, agents); +token.updatePolicy(TRANSFER_EXECUTOR_POLICY, executorAllowlist); + +vm.prank(transferAgent); +token.transferFrom(alice, bob, amount); // succeeds: transferAgent is allowlisted + +vm.prank(alice); +token.transfer(bob, amount); // reverts PolicyForbids(TRANSFER_EXECUTOR_POLICY, ...): alice is not allowlisted +``` + +The factory bootstrap bypass still applies. This sketch elides the `createB20` arguments: + +```solidity Bootstrap Bypass +initCalls = [ + abi.encodeCall(IB20.mint, (address(factory), amount)), + abi.encodeCall(IB20.updatePolicy, (TRANSFER_EXECUTOR_POLICY, ALWAYS_BLOCK_ID)), + abi.encodeCall(IB20.transfer, (to, amount)) +]; +factory.createB20(..., initCalls); // succeeds: bootstrap window bypasses the executor check +``` + +## Design Decisions and Alternatives Considered + +Denim centralizes the executor check in `_transfer`, on `msg.sender`, with no `msg.sender == from` +carve-out. It is the smallest change that closes both gaps, adds no interface surface, and matches +how the sender and receiver scopes are already enforced. + +### Fold Pause, Zero-Actor, and Allowance Into `_transfer` + +Allowance is specific to `transferFrom`. Folding it in would need a consume-allowance flag, and +moving the zero-actor checks after allowance would change revert order. + +### Add a Separate Check to `transfer` + +Adding a matching check to `transfer` while keeping the `transferFrom` carve-out leaves the +self-`transferFrom` bypass open and duplicates the check across two entrypoints. + +## Migration + +No action is needed for a token that never set `TRANSFER_EXECUTOR_POLICY`. The unset slot stays +always-allow, and the bootstrap bypass is unchanged. + +For a token with a restrictive `TRANSFER_EXECUTOR_POLICY`: + +1. **Holders using `transfer`:** they must be authorized under `TRANSFER_EXECUTOR_POLICY`, directly + or through a policy they belong to, to keep moving their own tokens. +2. **Holders using self-`transferFrom`:** the same authorization now applies to that path. +3. **To keep allowing holder-initiated transfers:** add those holders, or a policy covering them, to + the executor allowlist before Denim activates. + +The [`TRANSFER_EXECUTOR_POLICY`](/specifications/b20/reference/interfaces/ib20/transfer-executor-policy) +reference page describes the scope as it behaves before Denim activates. diff --git a/docs/base-chain/specs/reference/b20/changelog/03-denim-policyregistry-not-policy.mdx b/docs/base-chain/specs/reference/b20/changelog/03-denim-policyregistry-not-policy.mdx new file mode 100644 index 000000000..1a967a355 --- /dev/null +++ b/docs/base-chain/specs/reference/b20/changelog/03-denim-policyregistry-not-policy.mdx @@ -0,0 +1,167 @@ +--- +title: "PolicyRegistry: NOT / Invert Policies" +description: "Denim reserves bit 63 of a policy ID as an invert flag, so isAuthorized returns the opposite of any policy's result without a second, mirrored policy." +--- + +## Abstract + +[Denim](/upgrades/denim/overview) lets any policy ID reference the opposite (NOT) of its result at +query time. When bit 63 (`INVERTED_POLICY_BIT`) is set, `isAuthorized` resolves the base policy and +returns the negation of that policy's decision. The flag applies to every policy type: `ALLOWLIST`, +`BLOCKLIST`, `UNION`, and `INTERSECT`. + +Members stay on the base policy and are shared, not copied, so an update to the base also updates +its inverse. Invert creates no new record and no new create path. The change is non-breaking: +existing IDs have bit 63 unset and behave exactly as before. + +## Motivation + +The Policy Registry increasingly serves as a shared registry of address lists that other policies +compose around. For example, an issuer may maintain one KYC list and need it to mean "only these +addresses" in one scope and "exclude these addresses" in another. + +Before Denim, the opposite outcome required a second policy of the other type with a copy of the +same addresses. Every membership change had to land on both; if one update lagged, valid accounts +were rejected or invalid ones admitted. A composite that needed "NOT A" had to point at that mirror. + +Encoding inversion in the policy reference makes one registry entry reusable for `A OR B`, +`A AND NOT B`, or `NOT A` without extra policies. + +## What Changed + +### Policy ID Layout + +A policy ID is a `uint64`. Bits `0–55` hold a unique counter; bits `56–63` hold the `PolicyType`. +The four types in use (`0–3`) occupy bits `56–57`, leaving `58–63` unused. Denim reserves bit `63` +as the invert bit. + +```text Policy ID Layout + 63 62 56 55 0 ++---+-------------+-------------------------------+ +| I | PolicyType | unique counter | ++---+-------------+-------------------------------+ +``` + +### Interface Changes + +```solidity IPolicyRegistry.sol +// PolicyRegistryConstants +uint64 internal constant INVERTED_POLICY_BIT = uint64(1) << 63; + +function invertedPolicyId(uint64 policyId) external view returns (uint64); +``` + +| Symbol | Selector | Status | Behavior | +| --- | --- | --- | --- | +| `invertedPolicyId(uint64)` | `0x6b468933` | New view | Toggles bit 63 (`policyId ^ INVERTED_POLICY_BIT`). Never reverts, reads no state, and is involutive. | +| `isAuthorized(uint64,address)` | Unchanged | Extended | An inverted ID resolves the base and returns the negated result. Fail-closed on an unknown or malformed base. | +| `policyExists(uint64)` | Unchanged | Extended | Strips to base: `policyExists(invertedPolicyId(id)) == policyExists(id)`. | +| `policyAdmin(uint64)` | Unchanged | Extended | Strips to base. | +| `pendingPolicyAdmin(uint64)` | Unchanged | Extended | Strips to base. | +| `compositePolicyChildIds(uint64)` | Unchanged | Extended | Strips the queried composite's own flag; returns child IDs verbatim, including any per-child invert bit. | +| `createCompositePolicy(address,uint8,uint64[])` | Unchanged | Extended | A child ID may carry the invert bit ("A AND NOT X"); validated against its base. | +| `updateComposite(uint64,uint64[])` | Unchanged | Extended | Same per-child invert handling. | + +`invertedPolicyId` does not check existence. A missing or malformed base is denied later, at +`isAuthorized`. + +### Behavioral Changes + +#### Authorization + +`isAuthorized` gains a leading invert branch. All non-inverted paths are unchanged. + +```text Authorization Evaluation lines expandable wrap highlight={2-6} +isAuthorized(policyId, account): + if policyId has INVERTED_POLICY_BIT set: + base = policyId without the bit + if not policyExists(base): # fail-closed guard + return false + return not isAuthorized(base, account) + + ... existing ALLOWLIST / BLOCKLIST / UNION / INTERSECT dispatch ... +``` + +Inverting a composite negates the composite's combined result. An inverted ID over a never-created +base returns `false`; it never becomes allow-everyone. + +#### Getters Strip to Base + +Read views clear bit 63 with a shared `_basePolicyId(id) = id & ~INVERTED_POLICY_BIT` helper and read +the base record. An inverted ID has no record of its own; it mirrors the base's existence, admin, +pending admin, and child set. A token can store an inverted ID and re-validate it exactly as it would +a plain one. + +#### Composite Children + +A composite child may carry the invert bit. The registry checks existence and type against the base: + +| Child | Result | +| --- | --- | +| Inverted `ALLOWLIST` or `BLOCKLIST` | Accepted; stored with the invert bit kept | +| Inverted `UNION` or `INTERSECT` | Reverts `InvalidChildPolicy`, preserving the flat-tree invariant | +| Base does not exist | Reverts `PolicyNotFound`, which still takes precedence across the whole child set | +| Built-in sentinel (`ALWAYS_ALLOW_ID`, `ALWAYS_BLOCK_ID`), with or without the invert bit | Reverts `InvalidChildPolicy`, unchanged from Cobalt | + +### State and Gas + +Denim adds no storage slots. Invert is query-time only: storage keys, type decode, and existence +resolve against the stripped ID. An inverted query runs the fail-closed existence guard on the base, +then the existing dispatch, then one boolean flip in memory. Non-inverted queries are unchanged. + +## Examples + +Given a shared `ALLOWLIST` `sanctionedId` whose members are sanctioned addresses (authorized means +on the list), the inverse authorizes every account that is not on the list: + +```solidity Standalone Invert +uint64 notSanctioned = policyRegistry.invertedPolicyId(sanctionedId); // sanctionedId ^ (1 << 63) +``` + +Allow transfers only for accounts on `kycId` and not on `sanctionedId`, with an `INTERSECT` +composite and an inverted child: + +```solidity Composite With an Inverted Child +uint64[] memory children = new uint64[](2); +children[0] = kycId; +children[1] = policyRegistry.invertedPolicyId(sanctionedId); // not sanctioned +policyRegistry.createCompositePolicy(admin, IPolicyRegistry.PolicyType.INTERSECT, children); +``` + +Fail-closed: for any never-created base, `isAuthorized(base | INVERTED_POLICY_BIT, account)` returns +`false`. + +Round-trip: `invertedPolicyId(invertedPolicyId(id)) == id`, and +`policyExists(invertedPolicyId(id)) == policyExists(id)`. + +## Design Decisions and Alternatives Considered + +Encoding NOT in bit 63 adds no storage and no create path, and any policy, simple or composite, can +be inverted on its own. The tradeoff: the flag occupies unused `PolicyType` bitspace, and every +getter must strip it through `_basePolicyId`. + +### New `NOT` Policy Type + +`createNot(admin, base)` would allocate a record pointing at a base, with the clearest explorer +legibility. Standalone NOT would cost ~3 `SLOAD`s vs 1 for a mirror blocklist, and "A AND NOT X" +~6 vs 4. It also adds a create path and deepens hot-path recursion as a composite child. + +### Per-Child Invert Bitmask on the Composite + +A ≤4-bit mask packed into the children length word would flip individual children. It only works +inside a composite, so a simple policy could not be inverted without wrapping it in a two-child +composite. It could later compose on top of the invert bit. + +## Migration + +This change is non-breaking. All existing selectors, events, and errors are unchanged, and existing +composites are unaffected. To adopt: + +1. Compute the inverse with `invertedPolicyId(policyId)`, or set bit 63 directly. +2. Bind it to a B20 scope with `updatePolicy`, or pass it as a composite child. B20 needs no change; + it treats the ID as an opaque `uint64`. +3. Consumers that store policy IDs must still validate `policyExists(policyId)` at write time. This + works for inverted IDs because existence resolves to the base. + +For the current interface, see the +[IPolicyRegistry reference](/specifications/b20/reference/interfaces/i-policy-registry/index). diff --git a/docs/docs.json b/docs/docs.json index f495cce83..abaf9567c 100644 --- a/docs/docs.json +++ b/docs/docs.json @@ -465,7 +465,10 @@ "pages": [ "upgrades/denim/overview", "upgrades/denim/200ms-blocks", - "upgrades/denim/migrate-from-flashblocks" + "upgrades/denim/migrate-from-flashblocks", + "base-chain/specs/reference/b20/changelog/03-denim-b20-token-receiver", + "base-chain/specs/reference/b20/changelog/03-denim-b20-transfer-executor-enforcement", + "base-chain/specs/reference/b20/changelog/03-denim-policyregistry-not-policy" ] }, { diff --git a/docs/llms-full.txt b/docs/llms-full.txt index 668c00d68..ec2459c41 100644 --- a/docs/llms-full.txt +++ b/docs/llms-full.txt @@ -510,21 +510,27 @@ const client = createPublicClient({ chain: base, transport: http() }) ### Denim -- [Overview](https://docs.base.org/upgrades/denim/overview): Denim introduces native blocks at a 200ms cadence, onchain millisecond time through BaseTime, and millisecond-resolution RPC timestamps. +- [Overview](https://docs.base.org/upgrades/denim/overview): Denim introduces native blocks at a 200ms cadence, onchain millisecond time through BaseTime, millisecond-resolution RPC timestamps, and B20 transfer and policy improvements. - [200ms Native Blocks](https://docs.base.org/upgrades/denim/200ms-blocks): Specification for Denim's canonical 200ms blocks, including BaseTime, derivation, validation, and RPC behavior. - [Migrate From Flashblocks](https://docs.base.org/upgrades/denim/migrate-from-flashblocks): Migrate your Flashblocks integration to 200ms blocks. +- [B20: Reject the Token Itself as a Credit Recipient](https://docs.base.org/base-chain/specs/reference/b20/changelog/03-denim-b20-token-receiver): Denim reverts InvalidReceiver when a transfer, mint, or seize credits the B20 token's own address, so a mistyped recipient fails instead of locking funds. + +- [B20: Transfer Executor Policy Enforcement](https://docs.base.org/base-chain/specs/reference/b20/changelog/03-denim-b20-transfer-executor-enforcement): Denim enforces TRANSFER_EXECUTOR_POLICY on msg.sender for every transfer path, closing the transfer and self-transferFrom bypasses. + +- [PolicyRegistry: NOT / Invert Policies](https://docs.base.org/base-chain/specs/reference/b20/changelog/03-denim-policyregistry-not-policy): Denim reserves bit 63 of a policy ID as an invert flag, so isAuthorized returns the opposite of any policy's result without a second, mirrored policy. + ### Cobalt - [Overview](https://docs.base.org/upgrades/cobalt/overview): Cobalt improves the B20 token standard, adds validity transactions, introduces dynamic node upgrades, and migrates TEE signer registration to onchain attestation verification. - [Dynamic Upgrades](https://docs.base.org/upgrades/cobalt/dynamic-upgrades): An Ethereum smart contract stores upgrade timestamps for Base nodes, allowing forks to activate at the scheduled time with no client restart. -- [B20: Beryl to Cobalt Migration](https://docs.base.org/base-chain/specs/reference/b20/changelog/02-cobalt-b20asset-multiplier): B20 Asset multiplier becomes ERC-8056 conformant at Cobalt, with a scheduled multiplier setter for corporate actions. Every Beryl selector stays dialable. +- [B20: ERC-8056 Conformant Multiplier](https://docs.base.org/base-chain/specs/reference/b20/changelog/02-cobalt-b20asset-multiplier): B20 Asset multiplier becomes ERC-8056 conformant at Cobalt, with a scheduled multiplier setter for corporate actions. Every Beryl selector stays dialable. -- [B20: Seize Surface and burnBlocked Deprecation](https://docs.base.org/base-chain/specs/reference/b20/changelog/02-cobalt-b20-seize): The B20 seize surface at Cobalt and the deprecation of burnBlocked. Migration notes for teams integrated against the Beryl seize path. +- [B20: Seize Functionality](https://docs.base.org/base-chain/specs/reference/b20/changelog/02-cobalt-b20-seize): The B20 seize surface at Cobalt and the deprecation of burnBlocked. Migration notes for teams integrated against the Beryl seize path. - [PolicyRegistry: Composite Policies (UNION / INTERSECT)](https://docs.base.org/base-chain/specs/reference/b20/changelog/02-cobalt-policyregistry-composite-policy): Cobalt adds UNION and INTERSECT composite policies to PolicyRegistry so B20 integrations can combine simple authorization policies without flattening their member lists. diff --git a/docs/llms.txt b/docs/llms.txt index 603e87a01..58720b776 100644 --- a/docs/llms.txt +++ b/docs/llms.txt @@ -444,21 +444,27 @@ ### Denim -- [Overview](https://docs.base.org/upgrades/denim/overview): Denim introduces native blocks at a 200ms cadence, onchain millisecond time through BaseTime, and millisecond-resolution RPC timestamps. +- [Overview](https://docs.base.org/upgrades/denim/overview): Denim introduces native blocks at a 200ms cadence, onchain millisecond time through BaseTime, millisecond-resolution RPC timestamps, and B20 transfer and policy improvements. - [200ms Native Blocks](https://docs.base.org/upgrades/denim/200ms-blocks): Specification for Denim's canonical 200ms blocks, including BaseTime, derivation, validation, and RPC behavior. - [Migrate From Flashblocks](https://docs.base.org/upgrades/denim/migrate-from-flashblocks): Migrate your Flashblocks integration to 200ms blocks. +- [B20: Reject the Token Itself as a Credit Recipient](https://docs.base.org/base-chain/specs/reference/b20/changelog/03-denim-b20-token-receiver): Denim reverts InvalidReceiver when a transfer, mint, or seize credits the B20 token's own address, so a mistyped recipient fails instead of locking funds. + +- [B20: Transfer Executor Policy Enforcement](https://docs.base.org/base-chain/specs/reference/b20/changelog/03-denim-b20-transfer-executor-enforcement): Denim enforces TRANSFER_EXECUTOR_POLICY on msg.sender for every transfer path, closing the transfer and self-transferFrom bypasses. + +- [PolicyRegistry: NOT / Invert Policies](https://docs.base.org/base-chain/specs/reference/b20/changelog/03-denim-policyregistry-not-policy): Denim reserves bit 63 of a policy ID as an invert flag, so isAuthorized returns the opposite of any policy's result without a second, mirrored policy. + ### Cobalt - [Overview](https://docs.base.org/upgrades/cobalt/overview): Cobalt improves the B20 token standard, adds validity transactions, introduces dynamic node upgrades, and migrates TEE signer registration to onchain attestation verification. - [Dynamic Upgrades](https://docs.base.org/upgrades/cobalt/dynamic-upgrades): An Ethereum smart contract stores upgrade timestamps for Base nodes, allowing forks to activate at the scheduled time with no client restart. -- [B20: Beryl to Cobalt Migration](https://docs.base.org/base-chain/specs/reference/b20/changelog/02-cobalt-b20asset-multiplier): B20 Asset multiplier becomes ERC-8056 conformant at Cobalt, with a scheduled multiplier setter for corporate actions. Every Beryl selector stays dialable. +- [B20: ERC-8056 Conformant Multiplier](https://docs.base.org/base-chain/specs/reference/b20/changelog/02-cobalt-b20asset-multiplier): B20 Asset multiplier becomes ERC-8056 conformant at Cobalt, with a scheduled multiplier setter for corporate actions. Every Beryl selector stays dialable. -- [B20: Seize Surface and burnBlocked Deprecation](https://docs.base.org/base-chain/specs/reference/b20/changelog/02-cobalt-b20-seize): The B20 seize surface at Cobalt and the deprecation of burnBlocked. Migration notes for teams integrated against the Beryl seize path. +- [B20: Seize Functionality](https://docs.base.org/base-chain/specs/reference/b20/changelog/02-cobalt-b20-seize): The B20 seize surface at Cobalt and the deprecation of burnBlocked. Migration notes for teams integrated against the Beryl seize path. - [PolicyRegistry: Composite Policies (UNION / INTERSECT)](https://docs.base.org/base-chain/specs/reference/b20/changelog/02-cobalt-policyregistry-composite-policy): Cobalt adds UNION and INTERSECT composite policies to PolicyRegistry so B20 integrations can combine simple authorization policies without flattening their member lists. diff --git a/docs/specifications/b20/changelog.mdx b/docs/specifications/b20/changelog.mdx index 8f97516ba..50dee4686 100644 --- a/docs/specifications/b20/changelog.mdx +++ b/docs/specifications/b20/changelog.mdx @@ -3,10 +3,10 @@ title: "Changelog" description: "Per-hardfork, per-feature migration notes for the B20 token standard, newest first, including new methods, deprecations, and activation dates." --- -Per-hardfork, per-feature migration notes for the [B20 token standard](/specifications/b20). Each Cobalt entry is a focused, code-forward changelog for one scoped feature change that crosses a hardfork boundary. Newest first. +Per-hardfork, per-feature migration notes for the [B20 token standard](/specifications/b20). Each Cobalt and Denim entry is a focused, code-forward changelog for one scoped feature change that crosses a hardfork boundary. Newest first. -B20 method and event signatures are part of the chain's consensus surface. Existing selectors and behavior do not change; the standard grows by addition. +B20 method and event signatures are part of the chain's consensus surface. Existing selectors do not change, and new functionality ships by addition. Behavior can change at a hardfork boundary: two Denim entries below are breaking for some integrations. ## Hardfork Ordinals @@ -15,8 +15,17 @@ B20 method and event signatures are part of the chain's consensus surface. Exist |---------|----------|--------| | `01` | Beryl | Live | | `02` | Cobalt | Upcoming | +| `03` | Denim | Upcoming | -## [Cobalt](/upgrades/cobalt/overview) (Upcoming) — Ordinal 02 +## [Denim](/upgrades/denim/overview) (Upcoming), Ordinal 03 + +| Product(s) | Change | Affected interfaces | Entry | +|------------|--------|---------------------|-------| +| B20 Asset, B20 Stablecoin | (Breaking) Reject the token itself as a credit recipient | `IB20` (shared surface), `IB20Asset` (`batchMint`) | [Token receiver](/base-chain/specs/reference/b20/changelog/03-denim-b20-token-receiver) | +| B20 Asset, B20 Stablecoin | (Breaking) Transfer executor policy on every transfer path | `IB20` (shared surface) | [Transfer executor enforcement](/base-chain/specs/reference/b20/changelog/03-denim-b20-transfer-executor-enforcement) | +| PolicyRegistry | NOT / invert policies | `IPolicyRegistry` | [NOT policies](/base-chain/specs/reference/b20/changelog/03-denim-policyregistry-not-policy) | + +## [Cobalt](/upgrades/cobalt/overview) (Upcoming), Ordinal 02 | Product(s) | Change | Affected interfaces | Entry | |------------|--------|---------------------|-------| @@ -24,7 +33,7 @@ B20 method and event signatures are part of the chain's consensus surface. Exist | B20 Asset, B20 Stablecoin | Seize surface + `burnBlocked` deprecation | `IB20` (shared surface) | [Seize surface](/base-chain/specs/reference/b20/changelog/02-cobalt-b20-seize) | | PolicyRegistry | Composite Policies (UNION / INTERSECT) | `IPolicyRegistry` | [Composite policies](/base-chain/specs/reference/b20/changelog/02-cobalt-policyregistry-composite-policy) | -## [Beryl](/upgrades/beryl/overview) — Initial Release +## [Beryl](/upgrades/beryl/overview): Initial Release | Network | Activated | |---|---| diff --git a/docs/upgrades/denim/overview.mdx b/docs/upgrades/denim/overview.mdx index d3ec15a0b..275b0dde8 100644 --- a/docs/upgrades/denim/overview.mdx +++ b/docs/upgrades/denim/overview.mdx @@ -1,6 +1,6 @@ --- title: "Overview" -description: "Denim introduces native blocks at a 200ms cadence, onchain millisecond time through BaseTime, and millisecond-resolution RPC timestamps." +description: "Denim introduces native blocks at a 200ms cadence, onchain millisecond time through BaseTime, millisecond-resolution RPC timestamps, and B20 transfer and policy improvements." --- | | Status | Date | @@ -33,5 +33,13 @@ Denim adds onchain millisecond time through the BaseTime predeploy. A BaseTime m Block, header, transaction, log, and receipt responses gain optional millisecond-resolution timestamp fields (`timestampMs`, `blockTimestampMs`), derived from authenticated BaseTime metadata. The existing seconds-based `timestamp` field is unchanged for backward compatibility. + + + + +**Base** · Precompile + +Denim tightens B20 transfer rules and extends PolicyRegistry: transfers, mints, and seizes to the token's own address revert, `TRANSFER_EXECUTOR_POLICY` applies to every transfer path, and any policy can be inverted with a NOT flag. See [Token Receiver](/base-chain/specs/reference/b20/changelog/03-denim-b20-token-receiver), [Transfer Executor Enforcement](/base-chain/specs/reference/b20/changelog/03-denim-b20-transfer-executor-enforcement), and [NOT / Invert Policies](/base-chain/specs/reference/b20/changelog/03-denim-policyregistry-not-policy). +