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
15 changes: 10 additions & 5 deletions src/interfaces/IB20.sol
Original file line number Diff line number Diff line change
Expand Up @@ -233,13 +233,18 @@ interface IB20 {

/// @notice Emitted by `transferWithMemo`, `transferFromWithMemo`,
/// `mintWithMemo`, and `burnWithMemo` immediately AFTER the
/// underlying ERC-20 `Transfer` event. The memo carries no
/// from/to/amount fields; indexers join it to the preceding
/// `Transfer` log via `(transactionHash, logIndex - 1)`.
/// underlying ERC-20 `Transfer` event. Carries `from`, `to`,
/// `amount`, and a contract-global `nonce` so it is fully
/// self-contained — no join to the preceding `Transfer` log
/// is required. `nonce` is a monotonically incrementing
/// counter scoped to this token; it uniquely identifies each
/// memo'd operation even when `(from, to, amount, memo)` repeats
/// (e.g. repeated payments of the same invoice).
/// @dev Variants may emit this event from additional functions
/// (e.g. `redeem` on a Security token); the event signature
/// is shared.
event Memo(bytes32 indexed memo);
/// is shared. For mint variants `from == address(0)`; for
/// burn variants `to == address(0)`.
event Memo(address indexed from, address indexed to, uint256 amount, bytes32 indexed memo, uint256 nonce);

/// @notice Emitted by `burnBlocked` in addition to the standard
/// `Transfer(from, address(0), amount)`. Distinguishes
Expand Down
20 changes: 16 additions & 4 deletions test/lib/mocks/MockB20.sol
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,10 @@ contract MockB20 is IB20 {

function transferWithMemo(address to, uint256 amount, bytes32 memo) external returns (bool) {
_transfer(msg.sender, to, amount);
emit Memo(memo);
MockB20Storage.Layout storage $ = MockB20Storage.layout();
uint256 nonce = $.memoNonce;
unchecked { $.memoNonce = nonce + 1; }
emit Memo(msg.sender, to, amount, memo, nonce);
return true;
}

Expand All @@ -187,7 +190,10 @@ contract MockB20 is IB20 {
}
}
_transfer(from, to, amount);
emit Memo(memo);
MockB20Storage.Layout storage $ = MockB20Storage.layout();
uint256 nonce = $.memoNonce;
unchecked { $.memoNonce = nonce + 1; }
emit Memo(from, to, amount, memo, nonce);
return true;
}

