mirror of
https://github.com/logos-messaging/logos-messaging-rlnv2-contract.git
synced 2026-01-03 22:43:12 +00:00
Replace manual maxSupply with OZ ERC20CappedUpgradeable
This commit is contained in:
parent
9a4990a7d4
commit
f6e692c586
@ -11,14 +11,14 @@ 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) });
|
||||
// Read desired token cap from env or use default
|
||||
uint256 defaultCap = vm.envOr({ name: "TOKEN_CAP", defaultValue: uint256(1_000_000 * 10 ** 18) });
|
||||
|
||||
// Deploy the initial implementation
|
||||
address implementation = address(new TestStableToken());
|
||||
|
||||
// Encode the initialize call (maxSupply)
|
||||
bytes memory initData = abi.encodeCall(TestStableToken.initialize, (defaultMaxSupply));
|
||||
// Encode the initialize call (cap)
|
||||
bytes memory initData = abi.encodeCall(TestStableToken.initialize, (defaultCap));
|
||||
|
||||
// Deploy the proxy with initialization data
|
||||
ERC1967Proxy proxy = new ERC1967Proxy(implementation, initData);
|
||||
@ -27,9 +27,9 @@ contract DeployTokenWithProxy is BaseScript {
|
||||
// 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");
|
||||
// Check cap set
|
||||
uint256 actualMax = TestStableToken(proxyAddr).cap();
|
||||
if (actualMax != defaultCap) revert("Proxy token cap mismatch after initialization");
|
||||
|
||||
return proxy;
|
||||
}
|
||||
|
||||
@ -21,7 +21,7 @@ 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
|
||||
Add environment variable `TOKEN_CAP` to set the maximum supply of the token, otherwise it defaults to 1 million
|
||||
tokens.
|
||||
|
||||
### Deploy new TestStableToken with proxy contract
|
||||
@ -54,7 +54,7 @@ When upgrading a UUPS/ERC1967 proxy you should perform the upgrade and initializ
|
||||
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 * 10**18)
|
||||
# Encode the initializer calldata (example: set TOKEN_CAP to 1_000_000 * 10**18)
|
||||
DATA=$(cast abi-encode "initialize(uint256)" 1000000000000000000000000)
|
||||
|
||||
# Perform upgrade and call initializer atomically
|
||||
@ -63,7 +63,7 @@ cast send $TOKEN_PROXY_ADDRESS "upgradeToAndCall(address,bytes)" $NEW_IMPLEMENTA
|
||||
|
||||
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.
|
||||
time window where the proxy points to a new implementation but its storage (e.g., `cap`) is uninitialized.
|
||||
|
||||
### Add account to the allowlist to enable minting
|
||||
|
||||
|
||||
@ -5,6 +5,8 @@ import { BaseScript } from "../script/Base.s.sol";
|
||||
import { ERC20Upgradeable } from "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol";
|
||||
import { ERC20PermitUpgradeable } from
|
||||
"@openzeppelin/contracts-upgradeable/token/ERC20/extensions/ERC20PermitUpgradeable.sol";
|
||||
import { ERC20CappedUpgradeable } from
|
||||
"@openzeppelin/contracts-upgradeable/token/ERC20/extensions/ERC20CappedUpgradeable.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";
|
||||
@ -14,23 +16,24 @@ error AccountNotMinter();
|
||||
error AccountAlreadyMinter();
|
||||
error AccountNotInMinterList();
|
||||
error InsufficientETH();
|
||||
error ExceedsMaxSupply();
|
||||
error InvalidMaxSupply(uint256 supplied);
|
||||
error ExceedsCap();
|
||||
|
||||
contract TestStableToken is
|
||||
Initializable,
|
||||
ERC20Upgradeable,
|
||||
ERC20CappedUpgradeable,
|
||||
ERC20PermitUpgradeable,
|
||||
OwnableUpgradeable,
|
||||
UUPSUpgradeable
|
||||
{
|
||||
mapping(address => bool) public isMinter;
|
||||
uint256 public maxSupply;
|
||||
mapping(address account => bool allowed) public isMinter;
|
||||
|
||||
// mutable cap storage ( override cap() from ERC20CappedUpgradeable to return this)
|
||||
uint256 private _mutableCap;
|
||||
|
||||
event MinterAdded(address indexed account);
|
||||
event MinterRemoved(address indexed account);
|
||||
event ETHBurned(uint256 amount, address indexed minter, address indexed to, uint256 tokensMinted);
|
||||
event MaxSupplySet(uint256 oldMaxSupply, uint256 newMaxSupply);
|
||||
event CapSet(uint256 oldCap, uint256 newCap);
|
||||
|
||||
modifier onlyOwnerOrMinter() {
|
||||
if (msg.sender != owner() && !isMinter[msg.sender]) revert("AccountNotMinter");
|
||||
@ -41,14 +44,16 @@ contract TestStableToken is
|
||||
_disableInitializers();
|
||||
}
|
||||
|
||||
function initialize(uint256 _maxSupply) public initializer {
|
||||
function initialize(uint256 initialCap) public initializer {
|
||||
__ERC20_init("TestStableToken", "TST");
|
||||
__ERC20Permit_init("TestStableToken");
|
||||
__Ownable_init();
|
||||
__UUPSUpgradeable_init();
|
||||
if (_maxSupply == 0) revert InvalidMaxSupply(_maxSupply);
|
||||
// initialize capped supply (parent init does internal checks)
|
||||
__ERC20Capped_init(initialCap);
|
||||
|
||||
maxSupply = _maxSupply;
|
||||
// our mutable cap storage (used by the overridden cap())
|
||||
_mutableCap = initialCap;
|
||||
}
|
||||
|
||||
function _authorizeUpgrade(address newImplementation) internal override onlyOwner { }
|
||||
@ -66,13 +71,15 @@ contract TestStableToken is
|
||||
}
|
||||
|
||||
function mint(address to, uint256 amount) external onlyOwnerOrMinter {
|
||||
if (totalSupply() + amount > maxSupply) revert ExceedsMaxSupply();
|
||||
// pre-check so we use custom error
|
||||
if (totalSupply() + amount > cap()) revert ExceedsCap();
|
||||
// ERC20CappedUpgradeable::_mint will still enforce the cap as a safety
|
||||
_mint(to, amount);
|
||||
}
|
||||
|
||||
function mintWithETH(address to) external payable {
|
||||
if (msg.value == 0) revert InsufficientETH();
|
||||
if (totalSupply() + msg.value > maxSupply) revert ExceedsMaxSupply();
|
||||
if (totalSupply() + msg.value > cap()) revert ExceedsCap();
|
||||
|
||||
// Burn ETH by sending to zero address
|
||||
payable(address(0)).transfer(msg.value);
|
||||
@ -82,30 +89,48 @@ contract TestStableToken is
|
||||
emit ETHBurned(msg.value, msg.sender, to, msg.value);
|
||||
}
|
||||
|
||||
function setMaxSupply(uint256 _maxSupply) external onlyOwner {
|
||||
if (_maxSupply < totalSupply()) revert ExceedsMaxSupply();
|
||||
// Returns the configured cap - override to use a mutable storage slot.
|
||||
function cap() public view virtual override returns (uint256) {
|
||||
return _mutableCap;
|
||||
}
|
||||
|
||||
uint256 oldMaxSupply = maxSupply;
|
||||
maxSupply = _maxSupply;
|
||||
function setCap(uint256 newCap) external onlyOwner {
|
||||
if (newCap < totalSupply()) revert ExceedsCap();
|
||||
uint256 old = _mutableCap;
|
||||
_mutableCap = newCap;
|
||||
emit CapSet(old, newCap);
|
||||
}
|
||||
|
||||
emit MaxSupplySet(oldMaxSupply, _maxSupply);
|
||||
// Solidity requires an explicit override when multiple base classes in the
|
||||
// linearized inheritance chain declare the same function signature. Here
|
||||
// both ERC20Upgradeable (base) and ERC20CappedUpgradeable (overrider) are
|
||||
// present in the chain, so provide the override that forwards to super.
|
||||
function _mint(
|
||||
address account,
|
||||
uint256 amount
|
||||
)
|
||||
internal
|
||||
virtual
|
||||
override(ERC20CappedUpgradeable, ERC20Upgradeable)
|
||||
{
|
||||
super._mint(account, amount);
|
||||
}
|
||||
}
|
||||
|
||||
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.
|
||||
/// @dev Reads `TOKEN_CAP` from environment (wei). Defaults to 1_000_000 * 10**18.
|
||||
function run() public broadcast returns (address) {
|
||||
// Read desired max supply from env or use default
|
||||
uint256 defaultMaxSupply = vm.envOr({ name: "MAX_SUPPLY", defaultValue: uint256(1_000_000 * 10 ** 18) });
|
||||
// Read desired token cap from env or use default
|
||||
uint256 defaultCap = vm.envOr({ name: "TOKEN_CAP", defaultValue: uint256(1_000_000 * 10 ** 18) });
|
||||
|
||||
// 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));
|
||||
// Encode initializer calldata to run in proxy context (cap)
|
||||
bytes memory initData = abi.encodeCall(TestStableToken.initialize, (defaultCap));
|
||||
|
||||
// Deploy ERC1967Proxy with initialization data so storage (owner, maxSupply) is set atomically
|
||||
// Deploy ERC1967Proxy with initialization data so storage (owner, cap) is set atomically
|
||||
ERC1967Proxy proxy = new ERC1967Proxy(implementation, initData);
|
||||
|
||||
return address(proxy);
|
||||
|
||||
@ -8,8 +8,7 @@ import {
|
||||
AccountAlreadyMinter,
|
||||
AccountNotInMinterList,
|
||||
InsufficientETH,
|
||||
ExceedsMaxSupply,
|
||||
InvalidMaxSupply
|
||||
ExceedsCap
|
||||
} from "./TestStableToken.sol";
|
||||
import { ERC1967Proxy } from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol";
|
||||
import { DeployTokenWithProxy } from "../script/DeployTokenWithProxy.s.sol";
|
||||
@ -25,7 +24,7 @@ contract TestStableTokenTest is Test {
|
||||
event MinterAdded(address indexed account);
|
||||
event MinterRemoved(address indexed account);
|
||||
event ETHBurned(uint256 amount, address indexed minter, address indexed to, uint256 tokensMinted);
|
||||
event MaxSupplySet(uint256 oldMaxSupply, uint256 newMaxSupply);
|
||||
event CapSet(uint256 oldCap, uint256 newCap);
|
||||
|
||||
function setUp() public {
|
||||
// Deploy using the deployment script
|
||||
@ -278,77 +277,63 @@ contract TestStableTokenTest is Test {
|
||||
token.mintWithETH{ value: 0 }(user2);
|
||||
}
|
||||
|
||||
function test__MaxSupplyIsSetCorrectly() external {
|
||||
// maxSupply should be set to 1000000 * 10^18 by deployment script
|
||||
uint256 expectedMaxSupply = 1_000_000 * 10 ** 18;
|
||||
assertEq(token.maxSupply(), expectedMaxSupply);
|
||||
function test__CapIsSetCorrectly() external {
|
||||
// Cap should be set to 1000000 * 10^18 by deployment script
|
||||
uint256 expectedCap = 1_000_000 * 10 ** 18;
|
||||
assertEq(token.cap(), expectedCap);
|
||||
}
|
||||
|
||||
function test__CannotMintExceedingMaxSupply() external {
|
||||
uint256 currentMaxSupply = token.maxSupply();
|
||||
function test__CannotMintExceedingCap() external {
|
||||
uint256 currentCap = token.cap();
|
||||
|
||||
// Try to mint more than maxSupply
|
||||
// Try to mint more than cap
|
||||
vm.prank(owner);
|
||||
vm.expectRevert(abi.encodeWithSelector(ExceedsMaxSupply.selector));
|
||||
token.mint(user1, currentMaxSupply + 1);
|
||||
vm.expectRevert(abi.encodeWithSelector(ExceedsCap.selector));
|
||||
token.mint(user1, currentCap + 1);
|
||||
}
|
||||
|
||||
function test__CannotMintWithETHExceedingMaxSupply() external {
|
||||
uint256 currentMaxSupply = token.maxSupply();
|
||||
// Send an amount of ETH that would exceed maxSupply when minted as tokens
|
||||
uint256 ethAmount = currentMaxSupply + 1;
|
||||
function test__CannotMintWithETHExceedingCap() external {
|
||||
uint256 currentCap = token.cap();
|
||||
// Send an amount of ETH that would exceed cap when minted as tokens
|
||||
uint256 ethAmount = currentCap + 1;
|
||||
|
||||
// Try to mint more than maxSupply with ETH
|
||||
// Try to mint more than cap with ETH
|
||||
vm.deal(owner, ethAmount);
|
||||
vm.prank(owner);
|
||||
vm.expectRevert(abi.encodeWithSelector(ExceedsMaxSupply.selector));
|
||||
vm.expectRevert(abi.encodeWithSelector(ExceedsCap.selector));
|
||||
token.mintWithETH{ value: ethAmount }(user1);
|
||||
}
|
||||
|
||||
function test__OwnerCanSetMaxSupply() external {
|
||||
uint256 newMaxSupply = 2_000_000 * 10 ** 18;
|
||||
uint256 oldMaxSupply = token.maxSupply();
|
||||
function test__OwnerCanSetCap() external {
|
||||
uint256 newCap = 2_000_000 * 10 ** 18;
|
||||
uint256 oldCap = token.cap();
|
||||
|
||||
vm.expectEmit(true, true, false, false);
|
||||
emit MaxSupplySet(oldMaxSupply, newMaxSupply);
|
||||
emit CapSet(oldCap, newCap);
|
||||
|
||||
vm.prank(owner);
|
||||
token.setMaxSupply(newMaxSupply);
|
||||
token.setCap(newCap);
|
||||
|
||||
assertEq(token.maxSupply(), newMaxSupply);
|
||||
assertEq(token.cap(), newCap);
|
||||
}
|
||||
|
||||
function test__CannotSetMaxSupplyBelowTotalSupply() external {
|
||||
function test__CannotSetCapBelowTotalSupply() external {
|
||||
// First mint some tokens
|
||||
uint256 mintAmount = 1000 ether;
|
||||
vm.prank(owner);
|
||||
token.mint(user1, mintAmount);
|
||||
|
||||
// Try to set maxSupply below current totalSupply
|
||||
// Try to set cap below current totalSupply
|
||||
vm.prank(owner);
|
||||
vm.expectRevert(abi.encodeWithSelector(ExceedsMaxSupply.selector));
|
||||
token.setMaxSupply(mintAmount - 1);
|
||||
vm.expectRevert(abi.encodeWithSelector(ExceedsCap.selector));
|
||||
token.setCap(mintAmount - 1);
|
||||
}
|
||||
|
||||
function test__NonOwnerCannotSetMaxSupply() external {
|
||||
uint256 newMaxSupply = 2_000_000 * 10 ** 18;
|
||||
function test__NonOwnerCannotSetCap() external {
|
||||
uint256 newCap = 2_000_000 * 10 ** 18;
|
||||
|
||||
vm.prank(user1);
|
||||
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);
|
||||
token.setCap(newCap);
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user