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

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

import {B20Test} from "test/lib/B20Test.sol";
import {MockB20, B20Constants} from "test/lib/mocks/MockB20.sol";
import {PolicyRegistryConstants} from "test/lib/mocks/MockPolicyRegistry.sol";

/// @title Sequential check-order test for `transferFromWithMemo`.
///
/// @notice `transferFromWithMemo` carries the same preconditions as
/// `transferFrom`; the memo parameter adds no new revert conditions.
///
/// **Canonical order (Solidity reference, when `msg.sender != from`):**
/// 1. PAUSE (`whenNotPaused(TRANSFER)` modifier) → `ContractPaused`
/// 2. ZERO-RECEIVER (`to == address(0)`) → `InvalidReceiver`
/// 3. ZERO-SENDER (`from == address(0)`) → `InvalidSender`
/// 4. ALLOWANCE (`_consumeAllowance`) → `InsufficientAllowance`
/// 5. EXECUTOR-POLICY (`isAuthorized(executorPolicyId, msg.sender)`)
/// → `PolicyForbids(EXECUTOR, ...)`
/// 6. SENDER-POLICY (first `_transfer` body check, representative)
/// → `PolicyForbids(SENDER, ...)`
///
/// The single test below activates all conditions simultaneously,
/// then fixes them one at a time in canonical order, asserting that
/// the next-priority revert fires at each step.
contract B20TransferFromWithMemoRevertOrderTest is B20Test {
function test_transferFromWithMemo_revertOrder(
address caller,
address from,
address to,
uint256 amount,
bytes32 memo
) public {
_assumeValidCaller(caller);
_assumeValidActor(from);
_assumeValidActor(to);
vm.assume(caller != from);
amount = bound(amount, 1, type(uint128).max);

// Activate all conditions: TRANSFER paused, from=address(0) and to=address(0)
// as parameters, no allowance, executor policy blocks, sender policy blocks.
_pause(IB20.PausableFeature.TRANSFER);
_setPolicy(B20Constants.TRANSFER_EXECUTOR_POLICY, PolicyRegistryConstants.ALWAYS_BLOCK_ID);
_setPolicy(B20Constants.TRANSFER_SENDER_POLICY, PolicyRegistryConstants.ALWAYS_BLOCK_ID);

// 1. PAUSE fires first (all other violations also active).
vm.prank(caller);
vm.expectRevert(abi.encodeWithSelector(IB20.ContractPaused.selector, IB20.PausableFeature.TRANSFER));
token.transferFromWithMemo(address(0), address(0), amount, memo);
_grantRole(B20Constants.UNPAUSE_ROLE, unpauser);
vm.prank(unpauser);
token.unpause(_singleFeature(IB20.PausableFeature.TRANSFER));

// 2. ZERO-RECEIVER fires (pause cleared; from=address(0), to=address(0), no allowance,
// policies block).
vm.prank(caller);
vm.expectRevert(abi.encodeWithSelector(IB20.InvalidReceiver.selector, address(0)));
token.transferFromWithMemo(address(0), address(0), amount, memo);

// 3. ZERO-SENDER fires (pause+zero-receiver cleared; from=address(0), to=valid).
vm.prank(caller);
vm.expectRevert(abi.encodeWithSelector(IB20.InvalidSender.selector, address(0)));
token.transferFromWithMemo(address(0), to, amount, memo);

// 4. ALLOWANCE fires (pause+zero-addr cleared; from=valid, to=valid, no allowance,
// executor and sender policies still block).
vm.prank(caller);
vm.expectRevert(abi.encodeWithSelector(IB20.InsufficientAllowance.selector, caller, 0, amount));
token.transferFromWithMemo(from, to, amount, memo);
vm.prank(from);
token.approve(caller, amount);

// 5. EXECUTOR-POLICY fires (all earlier cleared; executor policy blocks,
// sender policy also blocks).
vm.prank(caller);
vm.expectRevert(
abi.encodeWithSelector(
IB20.PolicyForbids.selector,
B20Constants.TRANSFER_EXECUTOR_POLICY,
PolicyRegistryConstants.ALWAYS_BLOCK_ID
)
);
token.transferFromWithMemo(from, to, amount, memo);
_setPolicy(B20Constants.TRANSFER_EXECUTOR_POLICY, PolicyRegistryConstants.ALWAYS_ALLOW_ID);

// 6. SENDER-POLICY fires (all earlier cleared; sender policy still blocks,
// from has no balance — sender policy fires first).
vm.prank(caller);
vm.expectRevert(
abi.encodeWithSelector(
IB20.PolicyForbids.selector,
B20Constants.TRANSFER_SENDER_POLICY,
PolicyRegistryConstants.ALWAYS_BLOCK_ID
)
);
token.transferFromWithMemo(from, to, amount, memo);
_setPolicy(B20Constants.TRANSFER_SENDER_POLICY, PolicyRegistryConstants.ALWAYS_ALLOW_ID);
_mint(from, amount);

// Success — all conditions satisfied.
vm.prank(caller);
token.transferFromWithMemo(from, to, amount, memo);
}
}
93 changes: 93 additions & 0 deletions test/unit/B20/erc20/transferWithMemo_revertOrder.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

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

