Skip to content

Commit

Permalink
Add mint start and expiration (#13)
Browse files Browse the repository at this point in the history
* Allow minting to be bounded by time
  • Loading branch information
marcomariscal authored Dec 17, 2024
1 parent 4e104b0 commit ccce0ab
Show file tree
Hide file tree
Showing 4 changed files with 262 additions and 54 deletions.
35 changes: 34 additions & 1 deletion l2-contracts/src/ZkCappedMinterV2.sol
Original file line number Diff line number Diff line change
Expand Up @@ -25,19 +25,45 @@ contract ZkCappedMinterV2 is AccessControl, Pausable {
/// @notice Whether the contract has been permanently closed.
bool public closed;

/// @notice The timestamp when minting can begin.
uint48 public immutable START_TIME;

/// @notice The timestamp after which minting is no longer allowed (inclusive).
uint48 public immutable EXPIRATION_TIME;

/// @notice Error for when the cap is exceeded.
error ZkCappedMinterV2__CapExceeded(address minter, uint256 amount);

/// @notice Thrown when a mint action is taken while the contract is closed.
error ZkCappedMinterV2__ContractClosed();

/// @notice Error for when minting is attempted before the start time.
error ZkCappedMinterV2__NotStarted();

/// @notice Error for when minting is attempted after expiration.
error ZkCappedMinterV2__Expired();

/// @notice Error for when the start time is greater than or equal to expiration time, or start time is in the past.
error ZkCappedMinterV2__InvalidTime();

/// @notice Constructor for a new ZkCappedMinterV2 contract
/// @param _token The token contract where tokens will be minted.
/// @param _admin The address that will be granted the admin role.
/// @param _cap The maximum number of tokens that may be minted by the ZkCappedMinter.
constructor(IMintableAndDelegatable _token, address _admin, uint256 _cap) {
/// @param _startTime The timestamp when minting can begin.
/// @param _expirationTime The timestamp after which minting is no longer allowed (inclusive).
constructor(IMintableAndDelegatable _token, address _admin, uint256 _cap, uint48 _startTime, uint48 _expirationTime) {
if (_startTime > _expirationTime) {
revert ZkCappedMinterV2__InvalidTime();
}
if (_startTime < block.timestamp) {
revert ZkCappedMinterV2__InvalidTime();
}

TOKEN = _token;
CAP = _cap;
START_TIME = _startTime;
EXPIRATION_TIME = _expirationTime;

_grantRole(DEFAULT_ADMIN_ROLE, _admin);
_grantRole(PAUSER_ROLE, _admin);
Expand All @@ -60,6 +86,13 @@ contract ZkCappedMinterV2 is AccessControl, Pausable {
/// @param _amount The quantity of tokens, in raw decimals, that will be created.
function mint(address _to, uint256 _amount) external {
_revertIfClosed();

if (block.timestamp < START_TIME) {
revert ZkCappedMinterV2__NotStarted();
}
if (block.timestamp > EXPIRATION_TIME) {
revert ZkCappedMinterV2__Expired();
}
_requireNotPaused();
_checkRole(MINTER_ROLE, msg.sender);
_revertIfCapExceeded(_amount);
Expand Down
50 changes: 35 additions & 15 deletions l2-contracts/src/ZkCappedMinterV2Factory.sol
Original file line number Diff line number Diff line change
Expand Up @@ -22,41 +22,61 @@ contract ZkCappedMinterV2Factory {
/// @param token The token contract where tokens will be minted.
/// @param admin The address authorized to mint tokens.
/// @param cap The maximum number of tokens that may be minted.
event CappedMinterV2Created(address indexed minterAddress, IMintableAndDelegatable token, address admin, uint256 cap);
/// @param startTime The timestamp when minting can begin.
/// @param expirationTime The timestamp after which minting is no longer allowed.
event CappedMinterV2Created(
address indexed minterAddress,
IMintableAndDelegatable token,
address admin,
uint256 cap,
uint48 startTime,
uint48 expirationTime
);

/// @notice Deploys a new ZkCappedMinterV2 contract using CREATE2.
/// @param _token The token contract where tokens will be minted.
/// @param _admin The address authorized to mint tokens.
/// @param _cap The maximum number of tokens that may be minted.
/// @param _startTime The timestamp when minting can begin.
/// @param _expirationTime The timestamp after which minting is no longer allowed.
/// @param _saltNonce A user-provided nonce for salt calculation.
/// @return minterAddress The address of the newly deployed ZkCappedMinterV2.
function createCappedMinter(IMintableAndDelegatable _token, address _admin, uint256 _cap, uint256 _saltNonce)
external
returns (address minterAddress)
{
bytes memory saltArgs = abi.encode(_token, _admin, _cap);
function createCappedMinter(
IMintableAndDelegatable _token,
address _admin,
uint256 _cap,
uint48 _startTime,
uint48 _expirationTime,
uint256 _saltNonce
) external returns (address minterAddress) {
bytes memory saltArgs = abi.encode(_token, _admin, _cap, _startTime, _expirationTime);
bytes32 salt = _calculateSalt(saltArgs, _saltNonce);
ZkCappedMinterV2 instance = new ZkCappedMinterV2{salt: salt}(_token, _admin, _cap);
ZkCappedMinterV2 instance = new ZkCappedMinterV2{salt: salt}(_token, _admin, _cap, _startTime, _expirationTime);
minterAddress = address(instance);

emit CappedMinterV2Created(minterAddress, _token, _admin, _cap);
emit CappedMinterV2Created(minterAddress, _token, _admin, _cap, _startTime, _expirationTime);
}

/// @notice Computes the address of a ZkCappedMinterV2 deployed via this factory.
/// @param _token The token contract where tokens will be minted.
/// @param _admin The address authorized to mint tokens.
/// @param _cap The maximum number of tokens that may be minted.
/// @param _startTime The timestamp when minting can begin.
/// @param _expirationTime The timestamp after which minting is no longer allowed.
/// @param _saltNonce The nonce used for salt calculation.
/// @return addr The address of the ZkCappedMinterV2.
function getMinter(IMintableAndDelegatable _token, address _admin, uint256 _cap, uint256 _saltNonce)
external
view
returns (address addr)
{
bytes memory saltArgs = abi.encode(_token, _admin, _cap);
function getMinter(
IMintableAndDelegatable _token,
address _admin,
uint256 _cap,
uint48 _startTime,
uint48 _expirationTime,
uint256 _saltNonce
) external view returns (address addr) {
bytes memory saltArgs = abi.encode(_token, _admin, _cap, _startTime, _expirationTime);
bytes32 salt = _calculateSalt(saltArgs, _saltNonce);
addr = L2ContractHelper.computeCreate2Address(
address(this), salt, BYTECODE_HASH, keccak256(abi.encode(_token, _admin, _cap))
address(this), salt, BYTECODE_HASH, keccak256(abi.encode(_token, _admin, _cap, _startTime, _expirationTime))
);
}

Expand Down
123 changes: 107 additions & 16 deletions l2-contracts/test/ZkCappedMinterV2.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -10,19 +10,36 @@ import {console2} from "forge-std/Test.sol";
contract ZkCappedMinterV2Test is ZkTokenTest {
ZkCappedMinterV2 public cappedMinter;
uint256 constant DEFAULT_CAP = 100_000_000e18;
uint48 DEFAULT_START_TIME;
uint48 DEFAULT_EXPIRATION_TIME;

address cappedMinterAdmin = makeAddr("cappedMinterAdmin");

function setUp() public virtual override {
super.setUp();

cappedMinter = _createCappedMinter(cappedMinterAdmin, DEFAULT_CAP);
DEFAULT_START_TIME = uint48(vm.getBlockTimestamp());
DEFAULT_EXPIRATION_TIME = uint48(DEFAULT_START_TIME + 3 days);

cappedMinter = _createCappedMinter(cappedMinterAdmin, DEFAULT_CAP, DEFAULT_START_TIME, DEFAULT_EXPIRATION_TIME);

vm.prank(admin);
token.grantRole(MINTER_ROLE, address(cappedMinter));
}

function _createCappedMinter(address _admin, uint256 _cap) internal returns (ZkCappedMinterV2) {
return new ZkCappedMinterV2(IMintableAndDelegatable(address(token)), _admin, _cap);
function _createCappedMinter(address _admin, uint256 _cap, uint48 _startTime, uint48 _expirationTime)
internal
returns (ZkCappedMinterV2)
{
return new ZkCappedMinterV2(IMintableAndDelegatable(address(token)), _admin, _cap, _startTime, _expirationTime);
}

function _boundToValidTimeControls(uint48 _startTime, uint48 _expirationTime) internal view returns (uint48, uint48) {
// Using uint32 for time controls to prevent overflows in the ZkToken contract regarding block numbers needing to be
// casted to uint32.
_startTime = uint48(bound(_startTime, vm.getBlockTimestamp(), type(uint32).max - 1));
_expirationTime = uint48(bound(_expirationTime, _startTime + 1, type(uint32).max));
return (_startTime, _expirationTime);
}

function _grantMinterRole(ZkCappedMinterV2 _cappedMinter, address _cappedMinterAdmin, address _minter) internal {
Expand All @@ -43,14 +60,46 @@ contract ZkCappedMinterV2Test is ZkTokenTest {
}

contract Constructor is ZkCappedMinterV2Test {
function testFuzz_InitializesTheCappedMinterForAssociationAndFoundation(address _cappedMinterAdmin, uint256 _cap)
public
{
_cap = bound(_cap, 0, MAX_MINT_SUPPLY);
ZkCappedMinterV2 cappedMinter = _createCappedMinter(_cappedMinterAdmin, _cap);
function testFuzz_InitializesTheCappedMinterForAssociationAndFoundation(
address _admin,
uint256 _cap,
uint48 _startTime,
uint48 _expirationTime
) public {
(_startTime, _expirationTime) = _boundToValidTimeControls(_startTime, _expirationTime);
vm.warp(_startTime);

ZkCappedMinterV2 cappedMinter = _createCappedMinter(_admin, _cap, _startTime, _expirationTime);
assertEq(address(cappedMinter.TOKEN()), address(token));
assertEq(cappedMinter.hasRole(DEFAULT_ADMIN_ROLE, _cappedMinterAdmin), true);
assertEq(cappedMinter.CAP(), _cap);
assertEq(cappedMinter.START_TIME(), _startTime);
assertEq(cappedMinter.EXPIRATION_TIME(), _expirationTime);
}

function testFuzz_RevertIf_StartTimeAfterExpirationTime(
address _admin,
uint256 _cap,
uint48 _startTime,
uint48 _invalidExpirationTime
) public {
_startTime = uint48(bound(_startTime, 1, type(uint48).max));
_invalidExpirationTime = uint48(bound(_invalidExpirationTime, 0, _startTime - 1));
vm.expectRevert(ZkCappedMinterV2.ZkCappedMinterV2__InvalidTime.selector);
_createCappedMinter(_admin, _cap, _startTime, _invalidExpirationTime);
}

function testFuzz_RevertIf_StartTimeInPast(address _admin, uint256 _cap, uint48 _startTime, uint48 _expirationTime)
public
{
_startTime = uint48(bound(_startTime, 1, type(uint48).max));
vm.warp(_startTime);

_cap = bound(_cap, 1, DEFAULT_CAP);
uint48 _pastStartTime = _startTime - 1;
_expirationTime = uint48(bound(_expirationTime, _pastStartTime + 1, type(uint48).max));

vm.expectRevert(ZkCappedMinterV2.ZkCappedMinterV2__InvalidTime.selector);
_createCappedMinter(_admin, _cap, _pastStartTime, _expirationTime);
}
}

Expand All @@ -77,7 +126,9 @@ contract Mint is ZkCappedMinterV2Test {
address _receiver1,
address _receiver2,
uint256 _amount1,
uint256 _amount2
uint256 _amount2,
uint256 _startTime,
uint256 _expirationTime
) public {
_amount1 = bound(_amount1, 1, DEFAULT_CAP / 2);
_amount2 = bound(_amount2, 1, DEFAULT_CAP / 2);
Expand All @@ -99,6 +150,23 @@ contract Mint is ZkCappedMinterV2Test {
assertEq(token.balanceOf(_receiver2), balanceBefore2 + _amount2);
}

function testFuzz_CorrectlyPermanentlyBlocksMintingWhenClosed(address _minter, address _receiver, uint256 _amount)
public
{
_amount = bound(_amount, 1, DEFAULT_CAP);
vm.assume(_receiver != address(0));

vm.prank(cappedMinterAdmin);
cappedMinter.grantRole(MINTER_ROLE, _minter);

vm.prank(cappedMinterAdmin);
cappedMinter.close();

vm.expectRevert(ZkCappedMinterV2.ZkCappedMinterV2__ContractClosed.selector);
vm.prank(_minter);
cappedMinter.mint(_receiver, _amount);
}

function testFuzz_RevertIf_MintAttemptedByNonMinter(address _nonMinter, uint256 _amount) public {
_amount = bound(_amount, 1, DEFAULT_CAP);

Expand Down Expand Up @@ -127,17 +195,40 @@ contract Mint is ZkCappedMinterV2Test {
cappedMinter.mint(_receiver, _amount);
}

function testFuzz_CorrectlyPermanentlyBlocksMinting(address _minter, address _receiver, uint256 _amount) public {
function testFuzz_RevertIf_MintBeforeStartTime(
address _minter,
address _receiver,
uint256 _amount,
uint256 _beforeStartTime
) public {
vm.assume(_receiver != address(0));
_amount = bound(_amount, 1, DEFAULT_CAP);
_beforeStartTime = bound(_beforeStartTime, 0, cappedMinter.START_TIME() - 1);

vm.warp(_beforeStartTime);

_grantMinterRole(cappedMinter, cappedMinterAdmin, _minter);

vm.expectRevert(ZkCappedMinterV2.ZkCappedMinterV2__NotStarted.selector);
vm.prank(_minter);
cappedMinter.mint(_receiver, _amount);
}

function testFuzz_RevertIf_MintAfterExpiration(
address _minter,
address _receiver,
uint256 _amount,
uint256 _afterExpirationTime
) public {
_amount = bound(_amount, 1, DEFAULT_CAP);
vm.assume(_receiver != address(0));
_afterExpirationTime = bound(_afterExpirationTime, cappedMinter.EXPIRATION_TIME() + 1, type(uint256).max);

vm.prank(cappedMinterAdmin);
cappedMinter.grantRole(MINTER_ROLE, _minter);
vm.warp(_afterExpirationTime);

vm.prank(cappedMinterAdmin);
cappedMinter.close();
_grantMinterRole(cappedMinter, cappedMinterAdmin, _minter);

vm.expectRevert(ZkCappedMinterV2.ZkCappedMinterV2__ContractClosed.selector);
vm.expectRevert(ZkCappedMinterV2.ZkCappedMinterV2__Expired.selector);
vm.prank(_minter);
cappedMinter.mint(_receiver, _amount);
}
Expand Down
Loading

0 comments on commit ccce0ab

Please sign in to comment.