From b4508dd0d4a86891e5a33584da708c6ea2f34a8f Mon Sep 17 00:00:00 2001 From: Tanya S <120410716+stubbsta@users.noreply.github.com> Date: Tue, 26 Aug 2025 17:34:32 +0200 Subject: [PATCH] Use proxy for token contract (#30) * Add proxy contract for TST * Fix token proxy update function to use provided new TST address * Transfer token proxy contract ownership to deployer * Add Token Proxy Contract Owner as init input * Add UUPSUPgradeable to TST * Formatting * fix import format * Add README to explain TST usage * Linting fix * Check TST test transfer return val * Add descriptions in README for TST usage * Fix linting * Use TST token deployer in test conrtact, update test README * USe assertTrue in TST test --- script/Deploy.s.sol | 2 +- script/DeployTokenWithProxy.s.sol | 23 ++++++++ test/README.md | 90 +++++++++++++++++++++++++++++++ test/TestStableToken.sol | 30 +++++++++-- test/TestStableToken.t.sol | 38 +++++++++++-- test/WakuRlnV2.t.sol | 12 ++++- 6 files changed, 183 insertions(+), 12 deletions(-) create mode 100644 script/DeployTokenWithProxy.s.sol create mode 100644 test/README.md diff --git a/script/Deploy.s.sol b/script/Deploy.s.sol index 7b926ef..1da8735 100644 --- a/script/Deploy.s.sol +++ b/script/Deploy.s.sol @@ -9,7 +9,7 @@ import { LazyIMT } from "@zk-kit/imt.sol/LazyIMT.sol"; import { ERC1967Proxy } from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; import { DevOpsTools } from "lib/foundry-devops/src/DevOpsTools.sol"; import { BaseScript } from "./Base.s.sol"; -import "forge-std/console.sol"; +import { console } from "forge-std/console.sol"; contract DeployPriceCalculator is BaseScript { function run() public broadcast returns (address) { diff --git a/script/DeployTokenWithProxy.s.sol b/script/DeployTokenWithProxy.s.sol new file mode 100644 index 0000000..660eb71 --- /dev/null +++ b/script/DeployTokenWithProxy.s.sol @@ -0,0 +1,23 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.19 <0.9.0; + +import { BaseScript } from "./Base.s.sol"; +import { TestStableToken } from "../test/TestStableToken.sol"; +import { ERC1967Proxy } from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; + +contract DeployTokenWithProxy is BaseScript { + function run() public broadcast returns (address) { + return address(deploy()); + } + + function deploy() public returns (ERC1967Proxy) { + // Deploy the initial implementation + address implementation = address(new TestStableToken()); + + // Encode the initialize call + bytes memory data = abi.encodeCall(TestStableToken.initialize, ()); + + // Deploy the proxy with initialization data + return new ERC1967Proxy(implementation, data); + } +} diff --git a/test/README.md b/test/README.md new file mode 100644 index 0000000..1fe358f --- /dev/null +++ b/test/README.md @@ -0,0 +1,90 @@ +# TestStableToken + +The waku-rlnv2-contract [spec](https://github.com/waku-org/specs/blob/master/standards/core/rln-contract.md) defines +that DAI is to be used to pay for membership registration, with the end-goal being to deploy the contract on mainnet +using an existing stable DAI token. + +Before this, we need to perform extensive testing on testnet and local environments (such as +[waku-simulator](https://github.com/waku-org/waku-simulator)). During initial testing, we discovered the need to manage +token minting in testnet environments to limit membership registrations and enable controlled testing of the contract. + +TestStableToken is our custom token implementation designed specifically for testing environments, providing controlled +token distribution while mimicking DAI's behaviour. + +## Requirements + +- **Controlled minting**: Manage token minting through an allowlist of approved accounts, controlled by the token + contract owner +- **ETH burning mechanism**: Burn ETH when minting tokens to create economic cost (WIP) +- **Proxy architecture**: Use a proxy contract to minimize updates required when the token address changes across other + components (e.g., nwaku-compose repo or dogfooding instructions) + +## Usage + +### Deploy new TestStableToken with proxy contract + +This script deploys both the proxy and the TestStableToken implementation, initializing the proxy to point to the new +implementation. + +```bash +ETH_FROM=$DEPLOYER_ACCOUNT_ADDRESS forge script script/DeployTokenWithProxy.s.sol:DeployTokenWithProxy --rpc-url $RPC_URL --broadcast --private_key $DEPLOYER_ACCOUNT_PRIVATE_KEY +``` + +or + +```bash +MNEMONIC=$TWELVE_WORD_MNEMONIC forge script script/DeployTokenWithProxy.s.sol:DeployTokenWithProxy --rpc-url $RPC_URL --broadcast +``` + +### Deploy only TestStableToken contract implementation + +This script deploys only the TestStableToken implementation, which can then be used to update the proxy contract to +point to this new implementation. + +```bash +forge script test/TestStableToken.sol:TestStableTokenFactory --tc TestStableTokenFactory --rpc-url $RPC_URL --private-key $DEPLOYER_ACCOUNT_PRIVATE_KEY --broadcast +``` + +### Update the proxy contract to point to the new implementation + +```bash +# Upgrade the proxy to a new implementation +cast send $TOKEN_PROXY_ADDRESS "upgradeTo(address)" $NEW_IMPLEMENTATION_ADDRESS --rpc-url $RPC_URL --private-key $DEPLOYER_ACCOUNT_PRIVATE_KEY +``` + +### Add account to the allowlist to enable minting + +```bash +cast send $TOKEN_PROXY_ADDRESS "addMinter(address)" $ACCOUNT_ADDRESS --rpc-url $RPC_URL --private-key $DEPLOYER_ACCOUNT_PRIVATE_KEY +``` + +### Mint tokens to the account + +```bash +cast send $TOKEN_PROXY_ADDRESS "mint(address,uint256)" --rpc-url $RPC_URL --private-key $MINTER_ACCOUNT_PRIVATE_KEY +``` + +### Approve the token for the waku-rlnv2-contract to use + +```bash +cast send $TOKEN_PROXY_ADDRESS "approve(address,uint256)" $TOKEN_SPENDER_ADDRESS --rpc-url $RPC_URL --private-key $PRIVATE_KEY +``` + +### Remove the account from the allowlist to prevent further minting + +```bash +cast send $TOKEN_PROXY_ADDRESS "removeMinter(address)" $ACCOUNT_ADDRESS --rpc-url $RPC_URL --private-key $DEPLOYER_ACCOUNT_PRIVATE_KEY +``` + +### Query token information + +```bash +# Check if an account is a minter +cast call $TOKEN_PROXY_ADDRESS "isMinter(address)" $ACCOUNT_ADDRESS --rpc-url $RPC_URL + +# Check token balance +cast call $TOKEN_PROXY_ADDRESS "balanceOf(address)" $ACCOUNT_ADDRESS --rpc-url $RPC_URL + +# Check token allowance +cast call $TOKEN_PROXY_ADDRESS "allowance(address,address)" $TOKEN_OWNER_ADDRESS $TOKEN_SPENDER_ADDRESS --rpc-url $RPC_URL +``` diff --git a/test/TestStableToken.sol b/test/TestStableToken.sol index f1b257d..13fd482 100644 --- a/test/TestStableToken.sol +++ b/test/TestStableToken.sol @@ -2,15 +2,24 @@ pragma solidity >=0.8.19 <0.9.0; import { BaseScript } from "../script/Base.s.sol"; -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"; +import { ERC20Upgradeable } from "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol"; +import { ERC20PermitUpgradeable } from + "@openzeppelin/contracts-upgradeable/token/ERC20/extensions/ERC20PermitUpgradeable.sol"; +import { OwnableUpgradeable } from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; +import { Initializable } from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; +import { UUPSUpgradeable } from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; error AccountNotMinter(); error AccountAlreadyMinter(); error AccountNotInMinterList(); -contract TestStableToken is ERC20, ERC20Permit, Ownable { +contract TestStableToken is + Initializable, + ERC20Upgradeable, + ERC20PermitUpgradeable, + OwnableUpgradeable, + UUPSUpgradeable +{ mapping(address => bool) public isMinter; event MinterAdded(address indexed account); @@ -21,7 +30,18 @@ contract TestStableToken is ERC20, ERC20Permit, Ownable { _; } - constructor() ERC20("TestStableToken", "TST") ERC20Permit("TestStableToken") Ownable() { } + constructor() { + _disableInitializers(); + } + + function initialize() public initializer { + __ERC20_init("TestStableToken", "TST"); + __ERC20Permit_init("TestStableToken"); + __Ownable_init(); + __UUPSUpgradeable_init(); + } + + function _authorizeUpgrade(address newImplementation) internal override onlyOwner { } function addMinter(address account) external onlyOwner { if (isMinter[account]) revert AccountAlreadyMinter(); diff --git a/test/TestStableToken.t.sol b/test/TestStableToken.t.sol index 2fde09a..eff78c1 100644 --- a/test/TestStableToken.t.sol +++ b/test/TestStableToken.t.sol @@ -3,17 +3,26 @@ pragma solidity >=0.8.19 <0.9.0; import { Test } from "forge-std/Test.sol"; import { TestStableToken, AccountNotMinter, AccountAlreadyMinter, AccountNotInMinterList } from "./TestStableToken.sol"; +import { ERC1967Proxy } from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; +import { DeployTokenWithProxy } from "../script/DeployTokenWithProxy.s.sol"; contract TestStableTokenTest is Test { TestStableToken internal token; + DeployTokenWithProxy internal deployer; address internal owner; address internal user1; address internal user2; address internal nonMinter; function setUp() public { - token = new TestStableToken(); - owner = address(this); + // Deploy using the deployment script + deployer = new DeployTokenWithProxy(); + ERC1967Proxy proxy = deployer.deploy(); + + // Wrap proxy in TestStableToken interface + token = TestStableToken(address(proxy)); + + owner = address(deployer); user1 = vm.addr(1); user2 = vm.addr(2); nonMinter = vm.addr(3); @@ -22,15 +31,18 @@ contract TestStableTokenTest is Test { function test__OwnerCanAddMinterRole() external { assertFalse(token.isMinter(user1)); + vm.prank(owner); token.addMinter(user1); assertTrue(token.isMinter(user1)); } function test__OwnerCanRemoveMinterRole() external { + vm.prank(owner); token.addMinter(user1); assertTrue(token.isMinter(user1)); + vm.prank(owner); token.removeMinter(user1); assertFalse(token.isMinter(user1)); @@ -39,6 +51,7 @@ contract TestStableTokenTest is Test { function test__OwnerCanMintWithoutMinterRole() external { uint256 mintAmount = 1000 ether; + vm.prank(owner); token.mint(user1, mintAmount); assertEq(token.balanceOf(user1), mintAmount); @@ -51,6 +64,7 @@ contract TestStableTokenTest is Test { } function test__NonOwnerCannotRemoveMinterRole() external { + vm.prank(owner); token.addMinter(user1); vm.prank(user1); @@ -59,19 +73,23 @@ contract TestStableTokenTest is Test { } function test__CannotAddAlreadyMinterRole() external { + vm.prank(owner); token.addMinter(user1); vm.expectRevert(abi.encodeWithSelector(AccountAlreadyMinter.selector)); + vm.prank(owner); token.addMinter(user1); } function test__CannotRemoveNonMinterRole() external { vm.expectRevert(abi.encodeWithSelector(AccountNotInMinterList.selector)); + vm.prank(owner); token.removeMinter(user1); } function test__MinterRoleCanMint() external { uint256 mintAmount = 1000 ether; + vm.prank(owner); token.addMinter(user1); vm.prank(user1); @@ -90,7 +108,9 @@ contract TestStableTokenTest is Test { function test__MultipleMinterRolesCanMint() external { uint256 mintAmount = 500 ether; + vm.prank(owner); token.addMinter(user1); + vm.prank(owner); token.addMinter(user2); vm.prank(user1); @@ -104,7 +124,9 @@ contract TestStableTokenTest is Test { function test__RemovedMinterRoleCannotMint() external { uint256 mintAmount = 1000 ether; + vm.prank(owner); token.addMinter(user1); + vm.prank(owner); token.removeMinter(user1); vm.prank(user1); @@ -116,7 +138,8 @@ contract TestStableTokenTest is Test { uint256 mintAmount = 500 ether; // Owner is not in minter role but should still be able to mint - assertFalse(token.isMinter(address(this))); + assertFalse(token.isMinter(owner)); + vm.prank(owner); token.mint(user1, mintAmount); assertEq(token.balanceOf(user1), mintAmount); } @@ -125,20 +148,24 @@ contract TestStableTokenTest is Test { assertFalse(token.isMinter(user1)); assertFalse(token.isMinter(user2)); + vm.prank(owner); token.addMinter(user1); assertTrue(token.isMinter(user1)); assertFalse(token.isMinter(user2)); + vm.prank(owner); token.addMinter(user2); assertTrue(token.isMinter(user1)); assertTrue(token.isMinter(user2)); + vm.prank(owner); token.removeMinter(user1); assertFalse(token.isMinter(user1)); assertTrue(token.isMinter(user2)); } function test__ERC20BasicFunctionality() external { + vm.prank(owner); token.addMinter(user1); uint256 mintAmount = 1000 ether; @@ -149,7 +176,7 @@ contract TestStableTokenTest is Test { assertEq(token.totalSupply(), mintAmount); vm.prank(user2); - token.transfer(owner, 200 ether); + assertTrue(token.transfer(owner, 200 ether)); assertEq(token.balanceOf(user2), 800 ether); assertEq(token.balanceOf(owner), 200 ether); @@ -159,15 +186,18 @@ contract TestStableTokenTest is Test { vm.expectEmit(true, true, false, false); emit MinterAdded(user1); + vm.prank(owner); token.addMinter(user1); } function test__MinterRemovedEventEmitted() external { + vm.prank(owner); token.addMinter(user1); vm.expectEmit(true, true, false, false); emit MinterRemoved(user1); + vm.prank(owner); token.removeMinter(user1); } diff --git a/test/WakuRlnV2.t.sol b/test/WakuRlnV2.t.sol index ce02e79..b9f9d25 100644 --- a/test/WakuRlnV2.t.sol +++ b/test/WakuRlnV2.t.sol @@ -3,6 +3,7 @@ pragma solidity >=0.8.19 <0.9.0; import { Test } from "forge-std/Test.sol"; import { DeployPriceCalculator, DeployWakuRlnV2, DeployProxy } from "../script/Deploy.s.sol"; +import { DeployTokenWithProxy } from "../script/DeployTokenWithProxy.s.sol"; import "../src/WakuRlnV2.sol"; // solhint-disable-line import "../src/Membership.sol"; // solhint-disable-line import { IPriceCalculator } from "../src/IPriceCalculator.sol"; @@ -17,21 +18,27 @@ import "forge-std/console.sol"; contract WakuRlnV2Test is Test { WakuRlnV2 internal w; TestStableToken internal token; + DeployTokenWithProxy internal tokenDeployer; address internal deployer; uint256[] internal noIdCommitmentsToErase = new uint256[](0); function setUp() public virtual { - token = new TestStableToken(); + // Deploy TestStableToken through proxy using deployment script + tokenDeployer = new DeployTokenWithProxy(); + ERC1967Proxy tokenProxy = tokenDeployer.deploy(); + token = TestStableToken(address(tokenProxy)); + IPriceCalculator priceCalculator = (new DeployPriceCalculator()).deploy(address(token)); WakuRlnV2 wakuRlnV2 = (new DeployWakuRlnV2()).deploy(); ERC1967Proxy proxy = (new DeployProxy()).deploy(address(priceCalculator), address(wakuRlnV2)); w = WakuRlnV2(address(proxy)); - // TestStableTokening a large number of tokens to not have to worry about + // Minting a large number of tokens to not have to worry about // Not having enough balance + vm.prank(address(tokenDeployer)); token.mint(address(this), 100_000_000 ether); } @@ -634,6 +641,7 @@ contract WakuRlnV2Test is Test { vm.prank(priceCalculator.owner()); priceCalculator.setTokenAndPrice(address(token), 5 wei); (, uint256 price) = w.priceCalculator().calculate(membershipRateLimit); + vm.prank(address(tokenDeployer)); token.mint(address(this), price); vm.assume( w.minMembershipRateLimit() <= membershipRateLimit && membershipRateLimit <= w.maxMembershipRateLimit()