import {B20Test} from "test/lib/B20Test.sol";
import {MockB20, B20Constants} from "test/lib/mocks/MockB20.sol";
import {PolicyRegistryConstants} from "test/lib/mocks/MockPolicyRegistry.sol";

/// @title Sequential check-order test for `transferWithMemo`.
///
/// @notice `transferWithMemo` carries the same preconditions as `transfer`;
/// the memo parameter adds no new revert conditions.
///
/// **Canonical order (Solidity reference):**
/// 1. PAUSE (`whenNotPaused(TRANSFER)` modifier) → `ContractPaused`
/// 2. ZERO-RECEIVER (`to == address(0)`) → `InvalidReceiver`
/// 3. ZERO-SENDER (`from == address(0)`) → `InvalidSender`
/// 4. SENDER-POLICY (`_transfer` body) → `PolicyForbids(SENDER, ...)`
/// 5. RECEIVER-POLICY (`_transfer` body) → `PolicyForbids(RECEIVER, ...)`
/// 6. BALANCE (`_transfer` body) → `InsufficientBalance`
///
/// The public `transferWithMemo(to, amount, memo)` entry sets
/// `from = msg.sender`. The single test below activates all six
/// violations simultaneously, then fixes them one at a time in
/// canonical order, asserting that the next-priority revert fires
/// at each step.
contract B20TransferWithMemoRevertOrderTest is B20Test {
function test_transferWithMemo_revertOrder(address from, address to, uint256 amount, bytes32 memo) public {
_assumeValidActor(from);
_assumeValidActor(to);
amount = bound(amount, 1, type(uint128).max);

// Activate all six violations: TRANSFER paused, from=address(0) (via prank),
// to=address(0), sender policy blocks, receiver policy blocks, from has zero balance.
_pause(IB20.PausableFeature.TRANSFER);
_setPolicy(B20Constants.TRANSFER_SENDER_POLICY, PolicyRegistryConstants.ALWAYS_BLOCK_ID);
_setPolicy(B20Constants.TRANSFER_RECEIVER_POLICY, PolicyRegistryConstants.ALWAYS_BLOCK_ID);

// 1. PAUSE fires first (all other violations also active; pranking address(0) for ZERO-SENDER).
vm.prank(address(0));
vm.expectRevert(abi.encodeWithSelector(IB20.ContractPaused.selector, IB20.PausableFeature.TRANSFER));
token.transferWithMemo(address(0), amount, memo);
_grantRole(B20Constants.UNPAUSE_ROLE, unpauser);
vm.prank(unpauser);
token.unpause(_singleFeature(IB20.PausableFeature.TRANSFER));

// 2. ZERO-RECEIVER fires (pause cleared; from=address(0), to=address(0),
// policies block, zero balance — receiver is checked before sender).
vm.prank(address(0));
vm.expectRevert(abi.encodeWithSelector(IB20.InvalidReceiver.selector, address(0)));
token.transferWithMemo(address(0), amount, memo);

// 3. ZERO-SENDER fires (pause+zero-receiver cleared; from=address(0), to=valid).
vm.prank(address(0));
vm.expectRevert(abi.encodeWithSelector(IB20.InvalidSender.selector, address(0)));
token.transferWithMemo(to, amount, memo);

// 4. SENDER-POLICY fires (all earlier cleared; sender policy blocks, receiver also blocks).
vm.prank(from);
vm.expectRevert(
abi.encodeWithSelector(
IB20.PolicyForbids.selector,
B20Constants.TRANSFER_SENDER_POLICY,
PolicyRegistryConstants.ALWAYS_BLOCK_ID
)
);
token.transferWithMemo(to, amount, memo);
_setPolicy(B20Constants.TRANSFER_SENDER_POLICY, PolicyRegistryConstants.ALWAYS_ALLOW_ID);

// 5. RECEIVER-POLICY fires (all earlier cleared; receiver policy still blocks).
vm.prank(from);
vm.expectRevert(
abi.encodeWithSelector(
IB20.PolicyForbids.selector,
B20Constants.TRANSFER_RECEIVER_POLICY,
PolicyRegistryConstants.ALWAYS_BLOCK_ID
)
);
token.transferWithMemo(to, amount, memo);
_setPolicy(B20Constants.TRANSFER_RECEIVER_POLICY, PolicyRegistryConstants.ALWAYS_ALLOW_ID);

// 6. BALANCE fires (all earlier cleared; from has zero balance, amount>0).
vm.prank(from);
vm.expectRevert(abi.encodeWithSelector(IB20.InsufficientBalance.selector, from, 0, amount));
token.transferWithMemo(to, amount, memo);
_mint(from, amount);

// Success — all conditions satisfied.
vm.prank(from);
token.transferWithMemo(to, amount, memo);
}
}
33 changes: 33 additions & 0 deletions test/unit/B20/metadata/updateContractURI_revertOrder.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

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

