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
86 changes: 86 additions & 0 deletions test/unit/B20Security/redeem/redeemWithMemo_revertOrder.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import {IB20} from "src/interfaces/IB20.sol";
import {IB20Security} from "src/interfaces/IB20Security.sol";

import {B20Constants} from "src/lib/B20Constants.sol";

import {B20SecurityTest} from "test/lib/B20SecurityTest.sol";
import {PolicyRegistryConstants} from "test/lib/mocks/MockPolicyRegistry.sol";

/// @title Sequential revert-order test for `redeemWithMemo` (security variant).
///
/// @notice **Canonical order (Solidity reference `_redeemBurn`, shared with `redeem`):**
/// 1. PAUSE (`_isPaused(REDEEM)`) → `ContractPaused`
/// 2. POLICY (`isAuthorized(REDEEMSenderPolicyId, msg.sender)`) → `PolicyForbids`
/// 3. BELOW-MIN (`shares == 0 || shares < minimum`) → `BelowMinimumRedeemable`
/// 4. BALANCE (`fromBalance < amount` in `_burnRaw`) → `InsufficientBalance`
///
/// A single test activates all four conditions at once and fixes them from
/// highest to lowest priority, asserting each revert in turn then completing
/// a successful call once all conditions are resolved.
contract B20SecurityRedeemWithMemoRevertOrderTest is B20SecurityTest {
/// @dev Unpauses a single feature via the `unpauser` actor, lazily granting
/// `UNPAUSE_ROLE` on first call. Mirrors the inherited `_pause` helper.
function _unpause(IB20.PausableFeature feature) private {
if (!token.hasRole(B20Constants.UNPAUSE_ROLE, unpauser)) _grantRole(B20Constants.UNPAUSE_ROLE, unpauser);
vm.prank(unpauser);
token.unpause(_singleFeature(feature));
}

function test_redeemWithMemo_revertOrder(uint256 amount, uint256 minimum, bytes32 memo) public {
amount = bound(amount, 1, type(uint64).max);
minimum = bound(minimum, amount + 1, type(uint256).max);

// Setup: activate ALL revert conditions simultaneously.
// REDEEM is paused, the sender policy blocks all callers,
// minimumRedeemable > amount (so shares == amount < minimum with the
// default 1:1 WAD ratio), and alice has zero balance.
_pause(IB20.PausableFeature.REDEEM);
_setRedeemPolicy(PolicyRegistryConstants.ALWAYS_BLOCK_ID);
_updateMinimumRedeemable(minimum);
// alice balance is zero by default.

// 1. PAUSE fires first.
vm.prank(alice);
vm.expectRevert(abi.encodeWithSelector(IB20.ContractPaused.selector, IB20.PausableFeature.REDEEM));
security().redeemWithMemo(amount, memo);

// Fix: unpause REDEEM.
_unpause(IB20.PausableFeature.REDEEM);

// 2. POLICY fires next (pause fixed).
vm.prank(alice);
vm.expectRevert(
abi.encodeWithSelector(
IB20.PolicyForbids.selector, security().REDEEM_SENDER_POLICY(), PolicyRegistryConstants.ALWAYS_BLOCK_ID
)
);
security().redeemWithMemo(amount, memo);

// Fix: allow alice through the redeem-sender policy.
_setRedeemPolicy(PolicyRegistryConstants.ALWAYS_ALLOW_ID);

// 3. BELOW-MIN fires next (pause and policy fixed).
// With the default 1:1 WAD ratio shares == amount, and amount < minimum.
vm.prank(alice);
vm.expectRevert(abi.encodeWithSelector(IB20Security.BelowMinimumRedeemable.selector, amount, minimum));
security().redeemWithMemo(amount, memo);

// Fix: clear the minimum floor so shares == amount satisfies the check.
_updateMinimumRedeemable(0);

// 4. BALANCE fires next (pause, policy, and minimum fixed).
vm.prank(alice);
vm.expectRevert(abi.encodeWithSelector(IB20.InsufficientBalance.selector, alice, 0, amount));
security().redeemWithMemo(amount, memo);

// Fix: give alice exactly enough balance to cover the redemption.
_mint(alice, amount);

// Success: all conditions resolved.
vm.prank(alice);
security().redeemWithMemo(amount, memo);
}
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this has no tests in it? Also fine to skip tbh given we are cutting this functionality from B20 tonight

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nvm, I understand now, these are just documenting for clarity

Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import {IB20} from "src/interfaces/IB20.sol";

import {B20Constants} from "src/lib/B20Constants.sol";

import {B20SecurityTest} from "test/lib/B20SecurityTest.sol";

/// @title Sequential revert-order test for `updateMinimumRedeemable`.
///
/// @notice **Canonical order (Solidity reference):**
/// 1. ROLE (`onlyRole(DEFAULT_ADMIN_ROLE)` modifier) → `AccessControlUnauthorizedAccount`
///
/// A single test verifies the guard fires for an unauthorized caller and then
/// confirms the call succeeds once the role is granted.
contract B20SecurityUpdateMinimumRedeemableRevertOrderTest is B20SecurityTest {
function test_updateMinimumRedeemable_revertOrder(address caller, uint256 newMinimum) public {
// Exclude precompiles (which can distort msg.sender) and admin (who already
// holds DEFAULT_ADMIN_ROLE, which would make the initial call succeed).
_assumeValidCaller(caller);
vm.assume(caller != admin);

// 1. ROLE fires.
vm.prank(caller);
vm.expectRevert(
abi.encodeWithSelector(
IB20.AccessControlUnauthorizedAccount.selector, caller, B20Constants.DEFAULT_ADMIN_ROLE
)
);
security().updateMinimumRedeemable(newMinimum);

// Fix: grant DEFAULT_ADMIN_ROLE to caller.
_grantRole(B20Constants.DEFAULT_ADMIN_ROLE, caller);

// Success: all conditions resolved.
vm.prank(caller);
security().updateMinimumRedeemable(newMinimum);
}
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

missing tests?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nvm, I understand now, these are just documenting for clarity

Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import {IB20} from "src/interfaces/IB20.sol";

import {B20SecurityTest} from "test/lib/B20SecurityTest.sol";

/// @title Sequential revert-order test for `updateShareRatio`.
///
/// @notice **Canonical order (Solidity reference):**
/// 1. ROLE (`onlyRole(SECURITY_OPERATOR_ROLE)` modifier) → `AccessControlUnauthorizedAccount`
///
/// A single test verifies the guard fires for an unauthorized caller and then
/// confirms the call succeeds once the role is granted.
contract B20SecurityUpdateShareRatioRevertOrderTest is B20SecurityTest {
function test_updateShareRatio_revertOrder(address caller, uint256 newRatio) public {
// Exclude precompiles (which can distort msg.sender) and admin (needed
// internally by _grantRole to approve the role grant).
_assumeValidCaller(caller);
vm.assume(caller != admin);

// Resolve the role identifier before setting the prank; an external view
// call inside abi.encodeWithSelector would otherwise consume vm.prank
// before the state-changing call, sending it as address(this) instead.
bytes32 operatorRole = security().SECURITY_OPERATOR_ROLE();

// 1. ROLE fires.
vm.prank(caller);
vm.expectRevert(abi.encodeWithSelector(IB20.AccessControlUnauthorizedAccount.selector, caller, operatorRole));
security().updateShareRatio(newRatio);

// Fix: grant SECURITY_OPERATOR_ROLE to caller.
_grantRole(operatorRole, caller);

// Success: all conditions resolved.
vm.prank(caller);
security().updateShareRatio(newRatio);
}
}
Loading