This guide provides step-by-step instructions for deploying the TECClaim contract using Hardhat Ignition.
Before deploying, ensure you have:
-
Required Contract Addresses:
- TEC MiniMe Token address (the source token to create a snapshot from)
- DAI token address
- RETH token address (or other redeemable tokens)
-
Deployment Parameters:
- Claim deadline (Unix timestamp in seconds)
- Owner address (who will control the contract)
-
Funded Account:
- Private key with sufficient ETH for gas fees
- Recommended: 0.01 ETH for Optimism, more for Ethereum mainnet
The TECClaim contract uses the UUPS (Universal Upgradeable Proxy Standard) pattern with a factory for deployment:
┌─────────────────────────────────────┐
│ TECClaimFactory │
│ (contracts/TECClaimFactory.sol) │
│ - Deploys ERC1967Proxy instances │
│ - Configured with implementation │
│ - Simplifies deployment │
└──────────────┬──────────────────────┘
│
▼
┌─────────────────────────────────────┐
│ Proxy Contract │
│ (ERC1967Proxy) │
│ - Stores contract state │
│ - Delegates calls to implementation │
│ - Created by TECClaimFactory │
└──────────────┬──────────────────────┘
│
▼
┌─────────────────────────────────────┐
│ TECClaim Implementation │
│ (contracts/TECClaim.sol) │
│ - Contains business logic │
│ - Upgradeable by owner │
│ - UUPS upgrade mechanism │
│ - Creates snapshot token on init │
└──────────────┬──────────────────────┘
│
▼
┌─────────────────────────────────────┐
│ Non-Transferable Snapshot Token │
│ - Created via createCloneToken() │
│ - Frozen at snapshot block │
│ - Transfers disabled │
│ - TECClaim is controller │
│ - Created after proxy initialization│
└─────────────────────────────────────┘
Key Points:
- Users always interact with the Proxy address
- The Proxy delegates all calls to the Implementation
- State is stored in the Proxy, not the Implementation
- Owner can upgrade to new Implementation via UUPS pattern
- The Implementation address can change, but Proxy address stays constant
- TECClaimFactory simplifies proxy creation by encapsulating initialization
- A non-transferable snapshot is created via
createSnapshotToken()after initialization - The snapshot is frozen at the block when
createSnapshotToken()is called - Owner can adjust snapshot balances with
burn()before activating claims
The TECClaim contract uses a state machine to manage the claim lifecycle:
┌──────────┐
│ dormant │ (initial state, never used in practice)
└──────────┘
│
▼
┌────────────┐
│ configured │ After initialize() is called
└──────────┬─┘ - Contract is set up with parameters
│ - Parent token reference stored
│ - Redeemable tokens configured
│ - Snapshot token NOT yet created
│ - Cannot claim yet
│
│ createSnapshotToken() called by owner
│ (creates non-transferable snapshot at current block)
│
│ [Optional: burn() to adjust specific balances]
│
│ startClaim() called by owner
│ (transfers tokens to contract)
▼
┌──────────┐
│ active │ Users can claim their tokens
└──────────┬─┘ - claim() function is enabled
│ - Users burn snapshot tokens to receive redeemables
│ - Owner can block/unblock addresses
│
│ claimRemaining() called after deadline
│
▼
┌────────────┐
│ finialized │ Claim period ended
└────────────┘ - Owner claimed remaining tokens
- No more claims possible
State Transitions:
- dormant → configured: Happens during
initialize()(deployment) - Within configured: Owner calls
createSnapshotToken()to create the snapshot, optionally callsburn()to adjust balances - configured → active: Owner calls
startClaim()to transfer redeemable tokens and enable claiming - active → finialized: Owner calls
claimRemaining()after deadline to collect unclaimed tokens
Important: The startClaim() function must be called by the owner after deployment to activate claiming. This allows the owner to coordinate the transfer of redeemable tokens from multiple sources before users can start claiming.
The local deployment uses a forked Optimism network, allowing you to test with real Optimism mainnet state.
The configuration in hardhat.config.ts includes a local network that forks Optimism:
# In terminal 1
npx hardhat node --network localThis will start a local node on http://127.0.0.1:8545 with forked Optimism state.
Edit ignition/parameters/local.json with your deployment parameters. Example:
{
"TECClaimModule": {
"tecTokenAddress": "0x8fc7c1109c08904160d6ae36482b79814d45eb78",
"daiAddress": "0xDA10009cBd5D07dd0CeCc66161FC93D7c9000da1",
"rethAddress": "0x9Bcef72be871e61ED4fBbc7630889beE758eb81D",
"claimDeadline": "1779726356",
"ownerAddress": "0x4D9339dd97db55e3B9bCBE65dE39fF9c04d1C2cd"
}
}Important Notes:
tecTokenAddress: The source MiniMe token address to create a snapshot from- A non-transferable snapshot will be created when
createSnapshotToken()is called - The snapshot will be frozen at the block when
createSnapshotToken()is called
npx hardhat compileThis generates artifacts for TECClaim and TECClaimFactory.
In a new terminal:
npx hardhat ignition deploy ignition/modules/TECClaim.ts \
--parameters ignition/parameters/local.json \
--network localhostExpected output:
Hardhat Ignition 🚀
Deploying [ TECClaimModule ]
Batch #1
Executed TECClaimModule#TECClaim_Implementation
Batch #2
Executed TECClaimModule#TECClaimFactory
Batch #3
Executed TECClaimModule#TECClaim_Proxy_Creation
Batch #4
Executed TECClaimModule#TECClaim_CreateSnapshot
Batch #5
Executed TECClaimModule#TECClaim
[ TECClaimModule ] successfully deployed 🚀
Deployed Addresses
TECClaimModule#TECClaim_Implementation - 0x...
TECClaimModule#TECClaimFactory - 0x...
TECClaimModule#TECClaim_Proxy_Creation - 0x... (proxy address)
TECClaimModule#TECClaim - 0x... (proxy address)
If you need to burn specific addresses' snapshot tokens before activation (e.g., to exclude certain accounts):
# Using cast (Foundry)
cast send $PROXY_ADDRESS "burn(address,uint256)" $ADDRESS_TO_BURN $AMOUNT \
--rpc-url http://127.0.0.1:8545 \
--private-key $OWNER_KEYThis step is optional and should only be done if needed before activating claims.
After deployment, the contract is in the configured state. You need to transfer redeemable tokens and activate claiming:
Option A: Manual Transfer + startClaim()
Option A: Manual Transfer + startClaim()
If you want to use startClaim() to pull tokens from multiple sources:
# First, identify addresses that hold the redeemable tokens
# For example: DAO treasury, multisig wallets, etc.
# Then call startClaim with those addresses
# This requires those addresses to have approved the contract first
# The function will automatically pull all redeemableTokens from the specified addressesOption B: Direct Transfer (Simple)
You can also simply transfer tokens directly to the contract and then call startClaim() with an empty array:
// Transfer tokens to contract address
dai.transfer(proxyAddress, DAI_AMOUNT);
reth.transfer(proxyAddress, RETH_AMOUNT);
// Activate claiming (with empty array since tokens are already in contract)
claim.startClaim([]);Since you're using a forked network, you can impersonate accounts that hold these tokens.
Verify State Transition:
# Check state (should be 2 for "active")
cast call $PROXY_ADDRESS "state()(uint8)" --rpc-url http://127.0.0.1:8545The hardhat.config.ts already includes Optimism network configuration:
optimism: {
type: "http",
url: "https://lb.drpc.live/optimism/...", // Your RPC URL
chainId: 10,
}For production deployment, update with your own RPC URL (from Infura, Alchemy, or other providers).
Store your deployer private key securely:
npx hardhat keystore set OPTIMISM_PRIVATE_KEY
# Enter your private key (without 0x prefix)Update hardhat.config.ts to use it:
optimism: {
type: "http",
url: "https://mainnet.optimism.io",
chainId: 10,
accounts: [configVariable("OPTIMISM_PRIVATE_KEY")],
}The ignition/parameters/optimism.json file contains deployment parameters:
{
"TECClaimModule": {
"tecTokenAddress": "0x8fc7c1109c08904160d6ae36482b79814d45eb78",
"daiAddress": "0xDA10009cBd5D07dd0CeCc66161FC93D7c9000da1",
"rethAddress": "0x9Bcef72be871e61ED4fBbc7630889beE758eb81D",
"claimDeadline": "1779726356",
"ownerAddress": "0x4D9339dd97db55e3B9bCBE65dE39fF9c04d1C2cd"
}
}Important: Verify these addresses:
- TEC Token (MiniMe): The source TEC token address on Optimism to create snapshot from
- DAI:
0xDA10009cBd5D07dd0CeCc66161FC93D7c9000da1(Optimism DAI) - RETH:
0x9Bcef72be871e61ED4fBbc7630889beE758eb81D(Optimism RETH)
npx hardhat compilenpx hardhat ignition deploy ignition/modules/TECClaim.ts \
--network optimism \
--parameters ignition/parameters/optimism.jsonAfter deployment, verify on Optimistic Etherscan:
npx hardhat ignition verify <deployment-id>The deployment ID will be shown after successful deployment. Verification will make the contract readable on https://optimistic.etherscan.io
After deployment, the contract is in the configured state. You must activate it before users can claim:
Step 7a: Transfer Redeemable Tokens
Transfer the required amounts of redeemable tokens to the proxy address:
# Option 1: If tokens are in a single address
# Simply transfer to the proxy address using your wallet or Optimistic Etherscan
# Option 2: If tokens are spread across multiple addresses
# Prepare those addresses to approve the contractStep 7b: Call startClaim() to Activate
The owner must call startClaim() to transition from configured to active state:
// If tokens are already in the contract (transferred manually):
claim.startClaim([]);
// If tokens need to be pulled from multiple addresses:
address[] memory holders = [treasuryAddress, multisigAddress];
claim.startClaim(holders);
// Note: holders must approve the contract first
// The function will pull all redeemableTokens from the specified holdersUsing cast (Foundry):
# If tokens already transferred to contract:
cast send $PROXY_ADDRESS "startClaim(address[])" "[]" \
--rpc-url $OPTIMISM_RPC \
--private-key $OWNER_KEY
# To pull from multiple sources (example with 2 addresses):
cast send $PROXY_ADDRESS "startClaim(address[])" \
"[$TREASURY_ADDRESS,$MULTISIG_ADDRESS]" \
--rpc-url $OPTIMISM_RPC \
--private-key $OWNER_KEYStep 7c: Verify State
# Check state (should return 2 for "active")
cast call $PROXY_ADDRESS "state()(uint8)" --rpc-url $OPTIMISM_RPC-
Test Thoroughly
- Deploy and test on local forked network
- Test all claim scenarios
- Test state transitions (configured → active → finialized)
- Test
createSnapshotToken()function - Test
burn()function for snapshot adjustments - Test
startClaim()function with various scenarios - Verify snapshot token creation timing
- Test blocklist functionality
- Test owner functions
- Confirm deadline mechanism works
- Test claiming before
startClaim()is called (should revert)
-
Verify All Parameters
- TEC Token (MiniMe) source address is correct for Optimism
- DAI address:
0xDA10009cBd5D07dd0CeCc66161FC93D7c9000da1 - RETH address:
0x9Bcef72be871e61ED4fBbc7630889beE758eb81D - Claim deadline is set correctly (Unix timestamp)
- Owner address is secure (recommend Gnosis Safe multisig)
-
Security Considerations
- Owner is a multisig wallet (recommended)
- Private key for deployment is secure
- Sufficient ETH in deployer account (~0.01 ETH for gas)
- Contract has been audited (recommended for production)
-
Prepare Token Transfers
- Calculate exact token amounts to transfer
- Prepare DAI transfer transaction
- Prepare RETH transfer transaction
- Have token approvals ready if using multisig
- Decide strategy for
startClaim(): direct transfer or pull from multiple sources
- Compile contracts:
npx hardhat compile - Deploy to Optimism:
npx hardhat ignition deploy ignition/modules/TECClaim.ts --network optimism --parameters ignition/parameters/optimism.json - Save deployment addresses (especially proxy address)
- Verify on Optimistic Etherscan:
npx hardhat ignition verify <deployment-id> - Verify contract is in
configuredstate (state should be 1) - Verify snapshot token was created (
token()should return non-zero address) - (Optional) Call
burn()to adjust specific addresses' snapshot balances if needed - Set TECClaim as controller of the snapshot token (already done automatically by
createSnapshotToken()) - Transfer redeemable tokens to proxy address OR prepare approval if using
startClaim()to pull tokens - Call
startClaim()from owner address to activate claiming - Verify contract is in
activestate (state should be 2) - Verify TECClaim is the controller of the snapshot token
- Test with small claim (if possible)
After deployment, verify:
- Contract is verified on Optimistic Etherscan
- Owner address is correct (
owner()view function) - Claim deadline is set correctly (
claimDeadline()view function) - Source TEC Token address is correct (
parentToken()view function) - Snapshot token was created successfully (
token()returns non-zero address) - Snapshot token is non-transferable (verify
transfersEnabled()returns false on snapshot) - Redeemable tokens array is correct
- Contract state is
configured(1) after deployment and snapshot creation - All redeemable tokens are transferred to proxy contract
-
startClaim()has been called by owner - Contract state is now
active(2) - Test claim with small amount (if possible)
- Document deployment addresses in secure location
- Update frontend/UI with new proxy contract address
- Set up monitoring for contract events (especially state transitions)
- Save deployment ID for future reference
createSnapshotToken() - Owner only
- Purpose: Creates a non-transferable snapshot of the parent TEC token
- When to use: After deployment, before activating claims
- Requirements:
- Must be called by owner
- Contract must be in
configuredstate - Can only be called once (snapshot creation is permanent)
- Effects: Creates a MiniMe clone token with transfers disabled, frozen at current block
- State: Remains in
configuredstate after execution
burn(address _owner, uint256 _amount) - Owner only
- Purpose: Burns snapshot tokens from a specific address before activation
- When to use: After creating snapshot but before calling
startClaim(), if you need to exclude or adjust balances - Parameters:
_owner: Address to burn tokens from_amount: Amount of snapshot tokens to burn
- Requirements:
- Must be called by owner
- Contract must be in
configuredstate - Snapshot token must already exist (call
createSnapshotToken()first)
- Effects: Reduces the snapshot token balance of the specified address
- State: Remains in
configuredstate after execution
startClaim(address[] from) - Owner only
- Purpose: Activates the claim contract by transitioning from
configuredtoactivestate - When to use: After deployment and funding the contract with redeemable tokens
- Parameters:
from: Array of addresses to pull tokens from (can be empty if already funded)
- Requirements:
- Must be called by owner
- Contract must be in
configuredstate - If pulling tokens, source addresses must have approved the contract for all redeemable tokens
- State transition:
configured→active - Behavior: Iterates through all
redeemableTokensconfigured during initialization and pulls their balances from each address in thefromarray
Example usage scenarios:
// Scenario 1: Tokens already transferred to contract manually
claim.startClaim([]);
// Scenario 2: Pull all redeemable tokens from a single treasury
address[] memory sources = new address[](1);
sources[0] = treasuryAddress;
claim.startClaim(sources);
// Scenario 3: Pull all redeemable tokens from multiple sources
address[] memory sources = new address[](3);
sources[0] = treasury1;
sources[1] = treasury2;
sources[2] = multisig;
claim.startClaim(sources);
// This will pull DAI and RETH (all redeemableTokens) from all 3 addressesclaim()
- Purpose: Users burn their TEC tokens to receive proportional redeemable tokens
- Requirements:
- Contract must be in
activestate - User must have TEC tokens
- User must not be blocklisted
- Contract must be in
- Effects: Burns user's TEC tokens, transfers proportional DAI and RETH
claimRemaining() - Owner only
- Purpose: After deadline, owner claims remaining unclaimed tokens
- Requirements:
- Contract must be in
activestate - Current time must be past
claimDeadline
- Contract must be in
- State transition:
active→finialized
blockAddresses(address[] users) - Owner only
- Purpose: Prevent specific addresses from claiming
- Requirements: Contract must be in
activestate
unblockAddresses(address[] users) - Owner only
- Purpose: Remove addresses from blocklist
- Requirements: Contract must be in
activestate
state()(uint8)
- Returns current contract state: 0=dormant, 1=configured, 2=active, 3=finialized
claimDeadline()(uint64)
- Returns Unix timestamp when claim period ends
blocklist(address)(bool)
- Returns whether an address is blocklisted
owner()(address)
- Returns contract owner address
To view deployment details:
# List all deployments
npx hardhat ignition deployments
# View specific deployment info
npx hardhat ignition status chain-10The deployment artifacts are stored in ignition/deployments/ directory.
The TECClaim contract uses the UUPS (Universal Upgradeable Proxy Standard) pattern. To upgrade:
-
Deploy new implementation contract:
# Update ignition module to deploy new implementation only npx hardhat ignition deploy ignition/modules/TECClaimV2.ts --network optimism -
Call
upgradeToAndCall()from owner address:// From owner address tecClaim.upgradeToAndCall(newImplementationAddress, "");
-
Verify new implementation on Optimistic Etherscan
Create a new Ignition module for upgrades:
// ignition/modules/UpgradeTECClaim.ts
import { buildModule } from "@nomicfoundation/hardhat-ignition/modules";
export default buildModule("UpgradeTECClaimModule", (m) => {
const proxyAddress = m.getParameter("proxyAddress");
const newImplementation = m.contract("TECClaimV2", []);
// Upgrade logic here
return { newImplementation };
});Important: Test upgrades thoroughly on local/testnet before mainnet!
Problem: Hardhat Ignition cannot find the ERC1967Proxy artifact.
Solution: This issue is resolved by using TECClaimFactory instead of directly deploying ERC1967Proxy. The factory handles proxy creation internally. Ensure contracts/TECClaimFactory.sol exists and is compiled:
npx hardhat compileThe factory will automatically create ERC1967Proxy instances when you call the create function.
- Check you have enough ETH for gas (at least 0.01 ETH on Optimism)
- Verify RPC URL is accessible and responding
- Try increasing gas limit in hardhat.config.ts
- Check network congestion and try again later
- Verify all addresses are valid checksummed addresses
- Use a checksumming tool if needed: https://ethsum.netlify.app/
- Double-check addresses match the correct network (Optimism vs Ethereum)
- Wait a few minutes and try verification again
- Check Optimistic Etherscan API is accessible
- Try manual verification on Optimistic Etherscan if auto-verification fails
- Ensure you're verifying with the correct compiler version (0.8.28)
- Ensure you're calling the proxy address, not the implementation address
- Check you have the correct owner address for owner-only functions
- Verify transaction is being sent from the owner address
- Check deadline hasn't passed for time-sensitive functions
- Ensure contract has been properly initialized
- If port 8545 is in use:
kill -9 $(lsof -ti:8545) - If forking fails, check your RPC URL and API limits
- Try clearing the fork cache:
rm -rf cache/edr-fork-cache/
-
Use Multisig for Owner:
- Deploy with owner as a Gnosis Safe multisig on Optimism
- Requires multiple signatures for sensitive operations
- Protects against single point of failure
-
Test Thoroughly:
- Test on local forked network first
- Run full test suite:
npx hardhat test - Test all edge cases and failure scenarios
- Verify upgrade mechanism if planning upgrades
-
Smart Contract Audit:
- Get professional audit before mainnet deployment
- Review audit findings and implement fixes
- Consider bug bounty program
-
Monitoring & Alerts:
- Set up monitoring for
Claimevents - Monitor
AddressBlocked/AddressUnblockedevents - Track contract token balances
- Set up alerts for unusual activity
- Set up monitoring for
-
Secure Key Management:
- Use hardware wallet for owner address
- Never share private keys
- Store deployment information securely
- Keep backups of all addresses and parameters
-
Documentation:
- Document all deployment addresses
- Save deployment IDs from Ignition
- Keep record of all configuration parameters
- Document upgrade procedures if applicable
- Contract Source:
contracts/TECClaim.sol - Test Suite:
contracts/TECClaim.t.sol - Ignition Module:
ignition/modules/TECClaim.ts - Hardhat Config:
hardhat.config.ts - OpenZeppelin UUPS: https://docs.openzeppelin.com/contracts/4.x/api/proxy#UUPSUpgradeable
- Hardhat Ignition Docs: https://hardhat.org/ignition/docs
- Optimistic Etherscan: https://optimistic.etherscan.io
For issues or questions:
- Check test suite for examples:
contracts/TECClaim.t.sol - Review contract code in detail:
contracts/TECClaim.sol - Check Hardhat Ignition documentation
- Review this deployment guide thoroughly