From 0ea8e1d7a919e2e4b5315fcb24589cd9f9400857 Mon Sep 17 00:00:00 2001 From: stubbsta Date: Thu, 4 Sep 2025 10:55:11 +0200 Subject: [PATCH] Add mintWithEth function to TST to burn Eth --- test/TestStableToken.sol | 15 ++++ test/TestStableToken.t.sol | 140 +++++++++++++++++++++++++++++-------- test/WakuRlnV2.t.sol | 42 +++++++++++ 3 files changed, 168 insertions(+), 29 deletions(-) diff --git a/test/TestStableToken.sol b/test/TestStableToken.sol index 13fd482..4c0be30 100644 --- a/test/TestStableToken.sol +++ b/test/TestStableToken.sol @@ -12,6 +12,8 @@ import { UUPSUpgradeable } from "@openzeppelin/contracts-upgradeable/proxy/utils error AccountNotMinter(); error AccountAlreadyMinter(); error AccountNotInMinterList(); +error InsufficientETH(); +error ETHTransferFailed(); contract TestStableToken is Initializable, @@ -24,6 +26,7 @@ contract TestStableToken is event MinterAdded(address indexed account); event MinterRemoved(address indexed account); + event ETHBurned(uint256 amount, address indexed minter, address indexed to, uint256 tokensMinted); modifier onlyOwnerOrMinter() { if (msg.sender != owner() && !isMinter[msg.sender]) revert AccountNotMinter(); @@ -58,6 +61,18 @@ contract TestStableToken is function mint(address to, uint256 amount) external onlyOwnerOrMinter { _mint(to, amount); } + + function mintWithETH(address to, uint256 amount) external payable { + if (msg.value == 0) revert InsufficientETH(); + + // Burn ETH by sending to zero address + (bool success,) = payable(address(0)).call{ value: msg.value }(""); + if (!success) revert ETHTransferFailed(); + + _mint(to, amount); + + emit ETHBurned(msg.value, msg.sender, to, amount); + } } contract TestStableTokenFactory is BaseScript { diff --git a/test/TestStableToken.t.sol b/test/TestStableToken.t.sol index eff78c1..0f81719 100644 --- a/test/TestStableToken.t.sol +++ b/test/TestStableToken.t.sol @@ -2,7 +2,14 @@ pragma solidity >=0.8.19 <0.9.0; import { Test } from "forge-std/Test.sol"; -import { TestStableToken, AccountNotMinter, AccountAlreadyMinter, AccountNotInMinterList } from "./TestStableToken.sol"; +import { + TestStableToken, + AccountNotMinter, + AccountAlreadyMinter, + AccountNotInMinterList, + InsufficientETH, + ETHTransferFailed +} from "./TestStableToken.sol"; import { ERC1967Proxy } from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; import { DeployTokenWithProxy } from "../script/DeployTokenWithProxy.s.sol"; @@ -51,6 +58,9 @@ contract TestStableTokenTest is Test { function test__OwnerCanMintWithoutMinterRole() external { uint256 mintAmount = 1000 ether; + // Owner is not in minter role but should still be able to mint + assertFalse(token.isMinter(owner)); + vm.prank(owner); token.mint(user1, mintAmount); @@ -134,16 +144,6 @@ contract TestStableTokenTest is Test { 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(owner)); - vm.prank(owner); - token.mint(user1, mintAmount); - assertEq(token.balanceOf(user1), mintAmount); - } - function test__CheckMinterRoleMapping() external { assertFalse(token.isMinter(user1)); assertFalse(token.isMinter(user2)); @@ -164,24 +164,6 @@ contract TestStableTokenTest is Test { assertTrue(token.isMinter(user2)); } - function test__ERC20BasicFunctionality() external { - vm.prank(owner); - 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); - assertTrue(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); @@ -203,4 +185,104 @@ contract TestStableTokenTest is Test { event MinterAdded(address indexed account); event MinterRemoved(address indexed account); + event ETHBurned(uint256 amount, address indexed minter, address indexed to, uint256 tokensMinted); + + // New tests for ETH burning functionality + function test__MintRequiresETH() external { + uint256 mintAmount = 1000 ether; + + vm.prank(owner); + vm.expectRevert(abi.encodeWithSelector(InsufficientETH.selector)); + token.mintWithETH(user1, mintAmount); + } + + function test__ERC20BasicFunctionality() external { + uint256 mintAmount = 1000 ether; + uint256 ethAmount = 0.1 ether; + + vm.deal(user1, ethAmount); + vm.prank(user1); + token.mintWithETH{ value: ethAmount }(user2, mintAmount); + + assertEq(token.balanceOf(user2), mintAmount); + assertEq(token.totalSupply(), mintAmount); + + vm.prank(user2); + assertTrue(token.transfer(owner, 200 ether)); + + assertEq(token.balanceOf(user2), 800 ether); + assertEq(token.balanceOf(owner), 200 ether); + } + + function test__ETHBurnedEventEmitted() external { + uint256 mintAmount = 1000 ether; + uint256 ethAmount = 0.1 ether; + + vm.deal(owner, ethAmount); + + vm.expectEmit(true, true, true, true); + emit ETHBurned(ethAmount, owner, user1, mintAmount); + + vm.prank(owner); + token.mintWithETH{ value: ethAmount }(user1, mintAmount); + } + + function test__ETHIsBurnedToZeroAddress() external { + uint256 mintAmount = 1000 ether; + uint256 ethAmount = 0.1 ether; + address zeroAddress = address(0); + + uint256 zeroBalanceBefore = zeroAddress.balance; + + vm.deal(owner, ethAmount); + vm.prank(owner); + token.mintWithETH{ value: ethAmount }(user1, mintAmount); + + // ETH should be burned to zero address + assertEq(zeroAddress.balance, zeroBalanceBefore + ethAmount); + } + + function test__ContractDoesNotHoldETHAfterMint() external { + uint256 mintAmount = 1000 ether; + uint256 ethAmount = 0.1 ether; + + uint256 contractBalanceBefore = address(token).balance; + + vm.deal(owner, ethAmount); + vm.prank(owner); + token.mintWithETH{ value: ethAmount }(user1, mintAmount); + + // Contract should not hold any ETH after mint + assertEq(address(token).balance, contractBalanceBefore); + } + + function test__MintWithDifferentETHAmounts() external { + uint256 mintAmount = 1000 ether; + uint256[] memory ethAmounts = new uint256[](3); + ethAmounts[0] = 0.01 ether; + ethAmounts[1] = 1 ether; + ethAmounts[2] = 10 ether; + + for (uint256 i = 0; i < ethAmounts.length; i++) { + address user = vm.addr(i + 10); + vm.deal(owner, ethAmounts[i]); + + vm.expectEmit(true, true, true, true); + emit ETHBurned(ethAmounts[i], owner, user, mintAmount); + + vm.prank(owner); + token.mintWithETH{ value: ethAmounts[i] }(user, mintAmount); + + assertEq(token.balanceOf(user), mintAmount); + } + } + + function test__CannotMintWithZeroETH() external { + uint256 mintAmount = 1000 ether; + + // Anyone can call mintWithETH (public function), but it requires ETH + vm.prank(user1); + vm.expectRevert(abi.encodeWithSelector(InsufficientETH.selector)); + token.mintWithETH{ value: 0 }(user2, mintAmount); + } } diff --git a/test/WakuRlnV2.t.sol b/test/WakuRlnV2.t.sol index b9f9d25..572fb7a 100644 --- a/test/WakuRlnV2.t.sol +++ b/test/WakuRlnV2.t.sol @@ -634,6 +634,48 @@ contract WakuRlnV2Test is Test { } } + function test__NonMinterCanMintWithETHAndRegister() external { + uint256 idCommitment = 123; + uint32 membershipRateLimit = w.minMembershipRateLimit(); + address nonMinter = vm.addr(999); + + // Calculate required token amount for membership + (, uint256 price) = w.priceCalculator().calculate(membershipRateLimit); + uint256 ethAmount = price; // Use same amount of ETH as token price needed + + // Verify nonMinter is not a minter + assertFalse(token.isMinter(nonMinter)); + + // Non-minter uses mintWithETH to get tokens needed for membership + vm.deal(nonMinter, ethAmount); + vm.prank(nonMinter); + token.mintWithETH{ value: ethAmount }(nonMinter, price); + + // Verify tokens were minted + assertEq(token.balanceOf(nonMinter), price); + + // Non-minter approves and registers for membership + vm.startPrank(nonMinter); + token.approve(address(w), price); + w.register(idCommitment, membershipRateLimit, noIdCommitmentsToErase); + vm.stopPrank(); + + // Verify successful registration + assertTrue(w.isInMembershipSet(idCommitment)); + (uint32 fetchedRateLimit, uint32 index, uint256 rateCommitment) = w.getMembershipInfo(idCommitment); + assertEq(fetchedRateLimit, membershipRateLimit); + assertEq(index, 0); + assertNotEq(rateCommitment, 0); + + // Verify membership holder is the non-minter + (,,,,,, address holder,) = w.memberships(idCommitment); + assertEq(holder, nonMinter); + + // Verify tokens were transferred to membership contract + assertEq(token.balanceOf(address(w)), price); + assertEq(token.balanceOf(nonMinter), 0); + } + function test__WithdrawToken(uint32 membershipRateLimit) external { vm.pauseGasMetering(); uint256 idCommitment = 2;