ZkMinterModTriggerV1 - Usage Guide
Contributors:
Factory Labs:
With thanks to the ZK Team:
Date: 14/08/25
0. Introduction
This guide shows how to deploy, configure, and use ZkMinterModTriggerV1
in production or test environments.
To learn more about the genesis of the Trigger Mod, please read this introduction post.
TL;DR: Deploy → set minter → grant role → call
mint()
Goal: Ensure the contract is correctly configured, functionally sound, and free of high-risk vulnerabilities whilst meeting the intended business logic.
1. What the Contract Does
ZkMinterModTriggerV1
lets you mint tokens from a trusted IZkCappedMinter
and immediately execute multiple function calls-all in one atomic transaction.
Typical use-cases:
- Funding airdrops (e.g.,
MerkleDropFactory.addMerkleTree
) and bootstrapping liquidity - Batch interact with DeFi protocols after minting governance tokens
- Confirm the contract adheres to the documented specification
- Validate admin configuration and upgrade paths
- Verify minting logic and multi-call execution behave as expected
2. Prerequisites
- Solidity ≥0.8.24 tooling (Foundry/Hardhat)
- Deployed
IZkCappedMinter
(e.g.,ZkCappedMinterV2
) - Admin wallet (ideally multisig) to own the trigger
- Arrays of:
targets
- contract addresses to callfunctionSignatures
- 4-byte selectors (e.g.,bytes4(keccak256("transfer(address,uint256)"))
)callDatas
– ABI-encoded arguments only; do NOT include the 4-byte selector
- The three arrays must be parallel and equal length
Generating callDatas
quickly (script/CreateCallData.js
)
-
Open
script/CreateCallData.js
(TypeScript-flavoured) -
Edit the
functionSignatures
array - one entry per call (selector inferred automatically from the signature string) -
Edit the
argsList
array so each sub-array matches the arguments for the corresponding signature (useethers.parseUnits
for token amounts) -
Run the script:
npx ts-node script/CreateCallData.js # or pnpm ts-node ...
-
Copy the printed hex blobs into your
callDatas
array when deploying or updating the trigger- The script already strips the 4-byte selector, so you keep
callDatas
andfunctionSignatures
separate as required
- The script already strips the 4-byte selector, so you keep
-
If you later need different recipients/amounts, re-run the script and update only
callDatas
(array lengths must still match)
3. Deployment
ZkMinterModTriggerV1 trigger = new ZkMinterModTriggerV1(
admin, // address that can re-configure
targets, // address[] memory
functionSignatures,// bytes[] memory (each 4-byte selector)
callDatas // bytes[] memory (ABI-encoded args, NO selector)
);
- Read
docs/ZkMinterModTriggerV1.md
(design doc) - Review this workflow and add project specific notes
- Identify assumptions or external dependencies (e.g., ERC-20 compatibility of minted token)
4. Post-Deploy Setup
-
Link the minter
trigger.setMinter(address(zkCappedMinter));
-
Grant MINTER_ROLE to the trigger on the
zkCappedMinter
contract after callingsetMinter
; this ordering is required for successful mintingzkCappedMinter.grantRole(MINTER_ROLE, address(trigger));
Item | Questions / Actions |
---|---|
State Vars | • Does minter comply with IZkCappedMinter ?• Is admin set to a secure multisig? |
Constructor | • Arrays lengths equality enforcement • Initial targets / selectors / calldata sanity |
AdminOnly Modifier | • No privilege escalation or bypass |
Setter Functions | • Array length checks present • Copy-to-memory avoids calldata aliasing |
mint() Logic |
• Calls minter.mint(address(this), _amount) ; ensure _amount cannot exceed cap• Iterates over arrays and builds abi.encodePacked(selector, data) |
External Calls | • Re-entrancy impact minimal (no state written after external calls) • Revert logic on failure is correct |
Gas Usage | • Loop cost linear in targets.length ; verify realistic limits |
Tick off each item during review.
5. Mint-and-Call in One Transaction
Anyone can now execute:
trigger.mint(totalAmountToMint);
Under the hood this will:
minter.mint(address(this), totalAmountToMint)
- Loop through each configured target and call it with its calldata
If any call fails, the whole transaction reverts, guaranteeing atomicity.
-
Run Slither:
slither . --filter-paths "docs|scripts"
-
Address all HIGH and MEDIUM findings. Document any accepted risks
-
Optionally run Mythril or Echidna property-based tests
6. Updating Configuration
Only the admin
can adjust arrays or admin address:
setCallParameters(...)
- update all arrays at oncesetTargets(...)
,setFunctionSignatures(...)
,setCallDatas(...)
- update individuallysetAdmin(newAdmin)
- transfer admin
Remember to re-grant MINTER_ROLE if you migrate to a new trigger.
- Happy Path: Admin configures arrays, grants MINTER_ROLE, user calls
mint()
and all sub-calls succeed - Array Length Mismatch: Expect revert from setter and
mint()
- Failing Target Call: Make one target revert; ensure the whole tx reverts and state is unchanged
- Zero Targets: Edge case - decide whether allowed. Expect revert or noop per spec
7. Safety Tips
- Keep admin in a multisig or timelock
- Validate selectors & calldata off-chain before pushing to mainnet
- Limit
targets.length
to stay within block gas limits - Monitor emitted events and token balances after each
mint()
9. Quick Reference
Action | Function |
---|---|
Deploy | constructor(admin, targets, functionSignatures, data) |
Link minter | setMinter(address) |
Grant mint permission | minter.grantRole(MINTER_ROLE, trigger) |
Execute workflow | mint(amount) |
Update all arrays | setCallParameters(...) |
Move admin | setAdmin(newAdmin) |
- Confirm minted token’s inflation schedule remains within governance limits
- Ensure multi-call sequence cannot bypass spending caps or time-locks
- Verify admin address is governed by DAO / multisig with sufficient security controls
10. Post-Deployment Monitoring (Optional)
- Set up on-chain alerts for calls to
mint()
and changes toadmin
/config arrays - Track minted token supply vs cap in a dashboard
Annex 1: Full Example Walkthrough
Desired outcome: The ZkMinterModTrigger should distribute 1e18 amount of tokens to its targets, by minting 10e18 from the cappedminter.
Setup: The ZkMinterModTrigger is deployed with the following params:
The callData can be set using a helpful script CreateCallData.js
in the repo.
// targets
[
"0x69e5DC39E2bCb1C17053d2A4ee7CAEAAc5D36f91",
// ...
"0x69e5DC39E2bCb1C17053d2A4ee7CAEAAc5D36f99",
"0x69e5DC39E2bCb1C17053d2A4ee7CAEAAc5D36f90"
],
// functionSignatures (10 identical selectors for "transfer(address,uint256)")
[
"0xa9059cbb",
// ...
"0xa9059cbb"
],
// callDatas (10 encoded blobs) - Note these are illustrative only
[
"0x0000000000000000000000007a860e9c0986b5f7b1ab6ae7f0017d793dfcea2e0000000000000000000000000000000000000000000000000de0b6b3a7640000",
"0x0000000000000000000000005144edf6a2e7677433bbbd04618702c1c9df3c250000000000000000000000000000000000000000000000000de0b6b3a7640000",
"0x000000000000000000000000ee7d0b1fee97f5cf449f6e6f9d403db445bc93510000000000000000000000000000000000000000000000000de0b6b3a7640000",
"0x000000000000000000000000bc735044ec62c53a5c0c293191016e9c7788e8810000000000000000000000000000000000000000000000000de0b6b3a7640000",
"0x000000000000000000000000a453f6337b30798e260c624674846075811589900000000000000000000000000000000000000000000000000de0b6b3a7640000",
// ...
]
The ZkMinterModTrigger has the MINTER role granted on the ZkCappedMinter.
MINT call: Call the mint function on the ZkMinterModTrigger by passing 10e18 as the param. This will mint 10e18 from the cappedMinter and distribute 1e18 as it was configured.
NOTE: If you now want to mint a separate amount, update the callData accordingly. Either manually or using the script.
NOTE: If the Trigger is setup to distribute 1e18 and you mint 20e18 worth of the tokens, it will distribute the 10 (as it was configured to do so) and the rest of the 10 will just stay in trigger contract.
Annex 2: ZkMinterModTriggerV1 with Drips Integration Example
Full Example Walkthrough
Desired outcome: The ZkMinterModTrigger should distribute tokens to a Drips Drip List, which will automatically split and stream the funds to multiple recipients over time.
Setup: The ZkMinterModTrigger is deployed with the following params:
The callData can be set using the helpful script CreateDripCallData.js
in the repo.
// targets (single target - the token contract)
[
"0x69e5DC39E2bCb1C17053d2A4ee7CAEAAc5D36f96" // Token contract address
],
// functionSignatures (single selector for "transfer(address,uint256)")
[
"0xa9059cbb"
],
// callDatas (single encoded blob for Drips transfer)
[
"0x000000000000000000000000[dripListAddress]0000000000000000000000000000000000000000000000000de0b6b3a7640000"
]
Prerequisites
-
Create Drip List on Drips UI:
- Go to Drips Network
- Create a new Drip List
- Add your recipients and set their percentages
- Copy the Drip List address
-
Generate Call Data:
node script/CreateDripCallData.js
This will output the encoded callData for transferring tokens to your Drip List.
Deployment Configuration
The ZkMinterModTrigger has the MINTER role granted on the ZkCappedMinter.
MINT call: Call the mint function on the ZkMinterModTrigger by passing 10e18 as the param. This will:
- Mint 10e18 from the cappedMinter to the trigger contract
- Transfer 1e18 to the Drips Drip List
- Drips automatically distributes the funds to your recipients monthly
How Drips Integration Works
- Token Transfer: Your contract sends tokens to the Drip List address
- Automatic Distribution: Drips handles the monthly splitting based on your configured percentages
- Recipient Claims: Recipients can claim their funds from Drips at any time
- Dependency Tree: If recipients have their own Drip Lists, funds flow through the dependency tree
Benefits of Drips Integration
- Automatic Streaming: No need to manage individual transfers
- Dependency Support: Funds can flow to project dependencies
- Flexible Recipients: Easy to add/remove recipients via Drips UI