Roman Zajic 65f9e58df9
chore: adversarial tests (#42)
* test: malicious upgrade drains funds

* fix: formatting

* test: show success when unauthorized upgrade after malicious

* test: offchain proof post lazy erase
- multi-user erase reuse race

* fix: line length

* fix: remove offchain lazy erase test - rate limit still applies

* test: timestamp manipulation

* fix: rename tests

* test: front running for registration

* fix: unused variables

* test: register during spam conditions

* fix: delete failing tests
- test_MaliciousUpgradeDrainsFunds
- testFrontrunning_RegistrationRevertsForVictim
- testFrontrunning_SetFillingSpam

* fix: delete MaliciousImplementation

* fix: formatting with a new Foundry version

* test: testEraseAndReuse with Echidna

* fix: remove limit check

* fix: remove test_MultiUserEraseReuseRace
- test_TimestampManipulationRaces

* fix: skip Echidna contract during forge test

* test: Echidna contract with invariants
- registerMembership
- attemptExtensionRace
- attemptErasureRace

* fix: tune config file

* fix: run and cleanup scripts for echidna

* test: Echidna test replay

* fix: Solidity version

* fix: test_attemptExtensionRace_WakuRLN

* fix: invalid commitment in test_attemptExtensionRace_WakuRLN

* fix: invalid commitments in
test_attemptErasureRace_WakuRLN

* fix: line length

* fix: skip all Echidna tests in CI

* chore: fuzz test expansion (#40)

* test: register invalid

* test: multiple registers

* fix: increase max rejects

* test: erasure with fullErase idCommitments

* fix: reduce cyclomatic complexity

* fix: reduce complexity one step less

* fix: run tests in parallel

* fix: undo run tests in parallel - default already

* test: invalid extension with extreme values

* fix: line length

* test: set MaxTotalRateLimit

* test: set ActiveDuration

* test: Merkle inserts

* test: Merkle erasures

* test: GetRateCommitmentsRange

* test: GetMerkleProof

* fix: optimized MerkleInsert MerkleErasures

* fix: update gas snapshot

* test: malicious upgrade drains funds

* fix: formatting

* test: show success when unauthorized upgrade after malicious

* test: offchain proof post lazy erase
- multi-user erase reuse race

* fix: line length

* fix: remove offchain lazy erase test - rate limit still applies

* fix: remove fuzz tests from CI run

* fix: formatting

* fix: formatting coverage

* test: timestamp manipulation

* fix: rename tests

* test: front running for registration

* fix: unused variables

* test: register during spam conditions

* fix: delete failing tests
- test_MaliciousUpgradeDrainsFunds
- testFrontrunning_RegistrationRevertsForVictim
- testFrontrunning_SetFillingSpam

* fix: delete MaliciousImplementation

* fix: formatting with a new Foundry version

* test: testEraseAndReuse with Echidna

* fix: remove limit check

* fix: remove test_MultiUserEraseReuseRace
- test_TimestampManipulationRaces

* fix: skip Echidna contract during forge test

* test: Echidna contract with invariants
- registerMembership
- attemptExtensionRace
- attemptErasureRace

* fix: tune config file

* fix: run and cleanup scripts for echidna

* test: Echidna test replay

* fix: Solidity version

* fix: test_attemptExtensionRace_WakuRLN

* fix: invalid commitment in test_attemptExtensionRace_WakuRLN

* fix: invalid commitments in
test_attemptErasureRace_WakuRLN

* fix: line length

* fix: skip all Echidna tests in CI

* test: register invalid

* test: multiple registers

* fix: increase max rejects

* test: erasure with fullErase idCommitments

* fix: reduce cyclomatic complexity

* fix: reduce complexity one step less

* test: invalid extension with extreme values

* fix: line length

* test: set MaxTotalRateLimit

* test: set ActiveDuration

* test: Merkle inserts

* test: Merkle erasures

* test: GetRateCommitmentsRange

* test: GetMerkleProof

* fix: optimized MerkleInsert MerkleErasures

* fix: update gas snapshot

* fix: formatting

* fix: remove tests with high overlap

* fix: remove all tests originally meant for fuzzing

* fix: rename merged Echidna tests

* fix: formatting

* test: fuzzing for essential invariants

* test: EchidnaTest contract

* fix: remove unnecessary imports

* fix: remove unnecessary helpers

* fix: remove bounds from invariants

* fix: change test mode to property

* fix: update run script

* fix: max_test_rejects back to the original value

* fix: remove unused local variables

* test: malicious upgrade drains funds

* fix: formatting

* test: show success when unauthorized upgrade after malicious

* test: offchain proof post lazy erase
- multi-user erase reuse race

* fix: line length

* fix: remove offchain lazy erase test - rate limit still applies

* test: timestamp manipulation

* fix: rename tests

* test: front running for registration

* fix: unused variables

* test: register during spam conditions

* fix: delete failing tests
- test_MaliciousUpgradeDrainsFunds
- testFrontrunning_RegistrationRevertsForVictim
- testFrontrunning_SetFillingSpam

* fix: delete MaliciousImplementation

* fix: remove test_MultiUserEraseReuseRace
- test_TimestampManipulationRaces

* test: Echidna test replay

* fix: Solidity version

* fix: test_attemptExtensionRace_WakuRLN

* fix: invalid commitment in test_attemptExtensionRace_WakuRLN

* fix: invalid commitments in
test_attemptErasureRace_WakuRLN

* fix: line length

* fix: cleanup after rebase

* fix: remove redundant file

* fix: formatting

* fix: formatting

* fix: adorno + archive EchidnaReplayRaces.t.sol

* test: focus on erasures with timestamps

* fix: remove isolated test

* test: Echidna tests for races
- add dynamic assertions before operation
- untrack erased IDs

* fix: remove unused replay test
2025-11-14 19:16:11 +08:00
..
2024-11-01 16:22:15 -04:00
2025-11-14 19:16:11 +08:00

TestStableToken

The waku-rlnv2-contract spec 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). 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)

