Implement ERC20CappedUpgradable with setCap and mint override

This commit is contained in:
stubbsta 2025-09-11 10:51:44 +02:00
parent 76df72f977
commit cb2298aa3a
No known key found for this signature in database
2 changed files with 56 additions and 28 deletions

View File

@ -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";
@ -13,22 +15,24 @@ error AccountNotMinter();
error AccountAlreadyMinter();
error AccountNotInMinterList();
error InsufficientETH();
error ExceedsMaxSupply();
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();
@ -39,13 +43,17 @@ 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();
maxSupply = _maxSupply;
// initialize capped supply (parent init does internal checks)
__ERC20Capped_init(initialCap);
// our mutable cap storage (used by the overridden cap())
_mutableCap = initialCap;
}
function _authorizeUpgrade(address newImplementation) internal override onlyOwner { }
@ -63,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 our custom error (avoid OZ string revert)
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);
@ -79,13 +89,31 @@ 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. We 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);
}
}

View File

@ -8,7 +8,7 @@ import {
AccountAlreadyMinter,
AccountNotInMinterList,
InsufficientETH,
ExceedsMaxSupply
ExceedsCap
} from "./TestStableToken.sol";
import { ERC1967Proxy } from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol";
import { DeployTokenWithProxy } from "../script/DeployTokenWithProxy.s.sol";
@ -24,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
@ -280,41 +280,41 @@ contract TestStableTokenTest is Test {
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);
assertEq(token.cap(), expectedMaxSupply);
}
function test__CannotMintExceedingMaxSupply() external {
uint256 currentMaxSupply = token.maxSupply();
uint256 currentMaxSupply = token.cap();
// Try to mint more than maxSupply
vm.prank(owner);
vm.expectRevert(abi.encodeWithSelector(ExceedsMaxSupply.selector));
vm.expectRevert(abi.encodeWithSelector(ExceedsCap.selector));
token.mint(user1, currentMaxSupply + 1);
}
function test__CannotMintWithETHExceedingMaxSupply() external {
uint256 currentMaxSupply = token.maxSupply();
uint256 currentMaxSupply = token.cap();
// Send an amount of ETH that would exceed maxSupply when minted as tokens
uint256 ethAmount = currentMaxSupply + 1;
// Try to mint more than maxSupply 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();
uint256 oldMaxSupply = token.cap();
vm.expectEmit(true, true, false, false);
emit MaxSupplySet(oldMaxSupply, newMaxSupply);
emit CapSet(oldMaxSupply, newMaxSupply);
vm.prank(owner);
token.setMaxSupply(newMaxSupply);
token.setCap(newMaxSupply);
assertEq(token.maxSupply(), newMaxSupply);
assertEq(token.cap(), newMaxSupply);
}
function test__CannotSetMaxSupplyBelowTotalSupply() external {
@ -325,8 +325,8 @@ contract TestStableTokenTest is Test {
// Try to set maxSupply 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 {
@ -334,6 +334,6 @@ contract TestStableTokenTest is Test {
vm.prank(user1);
vm.expectRevert("Ownable: caller is not the owner");
token.setMaxSupply(newMaxSupply);
token.setCap(newMaxSupply);
}
}