import {B20Test} from "test/lib/B20Test.sol";
import {MockB20, B20Constants} from "test/lib/mocks/MockB20.sol";

/// @title Sequential check-order test for `updateContractURI`.
///
/// @notice **Canonical order (Solidity reference):**
/// 1. ROLE (`onlyRole(METADATA_ROLE)` modifier) → `AccessControlUnauthorizedAccount`
///
/// Single condition: caller must hold METADATA_ROLE. The test fires the
/// revert, grants the required role, and verifies success.
contract B20UpdateContractURIRevertOrderTest is B20Test {
function test_updateContractURI_revertOrder(address caller, string calldata newURI) public {
_assumeValidCaller(caller);
vm.assume(!token.hasRole(B20Constants.METADATA_ROLE, caller));

// 1. ROLE fires (caller lacks METADATA_ROLE).
vm.prank(caller);
vm.expectRevert(
abi.encodeWithSelector(IB20.AccessControlUnauthorizedAccount.selector, caller, B20Constants.METADATA_ROLE)
);
token.updateContractURI(newURI);
_grantRole(B20Constants.METADATA_ROLE, caller);

// Success — caller now holds METADATA_ROLE.
vm.prank(caller);
token.updateContractURI(newURI);
}
}
33 changes: 33 additions & 0 deletions test/unit/B20/metadata/updateName_revertOrder.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

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

import {B20Test} from "test/lib/B20Test.sol";
import {MockB20, B20Constants} from "test/lib/mocks/MockB20.sol";

/// @title Sequential check-order test for `updateName`.
///
/// @notice **Canonical order (Solidity reference):**
/// 1. ROLE (`onlyRole(METADATA_ROLE)` modifier) → `AccessControlUnauthorizedAccount`
///
/// Single condition: caller must hold METADATA_ROLE. The test fires the
/// revert, grants the required role, and verifies success.
contract B20UpdateNameRevertOrderTest is B20Test {
function test_updateName_revertOrder(address caller, string calldata newName) public {
_assumeValidCaller(caller);
vm.assume(!token.hasRole(B20Constants.METADATA_ROLE, caller));

// 1. ROLE fires (caller lacks METADATA_ROLE).
vm.prank(caller);
vm.expectRevert(
abi.encodeWithSelector(IB20.AccessControlUnauthorizedAccount.selector, caller, B20Constants.METADATA_ROLE)
);
token.updateName(newName);
_grantRole(B20Constants.METADATA_ROLE, caller);

// Success — caller now holds METADATA_ROLE.
vm.prank(caller);
token.updateName(newName);
}
}
33 changes: 33 additions & 0 deletions test/unit/B20/metadata/updateSymbol_revertOrder.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

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

import {B20Test} from "test/lib/B20Test.sol";
import {MockB20, B20Constants} from "test/lib/mocks/MockB20.sol";

