diff --git a/script/DeployTokenWithProxy.s.sol b/script/DeployTokenWithProxy.s.sol index c61561f..9545950 100644 --- a/script/DeployTokenWithProxy.s.sol +++ b/script/DeployTokenWithProxy.s.sol @@ -11,13 +11,29 @@ 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) }); + + // Validate value is sensible + require(defaultMaxSupply > 0, "MAX_SUPPLY must be > 0"); + // 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); + ERC1967Proxy proxy = new ERC1967Proxy(implementation, initData); + + // Post-deploy assertions to ensure initialization succeeded + // These revert the script if validation fails. + address proxyAddr = address(proxy); + + // Check maxSupply set + uint256 actualMax = TestStableToken(proxyAddr).maxSupply(); + if (actualMax != defaultMaxSupply) revert("Proxy maxSupply mismatch after initialization"); + + return proxy; } } diff --git a/test/README.md b/test/README.md index 34ac908..0a5b7fe 100644 --- a/test/README.md +++ b/test/README.md @@ -21,10 +21,12 @@ token distribution while mimicking DAI's behaviour. ## Usage +Add environment variable `MAX_SUPPLY` to set the maximum supply of the token, otherwise it defaults to 10 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 +38,32 @@ 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. The proxy contract can then be updated 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 +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 +### Update the proxy contract to point to the new implementation (safe, recommended) + +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 -# 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 +# Encode the initializer calldata (example: set MAX_SUPPLY to 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 diff --git a/test/TestStableToken.sol b/test/TestStableToken.sol index 02b3cde..8f64e7f 100644 --- a/test/TestStableToken.sol +++ b/test/TestStableToken.sol @@ -8,6 +8,7 @@ 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(); @@ -90,7 +91,27 @@ contract TestStableToken is } contract TestStableTokenFactory is BaseScript { + /// @notice Deploys the implementation and an ERC1967 proxy, initializing the proxy atomically. + /// @dev Reads `MAX_SUPPLY` from environment (wei). Defaults to 1_000_000 * 10**18. function run() public broadcast returns (address) { - return address(new TestStableToken()); + // Read desired max supply from env or use default + uint256 defaultMaxSupply = vm.envOr({ name: "MAX_SUPPLY", defaultValue: uint256(1_000_000 * 10 ** 18) }); + + // Validate value is sensible + if (defaultMaxSupply == 0) revert("MAX_SUPPLY must be > 0"); + + // Deploy the implementation + address implementation = address(new TestStableToken()); + + // Encode initializer calldata to run in proxy context (maxSupply) + bytes memory initData = abi.encodeCall(TestStableToken.initialize, (defaultMaxSupply)); + + // Deploy ERC1967Proxy with initialization data so storage (owner, maxSupply) is set atomically + ERC1967Proxy proxy = new ERC1967Proxy(implementation, initData); + + // // Only check maxSupply was initialized; owner checks are optional for basic deployments + // address proxyAddr = address(proxy); + + return address(proxy); } }