diff --git a/script/DeployTokenWithProxy.s.sol b/script/DeployTokenWithProxy.s.sol index c61561f..d5e441d 100644 --- a/script/DeployTokenWithProxy.s.sol +++ b/script/DeployTokenWithProxy.s.sol @@ -11,13 +11,16 @@ contract DeployTokenWithProxy is BaseScript { } function deploy() public returns (ERC1967Proxy) { + // Read desired max supply from env or use default + uint256 defaultMaxSupply = vm.envOr({ name: "MAX_SUPPLY", defaultValue: uint256(1_000_000 * 10 ** 18) }); + // Deploy the initial implementation address implementation = address(new TestStableToken()); - // Encode the initialize call - bytes memory data = abi.encodeCall(TestStableToken.initialize, (1_000_000 * 10 ** 18)); + // Encode the initialize call (maxSupply) + bytes memory initData = abi.encodeCall(TestStableToken.initialize, (defaultMaxSupply)); // Deploy the proxy with initialization data - return new ERC1967Proxy(implementation, data); + return new ERC1967Proxy(implementation, initData); } } diff --git a/test/.env.example.tst b/test/.env.example.tst new file mode 100644 index 0000000..c0f7a5b --- /dev/null +++ b/test/.env.example.tst @@ -0,0 +1,28 @@ +# Example environment variables for TestStableToken commands in test/README.md +# Either provide a private key (`DEPLOYER_ACCOUNT_PRIVATE_KEY`) or a mnemonic (`TWELVE_WORD_MNEMONIC`). + +# Deployer account (used as --from / ETH_FROM) +DEPLOYER_ACCOUNT_ADDRESS=0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +# Hex private key (prefixed with 0x) OR leave empty if you prefer to use mnemonic +DEPLOYER_ACCOUNT_PRIVATE_KEY=0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + +# Alternatively, use a mnemonic instead of a private key +TWELVE_WORD_MNEMONIC="test test test test test test test test test test test junk" + +# RPC URL for accessing testnet via HTTP. +# e.g. https://linea-sepolia.infura.io/v3/123aa110320f4aec179150fba1e1b1b1 +RPC_URL=https://linea-sepolia.infura.io/v3/ + +# Optional: override the default max supply (value is in wei; example below = 1_000_000 * 10**18) +# Uncomment and set to change the token cap used during initialize/upgrade +# MAX_SUPPLY=1000000000000000000000000 + +# Addresses used by various actions (leave commented if not applicable) +# Proxy contract (when calling upgrade, approve, mint, etc.) +# TOKEN_PROXY_ADDRESS=0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb + +# Example account to add to the minter allowlist +# ACCOUNT_ADDRESS=0xcccccccccccccccccccccccccccccccccccccccc + +# Private key for a minter account (used when sending mint transactions) +# MINTER_ACCOUNT_PRIVATE_KEY=0xcccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc \ No newline at end of file diff --git a/test/README.md b/test/README.md index 34ac908..b229fdd 100644 --- a/test/README.md +++ b/test/README.md @@ -19,12 +19,19 @@ token distribution while mimicking DAI's behaviour. - **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](https://getfoundry.sh/) 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 and the TestStableToken implementation, initializing the proxy to point to the new -implementation. +This script deploys both the proxy contract and the TestStableToken implementation contract, 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 @@ -36,22 +43,43 @@ or MNEMONIC=$TWELVE_WORD_MNEMONIC forge script script/DeployTokenWithProxy.s.sol:DeployTokenWithProxy --rpc-url $RPC_URL --broadcast ``` -### Deploy only TestStableToken contract implementation +### Deploy only TestStableToken implementation contract -This script deploys only the TestStableToken implementation, which can then be used to update the proxy contract to -point to this new implementation. +This script deploys only the TestStableToken implementation. See the upgrade instructions: +[Upgrade the proxy](#update-the-proxy-contract-to-point-to-the-new-implementation) ```bash -forge script test/TestStableToken.sol:TestStableTokenFactory --tc TestStableTokenFactory --rpc-url $RPC_URL --private-key $DEPLOYER_ACCOUNT_PRIVATE_KEY --broadcast +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 +#### Option 1: Update proxy implementation address only (recommended when the proxy is already initialized) + +When the proxy is already initialized and the `maxSupply` is set, you can simply update the implementation address using +`upgradeTo(address)`. + ```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 ``` +#### 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. + +```bash +# 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 ```bash @@ -68,8 +96,10 @@ cast send $TOKEN_PROXY_ADDRESS "mint(address,uint256)" --r #### 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. + ```bash -cast send $TOKEN_PROXY_ADDRESS "mintWithETH(address,uint256)" --value --rpc-url $RPC_URL --private-key $MINTING_ACCOUNT_PRIVATE_KEY --from $MINTING_ACCOUNT_ADDRESS +cast send $TOKEN_PROXY_ADDRESS "mintWithETH(address)" --value --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 diff --git a/test/TestStableToken.sol b/test/TestStableToken.sol index 02b3cde..81ef7be 100644 --- a/test/TestStableToken.sol +++ b/test/TestStableToken.sol @@ -8,12 +8,14 @@ import { ERC20PermitUpgradeable } from 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"; +import { ERC1967Proxy } from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; error AccountNotMinter(); error AccountAlreadyMinter(); error AccountNotInMinterList(); error InsufficientETH(); error ExceedsMaxSupply(); +error InvalidMaxSupply(uint256 supplied); contract TestStableToken is Initializable, @@ -44,6 +46,7 @@ contract TestStableToken is __ERC20Permit_init("TestStableToken"); __Ownable_init(); __UUPSUpgradeable_init(); + if (_maxSupply == 0) revert InvalidMaxSupply(_maxSupply); maxSupply = _maxSupply; } diff --git a/test/TestStableToken.t.sol b/test/TestStableToken.t.sol index 8954232..82f20db 100644 --- a/test/TestStableToken.t.sol +++ b/test/TestStableToken.t.sol @@ -8,7 +8,8 @@ import { AccountAlreadyMinter, AccountNotInMinterList, InsufficientETH, - ExceedsMaxSupply + ExceedsMaxSupply, + InvalidMaxSupply } from "./TestStableToken.sol"; import { ERC1967Proxy } from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; import { DeployTokenWithProxy } from "../script/DeployTokenWithProxy.s.sol"; @@ -336,4 +337,18 @@ contract TestStableTokenTest is Test { vm.expectRevert("Ownable: caller is not the owner"); token.setMaxSupply(newMaxSupply); } + + function test__InitializeZeroReverts() external { + // Deploy implementation directly + TestStableToken implementation = new TestStableToken(); + + // Build initializer calldata with zero + bytes memory initData = abi.encodeCall(TestStableToken.initialize, (uint256(0))); + + // Expect the InvalidMaxSupply reversion including the supplied value + vm.expectRevert(abi.encodeWithSelector(InvalidMaxSupply.selector, uint256(0))); + + // Attempt to deploy proxy with initData - should revert + new ERC1967Proxy(address(implementation), initData); + } }