Skip to content
Closed
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
88 changes: 14 additions & 74 deletions test/lib/B20AssetTest.sol
Original file line number Diff line number Diff line change
Expand Up @@ -7,86 +7,45 @@ import {IB20Asset} from "base-std/interfaces/IB20Asset.sol";

/// @notice Base test contract for `IB20Asset` unit tests.
///
/// Extends `B20Test` for the inherited test surface (actors, labels,
/// setUp wiring, the `_singleFeature` helper, the `_grantRole` /
/// `_mint` / `_pause` action wrappers, and the asset-variant token
/// deployed by `_deployToken`). Adds the variant-specific role holder
/// (`operator`) plus helpers for the announcement, multiplier,
/// and extra-metadata surfaces.
/// Extends `B20Test` for the inherited surface (actors, labels, setUp
/// wiring, role helpers, and the asset-variant token deployed by
/// `_deployToken`). Adds asset-specific actors and helpers for the
/// multiplier surface.
///
/// The inherited `token` member is typed `IB20`. Tests that need the
/// variant-only surface (`announce`, `batchMint`, etc.) cast inline via
/// the `asset` view-helper.
/// The inherited `token` member is typed `IB20`. Tests that need
/// asset-only methods cast via the `asset()` helper.
contract B20AssetTest is B20Test {
// -- Asset-variant role-holder actors --
address internal operator = makeAddr("operator");

// ============================================================
// ASSET-VARIANT EXTRA-METADATA FIXTURES
// ============================================================
// Test-only metadata-entry keys. The `extraMetadata` surface is
// a variant-agnostic key/value store; these three keys form a
// coherent generic example so tests don't accidentally encode a
// assets-specific assumption. All entries are post-creation
// additions exercised only by the variant tests; the factory does
// not seed any entry at bootstrap.

/// @notice Example metadata-entry key #1. String value chosen for readability;
/// the constant name stays generic so tests don't encode any
/// variant-specific assumption about what keys mean.
string internal constant METADATA_EXAMPLE_1 = "category";

/// @notice Example metadata-entry key #2.
string internal constant METADATA_EXAMPLE_2 = "region";

/// @notice Example metadata-entry key #3.
string internal constant METADATA_EXAMPLE_3 = "reference";

// -- Setup --
bytes32 internal constant OPERATOR_ROLE = keccak256("OPERATOR_ROLE");

function setUp() public virtual override {
super.setUp();
vm.label(operator, "operator");
}

// ============================================================
// VARIANT CAST CONVENIENCE
// ============================================================

/// @notice Returns `token` cast to `IB20Asset`. Saves typing
/// `IB20Asset(address(token))` at every callsite.
/// @notice Returns `token` cast to `IB20Asset`.
function asset() internal view returns (IB20Asset) {
return IB20Asset(address(token));
}

// ============================================================
// ASSET-ROLE HELPERS
// ============================================================

/// @notice Grants `OPERATOR_ROLE` to the `operator` actor as
/// the admin, idempotent.
/// @notice Grants `OPERATOR_ROLE` to the `operator` actor as admin.
function _grantOperator() internal {
bytes32 role = asset().OPERATOR_ROLE();
if (!token.hasRole(role, operator)) _grantRole(role, operator);
_grantRole(asset().OPERATOR_ROLE(), operator);
}

// ============================================================
// MULTIPLIER HELPERS
// ============================================================

/// @notice Sets the multiplier via the `operator` actor, lazily
/// granting `OPERATOR_ROLE` on first call.
/// @notice Sets the multiplier via the `operator` actor.
function _updateMultiplier(uint256 newMultiplier) internal {
_grantOperator();
vm.prank(operator);
asset().updateMultiplier(newMultiplier);
}

// ============================================================
// ANNOUNCEMENT HELPERS
// ============================================================

/// @notice Calls `announce` from the `operator` actor with explicit
/// caller, internalCalls, id, description, and URI.
/// @notice Posts an announcement from `caller` with the given fields.
function _announce(
address caller,
bytes[] memory internalCalls,
Expand All @@ -98,19 +57,12 @@ contract B20AssetTest is B20Test {
asset().announce(internalCalls, id, description, uri);
}

/// @notice Calls `announce` with defaults: `operator` caller, empty
/// internalCalls, plain description and URI. The caller
/// supplies the id so successive `_announce()` invocations
/// within one test don't collide on the consumed-id guard.
/// @notice Posts a pure-disclosure announcement from the `operator` actor.
function _announce(string memory id) internal {
_grantOperator();
_announce(operator, new bytes[](0), id, "description", "https://disclosures.example/");
}

// ============================================================
// BATCH HELPERS
// ============================================================

/// @notice Wraps a single address in a length-1 memory array.
function _singletonAddresses(address account) internal pure returns (address[] memory accounts) {
accounts = new address[](1);
Expand All @@ -128,16 +80,4 @@ contract B20AssetTest is B20Test {
blobs = new bytes[](1);
blobs[0] = blob;
}

// ============================================================
// VARIANT-ONLY CONSTANTS
// ============================================================
// Compile-time copies of the contract's variant-only constants.
// Tests reference these when they need the value in a context that
// can't make a contract call (e.g. inside a struct literal). The
// values match `asset().OPERATOR_ROLE()` etc. by construction;
// the per-constant test in `test/unit/B20Asset/constants/` pins
// that down.

bytes32 internal constant OPERATOR_ROLE = keccak256("OPERATOR_ROLE");
}
59 changes: 15 additions & 44 deletions test/unit/B20Asset/multiplier/updateMultiplier.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -3,59 +3,30 @@ pragma solidity ^0.8.20;

import {B20AssetTest} from "base-std-test/lib/B20AssetTest.sol";

import {IB20} from "base-std/interfaces/IB20.sol";
import {IB20Asset} from "base-std/interfaces/IB20Asset.sol";

import {MockB20AssetStorage} from "base-std-test/lib/mocks/MockB20Storage.sol";

contract B20AssetUpdateMultiplierTest is B20AssetTest {
/// @notice Verifies updateMultiplier reverts when caller lacks OPERATOR_ROLE
/// @dev Access control: only role-holders can rotate the multiplier; checks
/// AccessControlUnauthorizedAccount with OPERATOR_ROLE.
/// @notice Verifies updateMultiplier reverts when caller lacks the operator role
/// @dev Access control; checks AccessControlUnauthorizedAccount
function test_updateMultiplier_revert_unauthorized(address caller, uint256 newMultiplier) public {
_assumeValidCaller(caller);
vm.assume(caller != admin);
vm.assume(caller != operator);

vm.prank(caller);
vm.expectRevert(abi.encodeWithSelector(IB20.AccessControlUnauthorizedAccount.selector, caller, OPERATOR_ROLE));
asset().updateMultiplier(newMultiplier);
// unimplemented
}

/// @notice Verifies updateMultiplier reverts when newMultiplier is zero
/// @dev Input validation: zero is an invalid multiplier because stored zero is the
/// uninitialized-storage sentinel (read path normalizes it to WAD). Passing zero
/// would create an event/read inconsistency for off-chain indexers.
/// @dev Zero is the uninitialized-storage sentinel that normalizes to WAD on reads;
/// writing it through the public path creates an event/read inconsistency.
/// Checks InvalidMultiplier()
function test_updateMultiplier_revert_zeroMultiplier() public {
_grantOperator();
vm.prank(operator);
vm.expectRevert(IB20Asset.InvalidMultiplier.selector);
asset().updateMultiplier(0);
// unimplemented
}

/// @notice Verifies updateMultiplier writes the new value to the stored slot
/// @dev State invariant: the stored slot holds the supplied multiplier verbatim (no clamping,
/// no scaling). Paired slot assertion verifies the storage write lands at the
/// multiplier slot.
function test_updateMultiplier_success_writesSlot(uint256 newMultiplier) public {
vm.assume(newMultiplier != 0);
_updateMultiplier(newMultiplier);
assertEq(
uint256(vm.load(address(token), MockB20AssetStorage.multiplierSlot())),
newMultiplier,
"stored multiplier slot must reflect the write"
);
/// @notice Verifies updateMultiplier stores the new value and multiplier() reflects it
/// @dev Read-after-write: multiplier() returns newMultiplier
function test_updateMultiplier_success_storesMultiplier(uint256 newMultiplier) public {
// unimplemented
}

/// @notice Verifies updateMultiplier emits MultiplierUpdated with the new value
/// @dev Event integrity for the rotation; subscribers depend on this event to
/// re-derive holder scaled balances off-chain.
function test_updateMultiplier_success_emitsEvent(uint256 newMultiplier) public {
vm.assume(newMultiplier != 0);
_grantOperator();
vm.expectEmit(false, false, false, true, address(token));
emit IB20Asset.MultiplierUpdated(newMultiplier);
vm.prank(operator);
asset().updateMultiplier(newMultiplier);
/// @notice Verifies updateMultiplier emits MultiplierUpdated(newMultiplier)
/// @dev Event integrity; canonical MultiplierUpdated emission test
function test_updateMultiplier_success_emitsMultiplierUpdated(uint256 newMultiplier) public {
// unimplemented
}
}
Loading