/// @title Sequential check-order test for `updateSymbol`.
///
/// @notice **Canonical order (Solidity reference):**
/// 1. ROLE (`onlyRole(METADATA_ROLE)` modifier) → `AccessControlUnauthorizedAccount`
///
/// Single condition: caller must hold METADATA_ROLE. The test fires the
/// revert, grants the required role, and verifies success.
contract B20UpdateSymbolRevertOrderTest is B20Test {
function test_updateSymbol_revertOrder(address caller, string calldata newSymbol) public {
_assumeValidCaller(caller);
vm.assume(!token.hasRole(B20Constants.METADATA_ROLE, caller));

// 1. ROLE fires (caller lacks METADATA_ROLE).
vm.prank(caller);
vm.expectRevert(
abi.encodeWithSelector(IB20.AccessControlUnauthorizedAccount.selector, caller, B20Constants.METADATA_ROLE)
);
token.updateSymbol(newSymbol);
_grantRole(B20Constants.METADATA_ROLE, caller);

// Success — caller now holds METADATA_ROLE.
vm.prank(caller);
token.updateSymbol(newSymbol);
}
}
44 changes: 44 additions & 0 deletions test/unit/B20/pause/pause_revertOrder.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

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

import {B20Test} from "test/lib/B20Test.sol";
import {MockB20, B20Constants} from "test/lib/mocks/MockB20.sol";

/// @title Sequential check-order test for `pause`.
///
/// @notice **Canonical order (Solidity reference):**
/// 1. ROLE (`onlyRole(PAUSE_ROLE)` modifier) → `AccessControlUnauthorizedAccount`
/// 2. EMPTY-SET (`features.length == 0`) → `EmptyFeatureSet`
///
/// The single test below activates both violations simultaneously,
/// then fixes them one at a time in canonical order, asserting that
/// the next-priority revert fires at each step.
contract B20PauseRevertOrderTest is B20Test {
function test_pause_revertOrder(address caller) public {
_assumeValidCaller(caller);
vm.assume(caller != admin);

IB20.PausableFeature[] memory empty = new IB20.PausableFeature[](0);

// Both conditions active: caller lacks PAUSE_ROLE, features is empty.

// 1. ROLE fires first (empty-set also violated).
vm.prank(caller);
vm.expectRevert(
abi.encodeWithSelector(IB20.AccessControlUnauthorizedAccount.selector, caller, B20Constants.PAUSE_ROLE)
);
token.pause(empty);
_grantRole(B20Constants.PAUSE_ROLE, caller);

// 2. EMPTY-SET fires (role cleared; features still empty).
vm.prank(caller);
vm.expectRevert(abi.encodeWithSelector(IB20.EmptyFeatureSet.selector));
token.pause(empty);

// Success — all conditions satisfied.
vm.prank(caller);
token.pause(_singleFeature(IB20.PausableFeature.TRANSFER));
}
}
45 changes: 45 additions & 0 deletions test/unit/B20/pause/unpause_revertOrder.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

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

import {B20Test} from "test/lib/B20Test.sol";
import {MockB20, B20Constants} from "test/lib/mocks/MockB20.sol";

/// @title Sequential check-order test for `unpause`.
///
/// @notice **Canonical order (Solidity reference):**
/// 1. ROLE (`onlyRole(UNPAUSE_ROLE)` modifier) → `AccessControlUnauthorizedAccount`
/// 2. EMPTY-SET (`features.length == 0`) → `EmptyFeatureSet`
///
/// The single test below activates both violations simultaneously,
/// then fixes them one at a time in canonical order, asserting that
/// the next-priority revert fires at each step.
contract B20UnpauseRevertOrderTest is B20Test {
function test_unpause_revertOrder(address caller) public {
_assumeValidCaller(caller);
vm.assume(caller != admin);

IB20.PausableFeature[] memory empty = new IB20.PausableFeature[](0);

// Both conditions active: caller lacks UNPAUSE_ROLE, features is empty.

// 1. ROLE fires first (empty-set also violated).
vm.prank(caller);
vm.expectRevert(
abi.encodeWithSelector(IB20.AccessControlUnauthorizedAccount.selector, caller, B20Constants.UNPAUSE_ROLE)
);
token.unpause(empty);
_grantRole(B20Constants.UNPAUSE_ROLE, caller);

// 2. EMPTY-SET fires (role cleared; features still empty).
vm.prank(caller);
vm.expectRevert(abi.encodeWithSelector(IB20.EmptyFeatureSet.selector));
token.unpause(empty);

// Success — all conditions satisfied.
_pause(IB20.PausableFeature.TRANSFER);
vm.prank(caller);
token.unpause(_singleFeature(IB20.PausableFeature.TRANSFER));
}
}
Loading
Loading