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
This commit is contained in:
Tanya S 2025-08-26 17:34:32 +02:00 committed by GitHub
parent 900d4f95e0
commit b4508dd0d4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 183 additions and 12 deletions

View File

@ -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) {

View File

@ -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);
}
}

90
test/README.md Normal file
View File

@ -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)" <TO_ADDRESS> <AMOUNT> --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 <AMOUNT> --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
```

View File

@ -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();

View File

@ -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);
}

View File

@ -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()