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¶
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¶
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 |
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 |