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
125 changes: 78 additions & 47 deletions src/interfaces/IB20.sol
Original file line number Diff line number Diff line change
Expand Up @@ -469,15 +469,18 @@ interface IB20 {
/// @notice Allowance granted by `owner` to `spender`.
function allowance(address owner, address spender) external view returns (uint256);

/// @notice Transfers `amount` from `msg.sender` to `to`. Reverts with:
/// - `ContractPaused(TRANSFER)` if `TRANSFER` is paused.
/// - `PolicyForbids(TRANSFER_SENDER_POLICY, policyId)` if `msg.sender`
/// is not authorized under the active `TRANSFER_SENDER_POLICY` policy.
/// - `PolicyForbids(TRANSFER_RECEIVER_POLICY, policyId)` if `to` is not
/// authorized under the active `TRANSFER_RECEIVER_POLICY` policy.
/// - `InsufficientBalance(msg.sender, balance, amount)` if the
/// caller does not have enough balance.
/// - `InvalidReceiver(to)` if `to == address(0)`.
/// @notice Transfers `amount` from `msg.sender` to `to`. Preconditions
/// evaluate in the order listed; when multiple are violated,
/// the earliest-listed revert is the one that surfaces:
/// 1. `ContractPaused(TRANSFER)` if `TRANSFER` is paused.
/// 2. `InvalidReceiver(to)` if `to == address(0)`.
/// 3. `InvalidSender(msg.sender)` if `msg.sender == address(0)`.
/// 4. `PolicyForbids(TRANSFER_SENDER_POLICY, policyId)` if `msg.sender`
/// is not authorized under the active `TRANSFER_SENDER_POLICY`.
/// 5. `PolicyForbids(TRANSFER_RECEIVER_POLICY, policyId)` if `to`
/// is not authorized under the active `TRANSFER_RECEIVER_POLICY`.
/// 6. `InsufficientBalance(msg.sender, balance, amount)` if the
/// caller does not have enough balance.
/// @dev Does NOT consult the `TRANSFER_EXECUTOR_POLICY` policy: on direct
/// `transfer` the executor IS the sender, and the sender
/// check already covers that address. When the token is
Expand All @@ -486,13 +489,23 @@ interface IB20 {
function transfer(address to, uint256 amount) external returns (bool);

/// @notice Transfers `amount` from `from` to `to` using `msg.sender`'s
/// allowance. Reverts as `transfer` does, plus:
/// - `InsufficientAllowance(msg.sender, allowance, amount)`
/// if the caller does not have enough allowance from `from`.
/// - `InvalidSender(from)` if `from == address(0)`.
/// - `PolicyForbids(TRANSFER_EXECUTOR_POLICY, policyId)` if
/// `msg.sender != from` and `msg.sender` is not authorized
/// under the active `TRANSFER_EXECUTOR_POLICY` policy.
/// allowance. Preconditions evaluate in the order listed;
/// when multiple are violated, the earliest-listed revert is
/// the one that surfaces:
/// 1. `ContractPaused(TRANSFER)` if `TRANSFER` is paused.
/// 2. `InvalidReceiver(to)` if `to == address(0)`.
/// 3. `InvalidSender(from)` if `from == address(0)`.
/// 4. `InsufficientAllowance(msg.sender, allowance, amount)`
/// if the caller does not have enough allowance from `from`.
/// 5. `PolicyForbids(TRANSFER_EXECUTOR_POLICY, policyId)` if
/// `msg.sender != from` and `msg.sender` is not authorized
/// under the active `TRANSFER_EXECUTOR_POLICY`.
/// 6. `PolicyForbids(TRANSFER_SENDER_POLICY, policyId)` if `from`
/// is not authorized under the active `TRANSFER_SENDER_POLICY`.
/// 7. `PolicyForbids(TRANSFER_RECEIVER_POLICY, policyId)` if `to`
/// is not authorized under the active `TRANSFER_RECEIVER_POLICY`.
/// 8. `InsufficientBalance(from, balance, amount)` if `from`
/// does not have enough balance.
/// @dev The sender-side check is performed against `from` (the
/// party whose balance moves), the receiver check against
/// `to`, and the executor check against `msg.sender` only
Expand All @@ -502,11 +515,12 @@ interface IB20 {

/// @notice Sets `spender`'s allowance to `amount`. NOT gated by any
/// policy or by pause; only the act of MOVING balance is gated.
/// @dev Reverts with `InvalidApprover(msg.sender)` if the
/// caller is `address(0)` (theoretically unreachable for
/// normal callers but enforced for parity with OZ ERC20),
/// and `InvalidSpender(spender)` if
/// `spender == address(0)`.
/// @dev Reverts in the following canonical order (first violated
/// check wins):
/// 1. `InvalidApprover(msg.sender)` if the caller is
/// `address(0)` (theoretically unreachable for normal
/// callers but enforced for parity with OZ ERC20).
/// 2. `InvalidSpender(spender)` if `spender == address(0)`.
function approve(address spender, uint256 amount) external returns (bool);

/*//////////////////////////////////////////////////////////////
Expand Down Expand Up @@ -558,12 +572,17 @@ interface IB20 {
MINT / BURN
//////////////////////////////////////////////////////////////*/

/// @notice Mints `amount` to `to`. Requires `MINT_ROLE`. Subject to:
/// 1. `totalSupply + amount <= supplyCap` (else
/// `SupplyCapExceeded`).
/// 2. `MINT` is not paused (else `ContractPaused(MINT)`).
/// 3. `to` is authorized under the active `MINT_RECEIVER_POLICY`
/// policy (else `PolicyForbids(MINT_RECEIVER_POLICY, policyId)`).
/// @notice Mints `amount` to `to`. Preconditions evaluate in the
/// order listed; when multiple are violated, the
/// earliest-listed revert is the one that surfaces:
/// 1. `ContractPaused(MINT)` if `MINT` is paused.
/// 2. `AccessControlUnauthorizedAccount(msg.sender, MINT_ROLE)`
/// if the caller does not hold `MINT_ROLE`.
/// 3. `InvalidReceiver(to)` if `to == address(0)`.
/// 4. `PolicyForbids(MINT_RECEIVER_POLICY, policyId)` if `to`
/// is not authorized under the active `MINT_RECEIVER_POLICY`.
/// 5. `SupplyCapExceeded(cap, totalSupply + amount)` if the
/// mint would push `totalSupply` past `supplyCap`.
/// @dev Per-minter rate limiting is NOT enshrined at any level
/// (Default or variant). Minter quotas live in EVM
/// periphery contracts: a controller / wrapper that holds
Expand All @@ -578,12 +597,16 @@ interface IB20 {
/// after the standard `Transfer` event.
function mintWithMemo(address to, uint256 amount, bytes32 memo) external;

/// @notice Burns `amount` from the caller's own balance. Requires
/// `BURN_ROLE`. Subject to `BURN` not being paused (else
/// `ContractPaused(BURN)`). NOT subject to any policy:
/// burn destroys the caller's own supply with no recipient.
/// Reverts with `InsufficientBalance(caller, balance, amount)`
/// if the caller does not have enough balance.
/// @notice Burns `amount` from the caller's own balance. Preconditions
/// evaluate in the order listed; when multiple are violated,
/// the earliest-listed revert is the one that surfaces:
/// 1. `ContractPaused(BURN)` if `BURN` is paused.
/// 2. `AccessControlUnauthorizedAccount(msg.sender, BURN_ROLE)`
/// if the caller does not hold `BURN_ROLE`.
/// 3. `InsufficientBalance(caller, balance, amount)` if the
/// caller does not have enough balance.
/// NOT subject to any policy: burn destroys the caller's own
/// supply with no recipient.
/// @dev To destroy balance held by a third party (compliance
/// seizure from a policy-blocked address), use `burnBlocked`.
/// Emits `Transfer(caller, address(0), amount)`.
Expand All @@ -593,16 +616,18 @@ interface IB20 {
/// after the standard `Transfer` event.
function burnWithMemo(uint256 amount, bytes32 memo) external;

/// @notice Destroys `amount` of `from`'s balance. Requires
/// `BURN_BLOCKED_ROLE`. Subject to:
/// 1. `BURN` is not paused (else `ContractPaused(BURN)`).
/// 2. `from` is NOT authorized under the active
/// `TRANSFER_SENDER_POLICY` policy (else `AccountNotBlocked(from)`).
/// `burnBlocked` exists for seizure of policy-blocked
/// balance; calling it against an authorized address is
/// rejected by design.
/// 3. `amount <= balanceOf(from)` (else
/// `InsufficientBalance(from, balance, amount)`).
/// @notice Destroys `amount` of `from`'s balance. Preconditions
/// evaluate in the order listed; when multiple are violated,
/// the earliest-listed revert is the one that surfaces:
/// 1. `ContractPaused(BURN)` if `BURN` is paused.
/// 2. `AccessControlUnauthorizedAccount(msg.sender, BURN_BLOCKED_ROLE)`
/// if the caller does not hold `BURN_BLOCKED_ROLE`.
/// 3. `AccountNotBlocked(from)` if `from` IS authorized under
/// the active `TRANSFER_SENDER_POLICY` policy. `burnBlocked`
/// exists for seizure of policy-blocked balance; calling
/// it against a non-blocked address is rejected by design.
/// 4. `InsufficientBalance(from, balance, amount)` if
/// `amount > balanceOf(from)`.
/// @dev Designed for sanctions-seizure flows where compliance
/// requires destruction of balance held by a blocked
/// address. Tokens that follow a "freeze, never seize"
Expand Down Expand Up @@ -705,16 +730,22 @@ interface IB20 {
/// @notice Pauses the `features` operations. Additive: features
/// already paused remain paused, and the listed features
/// become paused (duplicates within the call are idempotent).
/// Requires `PAUSE_ROLE`. Reverts with `EmptyFeatureSet` if
/// `features.length == 0`.
/// Reverts in the following canonical order (first violated
/// check wins):
/// 1. `AccessControlUnauthorizedAccount(msg.sender, PAUSE_ROLE)`
/// if the caller does not hold `PAUSE_ROLE`.
/// 2. `EmptyFeatureSet()` if `features.length == 0`.
function pause(PausableFeature[] calldata features) external;

/// @notice Unpauses the `features` operations. Listed features
/// become unpaused; features not listed are unaffected
/// (duplicates are idempotent; unpausing a feature that is
/// not currently paused is a no-op for that feature).
/// Requires `UNPAUSE_ROLE`. Reverts with `EmptyFeatureSet`
/// if `features.length == 0`.
/// Reverts in the following canonical order (first violated
/// check wins):
/// 1. `AccessControlUnauthorizedAccount(msg.sender, UNPAUSE_ROLE)`
/// if the caller does not hold `UNPAUSE_ROLE`.
/// 2. `EmptyFeatureSet()` if `features.length == 0`.
function unpause(PausableFeature[] calldata features) external;

/*//////////////////////////////////////////////////////////////
Expand Down
104 changes: 63 additions & 41 deletions src/interfaces/IB20Security.sol
Original file line number Diff line number Diff line change
Expand Up @@ -374,18 +374,25 @@ interface IB20Security is IB20 {
/// allocations, secondary issuances, etc.) that need to
/// land many recipients in one transaction.
///
/// @dev Requires `MINT_ROLE`. Subject to the `MINT_RECEIVER_POLICY`
/// policy per recipient and to the `MINT` pause vector.
/// Reverts with `LengthMismatch(recipients.length,
/// amounts.length)` if the parallel arrays disagree, and
/// with `EmptyBatch()` if either array is empty.
/// All-or-nothing: if any element reverts (e.g.
/// `SupplyCapExceeded` after a partial accumulation, or
/// `PolicyForbids(MINT_RECEIVER_POLICY, ...)` for a
/// policy-blocked recipient), the entire transaction
/// reverts and no partial state is committed. Emits
/// `Transfer(address(0), recipients[i], amounts[i])` per
/// element. Standard usage is to invoke this through
/// @dev Reverts in the following canonical order (first violated
/// check wins):
/// 1. `ContractPaused(MINT)` if `MINT` is paused.
/// 2. `AccessControlUnauthorizedAccount(msg.sender, MINT_ROLE)`
/// if the caller does not hold `MINT_ROLE`.
/// 3. `LengthMismatch(recipients.length, amounts.length)` if
/// the parallel arrays disagree.
/// 4. `EmptyBatch()` if either array is empty.
/// 5..N. Per-element checks inside the loop: `InvalidReceiver`,
/// `PolicyForbids(MINT_RECEIVER_POLICY, ...)`,
/// `SupplyCapExceeded` — see `mint`'s natspec for the
/// per-element precedence.
/// The pause and role gates are evaluated ONCE for the
/// whole batch; per-element gates fire per recipient inside
/// the loop.
/// All-or-nothing: if any element reverts, the entire
/// transaction reverts and no partial state is committed.
/// Emits `Transfer(address(0), recipients[i], amounts[i])`
/// per element. Standard usage is to invoke this through
/// `announce(...)`'s `internalCalls`, which brackets the
/// issuance with a matching disclosure atomically (see
/// the contract-level "Announcement pairing" notes).
Expand All @@ -410,26 +417,32 @@ interface IB20Security is IB20 {
/// debits in one transaction without first arranging for
/// each account to be policy-blocked.
///
/// @dev Requires `BURN_FROM_ROLE`. NOT gated by any policy:
/// the corporate-actions desk is trusted to pick the right
/// set of accounts off-chain, and the role grant is the
/// on-chain authorization. Subject to the `BURN` pause
/// vector. Reverts with `LengthMismatch(accounts.length,
/// amounts.length)` if the parallel arrays disagree, and
/// with `EmptyBatch()` if either array is empty.
/// All-or-nothing: if any element reverts (e.g.
/// `InsufficientBalance(accounts[k], balance, amounts[k])`),
/// the entire transaction reverts and no partial state is
/// committed. Emits `Transfer(accounts[i], address(0),
/// amounts[i])` per element; does NOT emit `BurnedBlocked`
/// (that event is reserved for `burnBlocked`'s sanctions
/// semantics). Standard usage is to invoke this through
/// `announce(...)`'s `internalCalls`, which brackets the
/// clawback with a matching disclosure atomically (see the
/// contract-level "Announcement pairing" notes). Direct
/// invocation by a role holder remains permitted for
/// emergency override but produces no `Announcement` /
/// `EndAnnouncement` bracket.
/// @dev `BURN_FROM_ROLE` is enforced WITHOUT the factory-bootstrap
/// bypass — clawback against existing balances has no
/// init-time use case, so the role check is unconditional.
/// NOT gated by any policy: the corporate-actions desk is
/// trusted to pick the right set of accounts off-chain, and
/// the role grant is the on-chain authorization. Reverts in
/// the following canonical order (first violated check wins):
/// 1. `ContractPaused(BURN)` if `BURN` is paused.
/// 2. `AccessControlUnauthorizedAccount(msg.sender, BURN_FROM_ROLE)`
/// if the caller does not hold `BURN_FROM_ROLE`.
/// 3. `LengthMismatch(accounts.length, amounts.length)` if
/// the parallel arrays disagree.
/// 4. `EmptyBatch()` if either array is empty.
/// 5. Per-element `InsufficientBalance(accounts[k], balance,
/// amounts[k])` from `_burnRaw`.
/// All-or-nothing: if any element reverts, the entire
/// transaction reverts and no partial state is committed.
/// Emits `Transfer(accounts[i], address(0), amounts[i])`
/// per element; does NOT emit `BurnedBlocked` (that event
/// is reserved for `burnBlocked`'s sanctions semantics).
/// Standard usage is to invoke this through `announce(...)`'s
/// `internalCalls`, which brackets the clawback with a
/// matching disclosure atomically (see the contract-level
/// "Announcement pairing" notes). Direct invocation by a
/// role holder remains permitted for emergency override but
/// produces no `Announcement` / `EndAnnouncement` bracket.
///
/// @param accounts Accounts whose balances will be debited.
/// @param amounts Per-account amounts, parallel to `accounts`.
Expand All @@ -442,15 +455,24 @@ interface IB20Security is IB20 {
/// @notice Burns `amount` tokens from the caller, recording intent
/// to settle off-chain.
///
/// @dev Subject to the `REDEEM_SENDER_POLICY` policy and to the
/// `REDEEM` pause vector. Reverts with
/// `BelowMinimumRedeemable(shares, minimumRedeemable)` if
/// the corresponding share amount (`amount *
/// sharesToTokensRatio / WAD_PRECISION`) is zero OR is
/// strictly less than `minimumRedeemable`. Zero-share
/// redemptions are always rejected, regardless of
/// `minimumRedeemable`'s configured value, so a holder
/// cannot burn token dust that resolves to no shares.
/// @dev Reverts in the following canonical order (first violated
/// check wins):
/// 1. `ContractPaused(REDEEM)` if `REDEEM` is paused.
/// Enforced WITHOUT the factory-bootstrap bypass — redeem
/// is a holder-initiated path with no legitimate init-time
/// use case.
/// 2. `PolicyForbids(REDEEM_SENDER_POLICY, policyId)` if
/// `msg.sender` is not authorized under the active
/// `REDEEM_SENDER_POLICY` policy.
/// 3. `BelowMinimumRedeemable(shares, minimumRedeemable)` if
/// the corresponding share amount (`amount *
/// sharesToTokensRatio / WAD_PRECISION`) is zero OR is
/// strictly less than `minimumRedeemable`. Zero-share
/// redemptions are always rejected, regardless of
/// `minimumRedeemable`'s configured value, so a holder
/// cannot burn token dust that resolves to no shares.
/// 4. `InsufficientBalance(caller, balance, amount)` if the
/// caller does not have enough balance.
/// Emits `Transfer(caller, address(0), amount)` followed by
/// `Redeemed(caller, amount, sharesToTokensRatio)`.
///
Expand Down
Loading
Loading