diff --git a/test/TestStableToken.sol b/test/TestStableToken.sol index c546b4e..f1b257d 100644 --- a/test/TestStableToken.sol +++ b/test/TestStableToken.sol @@ -6,10 +6,36 @@ import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; import { ERC20Permit } from "@openzeppelin/contracts/token/ERC20/extensions/ERC20Permit.sol"; import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol"; +error AccountNotMinter(); +error AccountAlreadyMinter(); +error AccountNotInMinterList(); + contract TestStableToken is ERC20, ERC20Permit, Ownable { + mapping(address => bool) public isMinter; + + event MinterAdded(address indexed account); + event MinterRemoved(address indexed account); + + modifier onlyOwnerOrMinter() { + if (msg.sender != owner() && !isMinter[msg.sender]) revert AccountNotMinter(); + _; + } + constructor() ERC20("TestStableToken", "TST") ERC20Permit("TestStableToken") Ownable() { } - function mint(address to, uint256 amount) external onlyOwner { + function addMinter(address account) external onlyOwner { + if (isMinter[account]) revert AccountAlreadyMinter(); + isMinter[account] = true; + emit MinterAdded(account); + } + + function removeMinter(address account) external onlyOwner { + if (!isMinter[account]) revert AccountNotInMinterList(); + isMinter[account] = false; + emit MinterRemoved(account); + } + + function mint(address to, uint256 amount) external onlyOwnerOrMinter { _mint(to, amount); } } diff --git a/test/TestStableToken.t.sol b/test/TestStableToken.t.sol new file mode 100644 index 0000000..2fde09a --- /dev/null +++ b/test/TestStableToken.t.sol @@ -0,0 +1,176 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.19 <0.9.0; + +import { Test } from "forge-std/Test.sol"; +import { TestStableToken, AccountNotMinter, AccountAlreadyMinter, AccountNotInMinterList } from "./TestStableToken.sol"; + +contract TestStableTokenTest is Test { + TestStableToken internal token; + address internal owner; + address internal user1; + address internal user2; + address internal nonMinter; + + function setUp() public { + token = new TestStableToken(); + owner = address(this); + user1 = vm.addr(1); + user2 = vm.addr(2); + nonMinter = vm.addr(3); + } + + function test__OwnerCanAddMinterRole() external { + assertFalse(token.isMinter(user1)); + + token.addMinter(user1); + + assertTrue(token.isMinter(user1)); + } + + function test__OwnerCanRemoveMinterRole() external { + token.addMinter(user1); + assertTrue(token.isMinter(user1)); + + token.removeMinter(user1); + + assertFalse(token.isMinter(user1)); + } + + function test__OwnerCanMintWithoutMinterRole() external { + uint256 mintAmount = 1000 ether; + + token.mint(user1, mintAmount); + + assertEq(token.balanceOf(user1), mintAmount); + } + + function test__NonOwnerCannotAddMinterRole() external { + vm.prank(user1); + vm.expectRevert("Ownable: caller is not the owner"); + token.addMinter(user1); + } + + function test__NonOwnerCannotRemoveMinterRole() external { + token.addMinter(user1); + + vm.prank(user1); + vm.expectRevert("Ownable: caller is not the owner"); + token.removeMinter(user1); + } + + function test__CannotAddAlreadyMinterRole() external { + token.addMinter(user1); + + vm.expectRevert(abi.encodeWithSelector(AccountAlreadyMinter.selector)); + token.addMinter(user1); + } + + function test__CannotRemoveNonMinterRole() external { + vm.expectRevert(abi.encodeWithSelector(AccountNotInMinterList.selector)); + token.removeMinter(user1); + } + + function test__MinterRoleCanMint() external { + uint256 mintAmount = 1000 ether; + token.addMinter(user1); + + vm.prank(user1); + token.mint(user2, mintAmount); + + assertEq(token.balanceOf(user2), mintAmount); + } + + function test__NonMinterNonOwnerAccountCannotMint() external { + uint256 mintAmount = 1000 ether; + + vm.prank(nonMinter); + vm.expectRevert(abi.encodeWithSelector(AccountNotMinter.selector)); + token.mint(user1, mintAmount); + } + + function test__MultipleMinterRolesCanMint() external { + uint256 mintAmount = 500 ether; + token.addMinter(user1); + token.addMinter(user2); + + vm.prank(user1); + token.mint(owner, mintAmount); + + vm.prank(user2); + token.mint(owner, mintAmount); + + assertEq(token.balanceOf(owner), mintAmount * 2); + } + + function test__RemovedMinterRoleCannotMint() external { + uint256 mintAmount = 1000 ether; + token.addMinter(user1); + token.removeMinter(user1); + + vm.prank(user1); + vm.expectRevert(abi.encodeWithSelector(AccountNotMinter.selector)); + token.mint(user2, mintAmount); + } + + function test__OwnerCanAlwaysMintEvenWithoutMinterRole() external { + uint256 mintAmount = 500 ether; + + // Owner is not in minter role but should still be able to mint + assertFalse(token.isMinter(address(this))); + token.mint(user1, mintAmount); + assertEq(token.balanceOf(user1), mintAmount); + } + + function test__CheckMinterRoleMapping() external { + assertFalse(token.isMinter(user1)); + assertFalse(token.isMinter(user2)); + + token.addMinter(user1); + assertTrue(token.isMinter(user1)); + assertFalse(token.isMinter(user2)); + + token.addMinter(user2); + assertTrue(token.isMinter(user1)); + assertTrue(token.isMinter(user2)); + + token.removeMinter(user1); + assertFalse(token.isMinter(user1)); + assertTrue(token.isMinter(user2)); + } + + function test__ERC20BasicFunctionality() external { + token.addMinter(user1); + uint256 mintAmount = 1000 ether; + + vm.prank(user1); + token.mint(user2, mintAmount); + + assertEq(token.balanceOf(user2), mintAmount); + assertEq(token.totalSupply(), mintAmount); + + vm.prank(user2); + token.transfer(owner, 200 ether); + + assertEq(token.balanceOf(user2), 800 ether); + assertEq(token.balanceOf(owner), 200 ether); + } + + function test__MinterAddedEventEmitted() external { + vm.expectEmit(true, true, false, false); + emit MinterAdded(user1); + + token.addMinter(user1); + } + + function test__MinterRemovedEventEmitted() external { + token.addMinter(user1); + + vm.expectEmit(true, true, false, false); + emit MinterRemoved(user1); + + token.removeMinter(user1); + } + + event MinterAdded(address indexed account); + event MinterRemoved(address indexed account); +} diff --git a/test/WakuRlnV2.t.sol b/test/WakuRlnV2.t.sol index 9c6d7c7..ce02e79 100644 --- a/test/WakuRlnV2.t.sol +++ b/test/WakuRlnV2.t.sol @@ -763,39 +763,6 @@ contract WakuRlnV2Test is Test { } } - function test__TestStableToken__OnlyOwnerCanMint() external { - address nonOwner = vm.addr(1); - uint256 mintAmount = 1000 ether; - - vm.prank(nonOwner); - vm.expectRevert("Ownable: caller is not the owner"); - token.mint(nonOwner, mintAmount); - } - - function test__TestStableToken__OwnerMintsTransfersAndRegisters() external { - address recipient = vm.addr(2); - uint256 idCommitment = 3; - uint32 membershipRateLimit = w.minMembershipRateLimit(); - (, uint256 price) = w.priceCalculator().calculate(membershipRateLimit); - - // Owner (test contract) mints tokens to recipient - token.mint(recipient, price); - assertEq(token.balanceOf(recipient), price); - - // Recipient uses tokens to register - vm.startPrank(recipient); - token.approve(address(w), price); - w.register(idCommitment, membershipRateLimit, noIdCommitmentsToErase); - vm.stopPrank(); - - // Verify registration succeeded - assertTrue(w.isInMembershipSet(idCommitment)); - (,,,, uint32 fetchedMembershipRateLimit, uint32 index, address holder,) = w.memberships(idCommitment); - assertEq(fetchedMembershipRateLimit, membershipRateLimit); - assertEq(holder, recipient); - assertEq(index, 0); - } - function test__Upgrade() external { address testImpl = address(new WakuRlnV2()); bytes memory data = abi.encodeCall(WakuRlnV2.initialize, (address(0), 100, 1, 10, 10 minutes, 4 minutes));