Docs/add b20 spec - #1766
Conversation
|
Preview deployment for your docs. Learn more about Mintlify Previews.
💡 Tip: Enable Workflows to automatically generate PRs for you. |
🟡 Heimdall Review Status
|
| | `TRANSFER_RECEIVER_POLICY` | `to` | `transfer`, `transferFrom`, and memo variants | | ||
| | `TRANSFER_EXECUTOR_POLICY` | `msg.sender` | `transferFrom` only when `msg.sender != from` | | ||
| | `MINT_RECEIVER_POLICY` | `to` | `mint` and `mintWithMemo` | | ||
| | `SEIZE_HOLDER_POLICY` | `from` | `seizeWithMemo`; the holder is seizable only when not authorized by this policy | |
There was a problem hiding this comment.
Can you double check the policies are all listed? Looks like we need to add SEIZE_RECEIVER_POLICY as well.
There was a problem hiding this comment.
Reviewed against base/base-std as the source of truth. Overall the generated reference is mostly faithful: precompile addresses, roles, variant bytes, decimals, PolicyType values, ~38 IB20 function pages, all IB20Asset pages, and every non-enum selector check out.
A few things worth fixing before merge (inline):
- Systematic selector/topic bug: enum params are hashed by type name instead of
uint8, so every enum-bearing selector/topic is wrong (createB20,getB20Address,B20Created,UnsupportedVersion,createPolicy/createPolicyWithAccounts/createCompositePolicy). Mechanical to regenerate, but breaks integrators. SEIZE_RECEIVER_POLICYmissing: the standard has 6 policy scopes; the docs show 5. This also makesseizeWithMemowrong (it gates the recipient).seizeWithMemodocuments a stale signature (returns (bool)) and stale policy semantics.- Smaller fixes:
finalizeUpdateAdminaccess-control,ChildPoliciesOutsideOfRangesignature,burnBlocked"no longer part of this interface" (still declared, just deprecated), and missing docs forMIN/MAX_COMPOSITE_CHILD_POLICIESand theUIMultiplierUpdatedevent.
There's no redirect for the exact old path /base-chain/specs/upgrades/beryl/b20, which will 404 post-merge.
|
|
||
| | Field | Value | | ||
| |---|---| | ||
| | Selector | `0xb263cb84` | |
There was a problem hiding this comment.
Selector is wrong: the enum type name was hashed instead of uint8. ABI selectors normalize enums to uint8, so this should be computed from createB20(uint8,bytes32,bytes,bytes[]) → 0x62975e6a (verified with cast), not createB20(B20Variant,...).
Also wrong: getB20Address (0x8c30260f), the B20Created topic0 (0xfd9bf2730513a1709722ff379a0844dfd8f997d600693c2bcc659e188bbdba0d), UnsupportedVersion (0xc0d8b4e0), and the IPolicyRegistry create* functions. The Canonical signature fields should also show uint8. These would break integrators computing calldata / log filters.
|
|
||
| | Field | Value | | ||
| |---|---| | ||
| | Selector | `0xe494a1f6` | |
There was a problem hiding this comment.
Same enum-selector bug as createB20: this is computed from createPolicy(address,PolicyType). The correct ABI signature is createPolicy(address,uint8) → 0xca5d55f6 (verified with cast). Related: createPolicyWithAccounts → 0xa2d3044f, createCompositePolicy → 0x6fdd1491. Please regenerate all enum-bearing selectors with uint8 normalization.
| ## Signature | ||
|
|
||
| ```solidity | ||
| function seizeWithMemo(address from, address to, uint256 amount, bytes32 memo) external returns (bool); |
There was a problem hiding this comment.
Two issues vs base-std IB20.sol:474:
- Return type: source is
... external;with no return value; thereturns (bool)here (and "always true on success" in the description) is incorrect.
function seizeWithMemo(address from, address to, uint256 amount, bytes32 memo) external;
- Policy semantics (stale): the page says
tois not policy-checked, but source now gates the recipient underSEIZE_RECEIVER_POLICYand revertsPolicyForbids(SEIZE_RECEIVER_POLICY, …)whentoisn't authorized (IB20.sol:460–467).
| | `TRANSFER_RECEIVER_POLICY` | `to` | `transfer`, `transferFrom`, and memo variants | | ||
| | `TRANSFER_EXECUTOR_POLICY` | `msg.sender` | `transferFrom` only when `msg.sender != from` | | ||
| | `MINT_RECEIVER_POLICY` | `to` | `mint` and `mintWithMemo` | | ||
| | `SEIZE_HOLDER_POLICY` | `from` | `seizeWithMemo`; the holder is seizable only when not authorized by this policy | |
There was a problem hiding this comment.
Missing the 6th policy scope. B20Constants.sol and IB20.sol:274 define SEIZE_RECEIVER_POLICY (gates the seize to recipient), so there are 6 scopes, not 5. Add a row here and update the bytes32[5] array below (~line 75) to include it — as written, the sample audit tool silently skips a real scope. The same omission appears in reference/constants-and-addresses.mdx, index.mdx, token-lifecycle.mdx, and implementation/policy-configuration-in-code.mdx, and there is no dedicated constant page for it.
|
|
||
| ## Access control | ||
|
|
||
| Callable by the policy admin for the target policy. |
There was a problem hiding this comment.
Access control is inverted. Per IPolicyRegistry.sol:161–168, finalizeUpdateAdmin must be called by the staged pending admin (reverts Unauthorized otherwise) — it "promotes the caller to active admin." The current policy admin cannot call it; that's the point of the two-step transfer.
| The child-policy set is capped at 4. | ||
| Dev: Reverts with `IncompatiblePolicyType` when `policyType` is not UNION or INTERSECT. | ||
| Dev: Reverts with `ZeroAddress` when `admin` is `address(0)`. | ||
| Dev: Reverts with `ChildPoliciesOutsideOfRange(2, 4)` when `childPolicyIds.length` is not in `[2, 4]`. |
There was a problem hiding this comment.
ChildPoliciesOutsideOfRange is parameterless in source (IPolicyRegistry.sol:62: error ChildPoliciesOutsideOfRange();) — writing it as (2, 4) misrepresents the signature. The bound is the named range [MIN_COMPOSITE_CHILD_POLICIES, MAX_COMPOSITE_CHILD_POLICIES]; the literal 2 is not defined in the interface. Same fix needed on updateComposite.
| | [`DEFAULT_ADMIN_ROLE`](/base-chain/specs/upgrades/beryl/b20/specification/reference/interfaces/IB20/DEFAULT_ADMIN_ROLE) | `0xa217fddf` | The default top-level admin role (`bytes32(0)`). Required to call `grantRole`, `revokeRole`, | | ||
| | [`MINT_ROLE`](/base-chain/specs/upgrades/beryl/b20/specification/reference/interfaces/IB20/MINT_ROLE) | `0xe9a9c850` | Required to call `mint` and `mintWithMemo`. | | ||
| | [`BURN_ROLE`](/base-chain/specs/upgrades/beryl/b20/specification/reference/interfaces/IB20/BURN_ROLE) | `0xb930908f` | Required to call `burn` and `burnWithMemo`. | | ||
| | [`BURN_BLOCKED_ROLE`](/base-chain/specs/upgrades/beryl/b20/specification/reference/interfaces/IB20/BURN_BLOCKED_ROLE) | `0x32ad9be8` | Required to call the deprecated `burnBlocked` (no longer part of this interface; retained for | |
There was a problem hiding this comment.
burnBlocked is still declared in IB20.sol:453 (marked DEPRECATED), so "no longer part of this interface" is inaccurate. Two related gaps: burnBlocked(address,uint256) has no reference page (the only non-constant IB20 function without one), and SEIZE_RECEIVER_POLICY() (IB20.sol:274) is missing from this function table.
| --- | ||
|
|
||
|
|
||
| ## Functions |
There was a problem hiding this comment.
Two functions declared in IPolicyRegistry.sol are missing from this index (and have no pages): MIN_COMPOSITE_CHILD_POLICIES() (:242) and MAX_COMPOSITE_CHILD_POLICIES() (:247). 16 functions in source, 14 documented — and these two are exactly what the composite-range prose references.
| | error | `LengthMismatch` | `0xab8b67c6` | `IB20Asset` | A batched function was called with parallel arrays of differing lengths. | | ||
| | event | `Memo` | `0x6989f5818dcfd11f8cd53b27c94cec33dae1589735f03e639cba54553a1825e8` | `IB20` | Emitted by `transferWithMemo`, `transferFromWithMemo`, `mintWithMemo`, and `burnWithMemo` | | ||
| | error | `MissingRequiredField` | `0x4a43ae87` | `IB20Factory` | A required string argument was the empty string. | | ||
| | event | `MultiplierUpdateCancelled` | `0xf8929975f3e67bbd1e5ec70d4cceaa7cce7ea0e811720f29c96cf1724de09397` | `IB20Asset` | A scheduled multiplier update was cancelled. Emitted by `cancelScheduledMultiplier`, | |
There was a problem hiding this comment.
UIMultiplierUpdated(uint256 oldMultiplier, uint256 newMultiplier, uint256 effectiveAtTimestamp) is missing from this index. It's declared in IScaledUIAmount.sol (inherited by IB20Asset) and is the primary event emitted on multiplier changes — MultiplierUpdateCancelled is listed, but the update event itself is not.
roethke
left a comment
There was a problem hiding this comment.
Review — verified against current base/base-std (main)
Prose/concepts are strong and the asset-agnostic framing is good (no ticker / 1:1-backing / Reg-S leakage). Note this PR is a superset of #1774 (implementation) and #1775 (reference) — worth deciding whether to land the umbrella here or the two splits, to avoid conflicts.
🔴 Blocker — same UIMultiplier* rename as #1775
This PR ships the same reference files, so errors-and-events-index.mdx and IB20Asset/* use pre-rename names: setUIMultiplier→updateUIMultiplier, cancelScheduledMultiplier→cancelUIMultiplierUpdate, ScheduleOverlap→UIMultiplierUpdateExists, NoScheduledMultiplier→UIMultiplierUpdateDoesNotExist, MultiplierUpdateCancelled→UIMultiplierUpdateCancelled, and they omit the deprecated MultiplierUpdated(uint256) event. Selectors/topic0s are wrong as a result. Full detail in the #1775 review.
🟡 Should-fix — SEIZE_RECEIVER_POLICY missing from scope summaries
It exists in current IB20 and is enforced against to in seizeWithMemo, but is omitted from:
specification/index.mdx— Policy Integration table (lists only 5 scopes)concepts/policies-and-scopes.mdx— scope table and thebytes32[5]audit loop (should be[6])reference/constants-and-addresses.mdx— policy-scopes table (addkeccak256("SEIZE_RECEIVER_POLICY"))
The per-function pages include it; only these summaries omit it.
🟡 Should-fix — apps/guides/accept-b20-payments.mdx
The flow transfers raw units (parseUnits('10', decimals)) using an Asset-variant token address. For an Asset with a non-unit multiplier(), raw units ≠ the UI/displayed amount (scaledBalanceOf / toUIAmount), so "10 tokens" as the payer sees them won't equal the raw amount sent. Either note that payments are normally the Stablecoin variant (fixed 6 decimals, no multiplier), or add a raw-vs-UI conversion note. (Memo ABI, PolicyForbids/paused-transfer handling, and simulateContract advice are all correct.)
Nits
concepts/variants-asset-vs-stablecoin.mdx: noteupdateMultiplieris deprecated and name the scheduled path (updateUIMultiplier).b20-playground.mdx: "a 2-for-1 split… every balance doubles" — raw balances don't change; the multiplier doubles the scaled/displayed balance. Also "CUSIP" is securities-specific in an otherwise asset-agnostic file (prefer "issuer-defined identifiers").
Clean: overview.mdx, concepts/architecture-and-precompiles.mdx, concepts/roles-and-access-control.mdx, concepts/token-lifecycle.mdx.
What changed? Why?