Expand Down Expand Up @@ -217,7 +223,10 @@ contract MockB20 is IB20 {

function mintWithMemo(address to, uint256 amount, bytes32 memo) external {
_mint(to, amount);
emit Memo(memo);
MockB20Storage.Layout storage $ = MockB20Storage.layout();
uint256 nonce = $.memoNonce;
unchecked { $.memoNonce = nonce + 1; }
emit Memo(address(0), to, amount, memo, nonce);
}

function burn(uint256 amount) external {
Expand All @@ -226,7 +235,10 @@ contract MockB20 is IB20 {

function burnWithMemo(uint256 amount, bytes32 memo) external {
_burnSelf(msg.sender, amount);
emit Memo(memo);
MockB20Storage.Layout storage $ = MockB20Storage.layout();
uint256 nonce = $.memoNonce;
unchecked { $.memoNonce = nonce + 1; }
emit Memo(msg.sender, address(0), amount, memo, nonce);
}

function burnBlocked(address from, uint256 amount) external {
Expand Down
6 changes: 6 additions & 0 deletions test/lib/mocks/MockB20Storage.sol
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,11 @@ library MockB20Storage {
// gates (role / policy / pause checks). Token invariants (supply-cap
// math, balance accounting) are NOT bypassed.
bool initialized;
// ---------- Memo nonce ----------
// Monotonically incrementing counter for memo'd transfers, mints, and
// burns. Incremented on each *WithMemo call so the Memo event carries
// a globally unique nonce even when (from, to, amount, memo) repeats.
uint256 memoNonce;
}

// keccak256(abi.encode(uint256(keccak256("base.b20")) - 1)) & ~bytes32(uint256(0xff))
Expand Down Expand Up @@ -127,6 +132,7 @@ library MockB20Storage {
uint256 internal constant SUPPLY_CAP_OFFSET = 12;
uint256 internal constant NONCES_OFFSET = 13;
uint256 internal constant INITIALIZED_OFFSET = 14;
uint256 internal constant MEMO_NONCE_OFFSET = 15;

/// @notice Absolute slot for a top-level field of `Layout`.
/// @dev `STORAGE_LOCATION + offset`. The struct never crosses the
Expand Down
9 changes: 5 additions & 4 deletions test/unit/B20/memo/burnWithMemo.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -34,16 +34,17 @@ contract B20BurnWithMemoTest is B20Test {
assertEq(token.totalSupply(), supplyBefore - amount, "supply decreased");
}

/// @notice Verifies burnWithMemo emits Transfer(caller, address(0), amount) then Memo(memo)
/// @dev Event ordering: Memo follows Transfer; canonical Memo test for the burn path
/// @notice Verifies burnWithMemo emits Transfer(caller, address(0), amount) then Memo
/// @dev Memo is self-contained: carries from, to, amount, memo, and a unique nonce.
/// nonce is 0 for the first memo'd operation on a freshly-etched token.
function test_burnWithMemo_success_emitsTransferThenMemo(uint256 amount, bytes32 memo) public {
_grantRole(B20Constants.BURN_ROLE, burner);
_mint(burner, amount);

vm.expectEmit(true, true, false, true, address(token));
emit IB20.Transfer(burner, address(0), amount);
vm.expectEmit(true, false, false, false, address(token));
emit IB20.Memo(memo);
vm.expectEmit(address(token));
emit IB20.Memo(burner, address(0), amount, memo, 0);
vm.prank(burner);
token.burnWithMemo(amount, memo);
}
Expand Down
9 changes: 5 additions & 4 deletions test/unit/B20/memo/mintWithMemo.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -37,16 +37,17 @@ contract B20MintWithMemoTest is B20Test {
assertEq(token.totalSupply(), supplyBefore + amount, "supply increased");
}

/// @notice Verifies mintWithMemo emits Transfer(address(0), to, amount) then Memo(memo)
/// @dev Event ordering: Memo follows Transfer; canonical Memo test for the mint path
/// @notice Verifies mintWithMemo emits Transfer(address(0), to, amount) then Memo
/// @dev Memo is self-contained: carries from, to, amount, memo, and a unique nonce.
/// nonce is 0 for the first memo'd operation on a freshly-etched token.
function test_mintWithMemo_success_emitsTransferThenMemo(address to, uint256 amount, bytes32 memo) public {
_assumeValidActor(to);
_grantRole(B20Constants.MINT_ROLE, minter);

vm.expectEmit(true, true, false, true, address(token));
emit IB20.Transfer(address(0), to, amount);
vm.expectEmit(true, false, false, false, address(token));
emit IB20.Memo(memo);
vm.expectEmit(address(token));
emit IB20.Memo(address(0), to, amount, memo, 0);
vm.prank(minter);
token.mintWithMemo(to, amount, memo);
}
Expand Down
7 changes: 4 additions & 3 deletions test/unit/B20/memo/transferFromWithMemo.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,8 @@ contract B20TransferFromWithMemoTest is B20Test {
}

/// @notice Verifies transferFromWithMemo emits Transfer then Memo, in that order
/// @dev Memo is the second log; canonical Memo test for the transferFrom path
/// @dev Memo is self-contained: carries from, to, amount, memo, and a unique nonce.
/// nonce is 0 for the first memo'd operation on a freshly-etched token.
function test_transferFromWithMemo_success_emitsTransferThenMemo(
address caller,
address from,
Expand All @@ -76,8 +77,8 @@ contract B20TransferFromWithMemoTest is B20Test {

vm.expectEmit(true, true, false, true, address(token));
emit IB20.Transfer(from, to, amount);
vm.expectEmit(true, false, false, false, address(token));
emit IB20.Memo(memo);
vm.expectEmit(address(token));
emit IB20.Memo(from, to, amount, memo, 0);
vm.prank(caller);
token.transferFromWithMemo(from, to, amount, memo);
}
Expand Down
7 changes: 4 additions & 3 deletions test/unit/B20/memo/transferWithMemo.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,8 @@ contract B20TransferWithMemoTest is B20Test {
}

/// @notice Verifies transferWithMemo emits Transfer then Memo, in that order
/// @dev Memo is the second log; canonical Memo emission test for the transfer path
/// @dev Memo is self-contained: carries from, to, amount, memo, and a unique nonce.
/// nonce is 0 for the first memo'd operation on a freshly-etched token.
function test_transferWithMemo_success_emitsTransferThenMemo(
address from,
address to,
Expand All @@ -57,8 +58,8 @@ contract B20TransferWithMemoTest is B20Test {

vm.expectEmit(true, true, false, true, address(token));
emit IB20.Transfer(from, to, amount);
vm.expectEmit(true, false, false, false, address(token));
emit IB20.Memo(memo);
vm.expectEmit(address(token));
emit IB20.Memo(from, to, amount, memo, 0);
vm.prank(from);
token.transferWithMemo(to, amount, memo);
}
Expand Down