Skip to content

TTADeployer

TTADeployer is the factory contract that deploys Airdrop distributor instances on EVM chains, for both Merkle and ECDSA distributors. It deploys each instance as a minimal-proxy clone at a deterministic CREATE2 address, so you can predict the address before deployment.

The factory keeps a registry of implementation addresses for each distributor type, records the instance deployed for each project ID, and holds the fee configuration in one place: it sets the fee collector for each deployment, and the fee token is read from the fee collector. Clones are ERC-1167 minimal proxies, deployed with Solady's LibClone library, which keeps deployment gas low.

TTADeployer was named MDCreate2 until May 2025, when the contract was renamed and given an initializer. Some diagrams on this page still use the old name.

Architecture

Distributor types the TTADeployer clones (Merkle) Distributor types the TTADeployer clones (Merkle)
Distributor types the TTADeployer clones (Merkle)
Distributor types the TTADeployer clones (signature) Distributor types the TTADeployer clones (signature)
Distributor types the TTADeployer clones (signature)

Contract details

Storage

address public defaultFeeCollector;
mapping(string projectId => address deployment) public deployments;
mapping(uint8 mdType => address implementation) public implementations;
mapping(address deployment => address feeCollectors) internal _feeCollectors;

Functions

Deployment functions

Function Description
deploy() Deploys a new distributor instance
simulateDeploy() Predicts the deployment address without deploying
function deploy(uint8 mdType, string calldata projectId) external returns (address instance);

function simulateDeploy(uint8 mdType, string calldata projectId) external view returns (address);

deploy clones implementations[mdType] with the salt keccak256(abi.encode(projectId)), calls initialize(msg.sender) on the new instance in the same transaction, records it in deployments[projectId] and emits DidDeploy. The caller of deploy becomes the distributor's owner, and TTADeployer is recorded as the distributor's deployer, which the distributor later queries for fees.

Configuration functions

Function Description
setImplementation(uint8 mdType, address implementation) Registers the implementation for a distributor type
setDefaultFeeParams(address feeCollector) Sets the default fee collector
setDeploymentFeeParams(address deployment, address feeCollector) Sets the fee collector for one deployment
initialize(address newOwner) Sets the owner once, when TTADeployer itself is deployed

All configuration functions are owner only and emit no events.

View functions

Function Description
feeCollectors(address deployment) Returns the deployment's fee collector, or defaultFeeCollector if none is set
feeTokens(address deployment) Returns getFeeToken(deployment) from the deployment's fee collector
deployments(string projectId) Returns the instance deployed for a project ID
implementations(uint8 mdType) Returns the implementation registered for a type
defaultFeeCollector() Returns the default fee collector
version() Returns the factory version

Security

Access control

Role Permissions
Owner Register implementations, set fee parameters
Anyone Deploy distributors, simulate deployments

Deployment safety

Each project ID can be used only once; deploy reverts with UnsupportedOperation if it is already taken.

deploy does not check that an implementation is registered for mdType. Check that implementations(mdType) is not address(0) before deploying.

Each instance is initialized in the same transaction that creates it, so no one else can initialize it first. Implementation contracts disable their own initializers.

The salt depends only on the project ID, not on the caller, so whoever calls deploy first for a project ID owns that instance. Check owner() of the returned instance.

CREATE2 mechanics

Address calculation

TTADeployer deploys each instance, an ERC-1167 minimal proxy, with CREATE2. The address is:

address instance = address(uint160(uint256(keccak256(abi.encodePacked(
    bytes1(0xff),
    address(ttaDeployer),                      // sender (factory)
    keccak256(abi.encode(projectId)),          // salt
    keccak256(minimalProxyInitCode)            // ERC-1167 creation code that embeds implementations[mdType]
)))));

In practice, call simulateDeploy(mdType, projectId), which computes this with LibClone.predictDeterministicAddress.

Predictable addresses

Because the address is known before deployment, you can reference a distributor before it exists. A project ID gives the same address on different chains, as long as the TTADeployer and implementation addresses are the same on both chains (they are deployed with CREATE3 and a salt derived from their version string) and the project ID is unused.

Fees

Fee flow

How a distributor finds and pays its fee collector during a claim How a distributor finds and pays its fee collector during a claim
How a distributor finds and pays its fee collector during a claim

When a claim is charged a fee, the distributor calls feeCollectors(address(this)) and feeTokens(address(this)) on the TTADeployer that deployed it, then asks the fee collector for the fee amount. See the Merkle distributor and signature distributor pages for how each variant charges fees.

Fee configuration

The default collector applies to every deployment without its own collector, including deployments made before the default was set, because the distributor reads it at claim time. A custom collector overrides the default for a specific deployment. Fee tokens come from the fee collector contract.

Versioning

The factory has its own version, separate from the distributor implementations. version() returns a string such as 0.5.6-TTADeployer, with an EVM suffix (for example -cancun) on builds compiled for a specific EVM version. Distributor implementations report versions such as 0.4.7-TokenTableMerkleDistributor.

TTADeployer itself is deployed through CreateX with CREATE3, with deployment and initialize in one transaction. Each new version is a new contract at a new address. Existing distributors keep using the TTADeployer that deployed them. Deployed addresses per chain are recorded in the tokentable-evm repository under deployments/airdrop/<chainId>-latest.json.

Events

Event Description
DidDeploy Emitted when a distributor is deployed
event DidDeploy(uint8 mdType, string projectId, address instance);

Errors

Error Condition Selector
UnsupportedOperation Project ID already used 0x9ba6061b
Unauthorized Configuration function called by a non-owner 0x82b42900

Implementation types

The type numbers come from the DistributorType enum in the deployment scripts (script/helpers/DeployHelper.sol); the contract itself accepts any uint8. Check implementations(mdType) on the target chain.

Type Description
0 TokenTableMerkleDistributor
1 TokenTableNativeMerkleDistributor
2 SimpleERC721MerkleDistributor
3 NFTGatedMerkleDistributor (disabled in the deployment scripts, not registered)
4 CustomFeesNativeMerkleDistributor
5 SimpleNoMintERC721MerkleDistributor
6 FungibleTokenECDSADistributor
7 FungibleTokenWithFeesECDSADistributor
8 TokenTableMerkleDistributorWithFees
9 TokenTableMerkleMultitokenDistributorBatched