From f67bebf002982c565d34d5917d5eca8134f7c21b Mon Sep 17 00:00:00 2001 From: Rayyan Alam Date: Thu, 20 Aug 2026 14:37:31 -0400 Subject: [PATCH 1/5] docs(changelog): clarify seize integrator guidance Reframe the seize changelog's integrator implications around concrete pool-level exposure and preserve the burnBlocked context so readers can distinguish the new surface from the pre-existing risk. Co-authored-by: Cursor --- changelog/02_Cobalt_B20_seize.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/changelog/02_Cobalt_B20_seize.md b/changelog/02_Cobalt_B20_seize.md index a32350a..3c22dda 100644 --- a/changelog/02_Cobalt_B20_seize.md +++ b/changelog/02_Cobalt_B20_seize.md @@ -142,6 +142,16 @@ A shared seize-policy approach was rejected: burning and seizing have different The name `transferFromBlockedWithMemo` was considered and rejected. `seizeWithMemo` names the intent (seizure) rather than the mechanism (blocked transfer). +## Implications for Integrators + +Any contract that pools deposits from multiple end users and holds this token as a single on-chain balance can lose that entire pooled balance in one admin call. This includes lending-protocol vaults, AMM pools, staking contracts, custodial wallets, and bridges. The mechanism acts at the pooling contract's address, not at individual depositor-share granularity, so uninvolved depositors are affected alongside any targeted one. + +This is not a new risk. `burnBlocked` (still dialable, deprecated) already lets an issuer zero a blocked address's balance through block, burn, and reissue elsewhere. `seizeWithMemo` does not expand who is exposed. The change is operational: `seizeWithMemo` collapses that workaround into one call, redirects the balance instead of burning and reissuing it, and emits a dedicated `Seized` event. Both paths remain live. If an integrator contract is blocked under `TRANSFER_SENDER_POLICY` or not authorized under `SEIZE_HOLDER_POLICY`, an admin can move out its entire balance. + +To check current exposure, call `token.policyId(SEIZE_HOLDER_POLICY())` and `token.policyId(TRANSFER_SENDER_POLICY())` (`IB20.policyId`, `src/interfaces/IB20.sol:590`) to get the governing policy IDs for those slots. Then query the Policy Registry's `isAuthorized(policyId, account)` with the pooling contract's own address against each ID. If the contract is not authorized under the seize-holder policy, or is blocked under the transfer-sender policy, its entire balance is exposed at that time. This check is only point-in-time. An issuer can later change either slot with `updatePolicy`, so "not seizable today" is not a durable guarantee. For a stronger structural assessment, inspect the policy implementations currently assigned to those slots. A fixed allowlist is more stable than a policy the issuer can freely repoint. + +For a leading indicator, subscribe to `PolicyUpdated(policyScope, oldPolicyId, newPolicyId)` (`IB20.sol:179`) for the `SEIZE_HOLDER_POLICY` and `TRANSFER_SENDER_POLICY` scopes. This event fires when the issuer changes either governing policy, before any seizure or blocked-burn occurs. `Seized` and `BurnedBlocked` are lagging signals because they fire only after funds have already moved. Before onboarding a B20 asset or stablecoin as a deposit or collateral asset, run the exposure check against the pooling contract's own address and keep the `PolicyUpdated` subscription running for the life of the integration. If a seizure or blocked-burn is detected, snapshot the pool's per-depositor share accounting immediately. The loss happens atomically in the seize or burn call, and the on-chain events do not identify which depositors should absorb it. + ## Migration Steps **Backwards-compatible:** `burnBlocked` continues to work unchanged. No action is required if you do not need seize behavior yet. From 2454c0ec9beb66467515900e356ef4eecd7659b0 Mon Sep 17 00:00:00 2001 From: Rayyan Alam Date: Thu, 20 Aug 2026 14:56:39 -0400 Subject: [PATCH 2/5] docs(changelog): soften seize integrator wording Address the PR feedback by reframing the integrator impact in terms of funds moving outside the regular transfer flow and by simplifying the policy-ID exposure check as a point-in-time signal rather than a guarantee. Co-authored-by: Cursor --- changelog/02_Cobalt_B20_seize.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/changelog/02_Cobalt_B20_seize.md b/changelog/02_Cobalt_B20_seize.md index 3c22dda..aaf4530 100644 --- a/changelog/02_Cobalt_B20_seize.md +++ b/changelog/02_Cobalt_B20_seize.md @@ -144,11 +144,11 @@ The name `transferFromBlockedWithMemo` was considered and rejected. `seizeWithMe ## Implications for Integrators -Any contract that pools deposits from multiple end users and holds this token as a single on-chain balance can lose that entire pooled balance in one admin call. This includes lending-protocol vaults, AMM pools, staking contracts, custodial wallets, and bridges. The mechanism acts at the pooling contract's address, not at individual depositor-share granularity, so uninvolved depositors are affected alongside any targeted one. +For pooled-balance integrators, `seizeWithMemo` and `burnBlocked` create a path for funds to move without the regular transfer flow. This matters for systems that keep internal vault accounting against one on-chain token balance, such as lending-protocol vaults, AMM pools, staking contracts, custodial wallets, and bridges. The mechanism acts at the pooling contract's address, not at individual depositor-share granularity, so the accounting impact falls on the pool as a whole. -This is not a new risk. `burnBlocked` (still dialable, deprecated) already lets an issuer zero a blocked address's balance through block, burn, and reissue elsewhere. `seizeWithMemo` does not expand who is exposed. The change is operational: `seizeWithMemo` collapses that workaround into one call, redirects the balance instead of burning and reissuing it, and emits a dedicated `Seized` event. Both paths remain live. If an integrator contract is blocked under `TRANSFER_SENDER_POLICY` or not authorized under `SEIZE_HOLDER_POLICY`, an admin can move out its entire balance. +This is not a new risk. `burnBlocked` (still dialable, deprecated) already lets an issuer zero a blocked address's balance through block, burn, and reissue elsewhere. `seizeWithMemo` does not expand who is exposed. The change is operational: `seizeWithMemo` collapses that workaround into one call, redirects the balance instead of burning and reissuing it, and emits a dedicated `Seized` event. Both paths remain live. If an integrator contract is blocked under `TRANSFER_SENDER_POLICY` or not authorized under `SEIZE_HOLDER_POLICY`, funds can move out of that contract without the regular transfer flow. -To check current exposure, call `token.policyId(SEIZE_HOLDER_POLICY())` and `token.policyId(TRANSFER_SENDER_POLICY())` (`IB20.policyId`, `src/interfaces/IB20.sol:590`) to get the governing policy IDs for those slots. Then query the Policy Registry's `isAuthorized(policyId, account)` with the pooling contract's own address against each ID. If the contract is not authorized under the seize-holder policy, or is blocked under the transfer-sender policy, its entire balance is exposed at that time. This check is only point-in-time. An issuer can later change either slot with `updatePolicy`, so "not seizable today" is not a durable guarantee. For a stronger structural assessment, inspect the policy implementations currently assigned to those slots. A fixed allowlist is more stable than a policy the issuer can freely repoint. +Issuers can check current exposure by reading the policy IDs assigned to `SEIZE_HOLDER_POLICY` and `TRANSFER_SENDER_POLICY` with `token.policyId(...)` (`IB20.policyId`, `src/interfaces/IB20.sol:590`). Then they can query the Policy Registry's `isAuthorized(policyId, account)` with the pooling contract's own address against each ID. If the contract is not authorized under the seize-holder policy, or is blocked under the transfer-sender policy, funds can be seized or burned from that vault balance under the current configuration. This check is only point-in-time. An issuer can later change either slot with `updatePolicy`, so "not seizable today" is not a strong guarantee. For a stronger structural assessment, inspect the policy implementations currently assigned to those slots. A fixed allowlist is more stable than a policy the issuer can freely repoint. For a leading indicator, subscribe to `PolicyUpdated(policyScope, oldPolicyId, newPolicyId)` (`IB20.sol:179`) for the `SEIZE_HOLDER_POLICY` and `TRANSFER_SENDER_POLICY` scopes. This event fires when the issuer changes either governing policy, before any seizure or blocked-burn occurs. `Seized` and `BurnedBlocked` are lagging signals because they fire only after funds have already moved. Before onboarding a B20 asset or stablecoin as a deposit or collateral asset, run the exposure check against the pooling contract's own address and keep the `PolicyUpdated` subscription running for the life of the integration. If a seizure or blocked-burn is detected, snapshot the pool's per-depositor share accounting immediately. The loss happens atomically in the seize or burn call, and the on-chain events do not identify which depositors should absorb it. From af7f0adae203812093d3d615f9cd1b0663b10770 Mon Sep 17 00:00:00 2001 From: Rayyan Alam Date: Thu, 20 Aug 2026 15:00:21 -0400 Subject: [PATCH 3/5] docs(changelog): trim seize integrator guidance Remove the monitoring and incident-response paragraph from the integrator section so the entry stays focused on the core exposure explanation and immediate policy checks. Co-authored-by: Cursor --- changelog/02_Cobalt_B20_seize.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/changelog/02_Cobalt_B20_seize.md b/changelog/02_Cobalt_B20_seize.md index aaf4530..38b3875 100644 --- a/changelog/02_Cobalt_B20_seize.md +++ b/changelog/02_Cobalt_B20_seize.md @@ -150,8 +150,6 @@ This is not a new risk. `burnBlocked` (still dialable, deprecated) already lets Issuers can check current exposure by reading the policy IDs assigned to `SEIZE_HOLDER_POLICY` and `TRANSFER_SENDER_POLICY` with `token.policyId(...)` (`IB20.policyId`, `src/interfaces/IB20.sol:590`). Then they can query the Policy Registry's `isAuthorized(policyId, account)` with the pooling contract's own address against each ID. If the contract is not authorized under the seize-holder policy, or is blocked under the transfer-sender policy, funds can be seized or burned from that vault balance under the current configuration. This check is only point-in-time. An issuer can later change either slot with `updatePolicy`, so "not seizable today" is not a strong guarantee. For a stronger structural assessment, inspect the policy implementations currently assigned to those slots. A fixed allowlist is more stable than a policy the issuer can freely repoint. -For a leading indicator, subscribe to `PolicyUpdated(policyScope, oldPolicyId, newPolicyId)` (`IB20.sol:179`) for the `SEIZE_HOLDER_POLICY` and `TRANSFER_SENDER_POLICY` scopes. This event fires when the issuer changes either governing policy, before any seizure or blocked-burn occurs. `Seized` and `BurnedBlocked` are lagging signals because they fire only after funds have already moved. Before onboarding a B20 asset or stablecoin as a deposit or collateral asset, run the exposure check against the pooling contract's own address and keep the `PolicyUpdated` subscription running for the life of the integration. If a seizure or blocked-burn is detected, snapshot the pool's per-depositor share accounting immediately. The loss happens atomically in the seize or burn call, and the on-chain events do not identify which depositors should absorb it. - ## Migration Steps **Backwards-compatible:** `burnBlocked` continues to work unchanged. No action is required if you do not need seize behavior yet. From 745770c6f8147c4d02a71d7fe0585d81f858153e Mon Sep 17 00:00:00 2001 From: Rayyan Alam Date: Thu, 20 Aug 2026 15:00:43 -0400 Subject: [PATCH 4/5] docs(changelog): simplify seize exposure guidance Drop the structural-assessment tangent so the integrator section stays focused on the direct policy check and its limits. Co-authored-by: Cursor --- changelog/02_Cobalt_B20_seize.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelog/02_Cobalt_B20_seize.md b/changelog/02_Cobalt_B20_seize.md index 38b3875..db2fd86 100644 --- a/changelog/02_Cobalt_B20_seize.md +++ b/changelog/02_Cobalt_B20_seize.md @@ -148,7 +148,7 @@ For pooled-balance integrators, `seizeWithMemo` and `burnBlocked` create a path This is not a new risk. `burnBlocked` (still dialable, deprecated) already lets an issuer zero a blocked address's balance through block, burn, and reissue elsewhere. `seizeWithMemo` does not expand who is exposed. The change is operational: `seizeWithMemo` collapses that workaround into one call, redirects the balance instead of burning and reissuing it, and emits a dedicated `Seized` event. Both paths remain live. If an integrator contract is blocked under `TRANSFER_SENDER_POLICY` or not authorized under `SEIZE_HOLDER_POLICY`, funds can move out of that contract without the regular transfer flow. -Issuers can check current exposure by reading the policy IDs assigned to `SEIZE_HOLDER_POLICY` and `TRANSFER_SENDER_POLICY` with `token.policyId(...)` (`IB20.policyId`, `src/interfaces/IB20.sol:590`). Then they can query the Policy Registry's `isAuthorized(policyId, account)` with the pooling contract's own address against each ID. If the contract is not authorized under the seize-holder policy, or is blocked under the transfer-sender policy, funds can be seized or burned from that vault balance under the current configuration. This check is only point-in-time. An issuer can later change either slot with `updatePolicy`, so "not seizable today" is not a strong guarantee. For a stronger structural assessment, inspect the policy implementations currently assigned to those slots. A fixed allowlist is more stable than a policy the issuer can freely repoint. +Issuers can check current exposure by reading the policy IDs assigned to `SEIZE_HOLDER_POLICY` and `TRANSFER_SENDER_POLICY` with `token.policyId(...)` (`IB20.policyId`, `src/interfaces/IB20.sol:590`). Then they can query the Policy Registry's `isAuthorized(policyId, account)` with the pooling contract's own address against each ID. If the contract is not authorized under the seize-holder policy, or is blocked under the transfer-sender policy, funds can be seized or burned from that vault balance under the current configuration. This check is only point-in-time. An issuer can later change either slot with `updatePolicy`, so "not seizable today" is not a strong guarantee. ## Migration Steps From e49adc264fb1829e54b724bcfb3d223d5b219265 Mon Sep 17 00:00:00 2001 From: Rayyan Alam Date: Thu, 20 Aug 2026 16:37:02 -0400 Subject: [PATCH 5/5] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- changelog/02_Cobalt_B20_seize.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelog/02_Cobalt_B20_seize.md b/changelog/02_Cobalt_B20_seize.md index db2fd86..2d63776 100644 --- a/changelog/02_Cobalt_B20_seize.md +++ b/changelog/02_Cobalt_B20_seize.md @@ -148,7 +148,7 @@ For pooled-balance integrators, `seizeWithMemo` and `burnBlocked` create a path This is not a new risk. `burnBlocked` (still dialable, deprecated) already lets an issuer zero a blocked address's balance through block, burn, and reissue elsewhere. `seizeWithMemo` does not expand who is exposed. The change is operational: `seizeWithMemo` collapses that workaround into one call, redirects the balance instead of burning and reissuing it, and emits a dedicated `Seized` event. Both paths remain live. If an integrator contract is blocked under `TRANSFER_SENDER_POLICY` or not authorized under `SEIZE_HOLDER_POLICY`, funds can move out of that contract without the regular transfer flow. -Issuers can check current exposure by reading the policy IDs assigned to `SEIZE_HOLDER_POLICY` and `TRANSFER_SENDER_POLICY` with `token.policyId(...)` (`IB20.policyId`, `src/interfaces/IB20.sol:590`). Then they can query the Policy Registry's `isAuthorized(policyId, account)` with the pooling contract's own address against each ID. If the contract is not authorized under the seize-holder policy, or is blocked under the transfer-sender policy, funds can be seized or burned from that vault balance under the current configuration. This check is only point-in-time. An issuer can later change either slot with `updatePolicy`, so "not seizable today" is not a strong guarantee. +Issuers can check current exposure by reading the policy IDs assigned to `SEIZE_HOLDER_POLICY` and `TRANSFER_SENDER_POLICY` with `token.policyId(...)` (`IB20.policyId`, `src/interfaces/IB20.sol`). Then they can query the Policy Registry's `isAuthorized(policyId, account)` with the pooling contract's own address against each ID. If the contract is not authorized under the seize-holder policy, or is blocked under the transfer-sender policy, funds can be seized or burned from that vault balance under the current configuration. This check is only point-in-time. An issuer can later change either slot with `updatePolicy`, so "not seizable today" is not a strong guarantee. ## Migration Steps