Prerequisites

  • Foundry installed
  • An Ethereum account with testnet ETH for deploying contracts and sending transactions

Usage

Add environment variable MAX_SUPPLY to set the maximum supply of the token, otherwise it defaults to 1 million tokens.

Deploy new TestStableToken with proxy contract

This script deploys both the proxy contract and the TestStableToken implementation contract, initializing the proxy to point to the new implementation.

ETH_FROM=$DEPLOYER_ACCOUNT_ADDRESS forge script script/DeployTokenWithProxy.s.sol:DeployTokenWithProxy --rpc-url $RPC_URL --broadcast --private_key $DEPLOYER_ACCOUNT_PRIVATE_KEY

or

MNEMONIC=$TWELVE_WORD_MNEMONIC forge script script/DeployTokenWithProxy.s.sol:DeployTokenWithProxy --rpc-url $RPC_URL --broadcast

Deploy only TestStableToken implementation contract

This script deploys only the TestStableToken implementation. See the upgrade instructions: Upgrade the proxy

ETH_FROM=$DEPLOYER_ACCOUNT_ADDRESS 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

When the proxy is already initialized and the maxSupply is set, you can simply update the implementation address using upgradeTo(address).

cast send $TOKEN_PROXY_ADDRESS "upgradeTo(address)" $NEW_IMPLEMENTATION_ADDRESS --rpc-url $RPC_URL --private-key $DEPLOYER_ACCOUNT_PRIVATE_KEY

Option 2: Update proxy implementation address and initialize cap

When upgrading a UUPS/ERC1967 proxy you should perform the upgrade and initialization in the same transaction to avoid leaving the proxy in an uninitialized state. Use upgradeToAndCall(address,bytes) with the initializer calldata.

# Encode the initializer calldata (example: set MAX_SUPPLY to 1_000_000 ETH = 1_000_000 * 10**18)
DATA=$(cast abi-encode "initialize(uint256)" 1000000000000000000000000)

# Perform upgrade and call initializer atomically
cast send $TOKEN_PROXY_ADDRESS "upgradeToAndCall(address,bytes)" $NEW_IMPLEMENTATION_ADDRESS $DATA --rpc-url $RPC_URL --private-key $DEPLOYER_ACCOUNT_PRIVATE_KEY

If you must call upgradeTo separately (not recommended), follow up immediately with an initialize(...) call in the same transaction or as the next transaction from the owner/multisig. However, prefer upgradeToAndCall to eliminate the time window where the proxy points to a new implementation but its storage (e.g., maxSupply) is uninitialized.

Add account to the allowlist to enable minting

cast send $TOKEN_PROXY_ADDRESS "addMinter(address)" $ACCOUNT_ADDRESS --rpc-url $RPC_URL --private-key $DEPLOYER_ACCOUNT_PRIVATE_KEY

Mint tokens to the account

Option 1: Restricted minting (requires minter privileges)

cast send $TOKEN_PROXY_ADDRESS "mint(address,uint256)" <TO_ADDRESS> <AMOUNT> --rpc-url $RPC_URL --private-key $MINTER_ACCOUNT_PRIVATE_KEY

Option 2: Public minting by burning ETH (no privileges required)

The total tokens minted is determined by the amount of ETH sent with the transaction. There is a lower limit to the amount that can be minted in a single transaction to prevent spam.

cast send $TOKEN_PROXY_ADDRESS "mintWithETH(address)" <TO_ACCOUNT> --value <ETH_AMOUNT> --rpc-url $RPC_URL --private-key $MINTING_ACCOUNT_PRIVATE_KEY --from $MINTING_ACCOUNT_ADDRESS

Note: The mintWithETH function is public and can be called by anyone. It requires sending ETH with the transaction (using --value), which gets burned (sent to address(0)) as an economic cost for minting tokens. This provides a permissionless way to obtain tokens for testing without requiring minter privileges.

Approve the token for the waku-rlnv2-contract to use

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

cast send $TOKEN_PROXY_ADDRESS "removeMinter(address)" $ACCOUNT_ADDRESS --rpc-url $RPC_URL --private-key $DEPLOYER_ACCOUNT_PRIVATE_KEY

Query token information

# 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