Two pieces of pure on-chain infrastructure reused by essentially every project:
Create2Factory— deterministic (CREATE2) contract deployment with front-run-safe salt namespacing.Multicall3— a Multicall3-compatible call aggregator for batching reads and writes into a single transaction.
Both are permissionless, stateless utilities. Neither holds privileges, ownership, or funds across a call. They are safe to deploy once per chain and share as public singletons.
Foundry, Solidity 0.8.26, via-ir, cancun, OpenZeppelin v5.0.2 remappings. 35 tests, all green.
CREATE2 lets you compute a contract's address before you deploy it — the address depends only on the factory address, a salt, and the init-code hash, never on a nonce. This makes it possible to reserve the same contract address across many chains, or to reference a contract at a known address before it exists.
A naive CREATE2 factory derives the address from the raw salt alone. That salt is public in the mempool,
so an attacker can watch for your deploy transaction, copy your salt and bytecode, and land the same
address first — either griefing you (your later deploy now reverts on collision) or, worse, occupying
your predicted address with their bytecode.
This factory closes that hole by namespacing the salt with the caller:
effectiveSalt = keccak256(abi.encodePacked(msg.sender, rawSalt))
The address any account can reach is derived from a salt that includes its own address, so:
- Two different senders using the same raw salt always get different addresses — no cross-user collision.
- An attacker can never occupy the address you predicted, because reproducing it would require deploying as you.
computeAddressapplies the identical namespacing, so the predicted address always equals whatdeployactually produces for that same caller.
This is the safer default. The trade-off: the deployment address depends on who calls deploy, so a
multi-chain vanity/reserved address must be deployed from the same EOA (or the same wrapper) on every chain.
| Function | Purpose |
|---|---|
deploy(uint256 value, bytes32 salt, bytes bytecode) payable → address |
CREATE2-deploy bytecode, forwarding value wei to the constructor. Requires msg.value == value. Reverts DeployFailed if CREATE2 returns the zero address (salt collision or a reverting constructor). Emits Deployed(addr, salt, msg.sender). |
deployAndCall(value, salt, bytecode, initData) payable → address |
deploy, then invoke initData on the fresh contract; bubbles the inner revert reason if the init call fails. |
computeAddress(bytes32 salt, bytes32 bytecodeHash) view → address |
Predict the address deploy will produce for the caller (caller-namespaced). Off-chain, call with from set to the intended deployer. |
computeAddressFrom(address deployer, salt, bytecodeHash) view → address |
Predict the address for an explicit deployer. |
effectiveSalt(address deployer, bytes32 salt) pure → bytes32 |
The namespaced salt actually fed to CREATE2. |
deployedCodeSize(address) view → uint256 |
Runtime code size at an address (0 for EOAs / not-yet-deployed). |
computeAddress uses the standard formula — address(uint160(uint256(keccak256(0xff ++ factory ++ effectiveSalt ++ bytecodeHash)))) — and a fuzz test asserts it matches the address deploy produces for arbitrary salts, deployers, and constructor args.
Batch many calls into one transaction. Same surface as the canonical Multicall3, so existing tooling and ABIs work unchanged.
| Function | Failure semantics |
|---|---|
aggregate(Call[]) → (blockNumber, bytes[] returnData) |
Reverts if any call fails (bubbles the reason). Return data in input order. |
tryAggregate(bool requireSuccess, Call[]) → Result[] |
Per-call {success, returnData}. Reverts only if requireSuccess and a call fails. |
tryBlockAndAggregate / blockAndAggregate |
As above, plus block number and previous block hash. |
aggregate3(Call3[]) → Result[] |
Per-call allowFailure: a failing call reverts the whole batch unless its allowFailure is set. |
aggregate3Value(Call3Value[]) payable → Result[] |
Per-call value. Requires Σ value == msg.value. |
Structs: Call{target, callData}, Call3{target, allowFailure, callData},
Call3Value{target, allowFailure, value, callData}, Result{success, returnData}.
View helpers: getBlockNumber, getBlockHash, getLastBlockHash, getCurrentBlockTimestamp,
getCurrentBlockGasLimit, getCurrentBlockCoinbase, getBasefee, getChainId, getEthBalance.
aggregate3Value sums the per-call value fields and requires the total to equal msg.value. Every
wei sent in is routed out to a callee within the same transaction; a mismatch reverts. The contract can
therefore never accumulate or leak ETH across a call, which is what makes it safe as a shared, permissionless
singleton — there is no balance for anyone to drain, and no state for anyone to corrupt. Calls use low-level
.call, so results (including revert data) are aggregated faithfully.
Permissionless, stateless utilities:
- No owner, no admin, no upgradeability. Nothing to pause, no privileged path.
- No storage. Neither contract keeps state between transactions.
- No trapped funds. The factory forwards constructor value with a
msg.value == valueguard; the multicall forwards per-call value with aΣ value == msg.valueguard. - Callers are responsible for the bytecode they deploy and the calls they batch. These contracts add no
trust assumptions of their own beyond correct CREATE2 /
.callforwarding.
forge build --sizes # both contracts are ~1-3 KB runtime, well under the EIP-170 24 KB limit
forge test # 35 tests
forge fmt
forge snapshot # gas snapshot (advisory in CI)- Create2Factory — deploy address equals
computeAddress; redeploying the same salt+bytecode reverts (collision); different salt → different address; two senders with the same raw salt get different addresses (front-run cannot steal an address); value forwarded to a payable constructor andvalue-mismatch reverts;deployAndCallruns init and bubbles reverts; constructor revert surfacesDeployFailed; bytecode-hash mismatch changes the predicted address; fuzz over salts/deployers/args (computeAddressalways matchesdeploy) and fuzz that different senders never collide. - Multicall3 —
aggregatereturns data in order and reverts on any failure;tryAggregate(false)returns success flags without reverting;aggregate3allowFailurelets a bad call through while others succeed;aggregate3Valueroutes per-call value and reverts on a value-sum mismatch (over- and under-funded); no-funds-trapped property; view helpers return correct chain data; fuzz over batches preserving order and per-call success.
A mutation sanity check (removing the salt namespacing, and separately removing the value-sum check) confirms tests fail on the mutant and pass once reverted.
forge script script/Deploy.s.sol:Deploy --rpc-url <RPC> --broadcastBoth contracts take no constructor args and need no post-deploy configuration.
A second, adversarial pass focused on ETH custody, address prediction under reentrancy, and value routing.
Real bug found and fixed — aggregate3Value stranded ETH. When a call carried value > 0 and had
allowFailure = true, a revert in that sub-call rolled back only its ETH transfer (the funds stayed in the
aggregator) while valAccumulator still counted the earmarked value. The msg.value == valAccumulator check
therefore passed and the batch "succeeded" with ETH trapped in the contract. Because that same sum-check bars
anyone from ever forwarding more than msg.value, the trapped ETH was unrecoverable — a permanent leak that
directly contradicted the contract's stated "never retains a balance" invariant. Fix: track value actually
sent (valSent) and refund valAccumulator - valSent to the caller at the end, so the contract keeps nothing
of msg.value regardless of the failure mix. Regression tests fail on the pre-fix contract and pass after.
Reviewed and found sound (no change needed):
computeAddress/computeAddressFromreproducedeploy's caller-namespaced salt exactly; prediction never diverges from the landed address (fuzzed across salts/deployers/args).- A reverting constructor and a salt/bytecode collision both surface
DeployFailed; a faileddeployreverts the whole tx, so forwardedvalueis refunded, never stranded in the factory. deployAndCallreverts (bubbling the reason) if init fails, so there is no half-deployed-and-stuck state.- A constructor reentering the factory is namespaced by its own address (the reentrant
msg.sender), so it can never land in another deployer's namespace; the stateless factory holds no value to drain. - Multicall value-reuse: a malicious target cannot reenter to spend the aggregator's transient balance — the
value-sum check rejects any reentry that forwards more than its own fresh
msg.value. - Metamorphic redeploy (CREATE2 +
SELFDESTRUCT) is effectively neutralized on Cancun by EIP-6780; addresses remain deterministic regardless.
Coverage added: stranded-value refund (regression) + all-allowed-failures full refund + a fuzzed
allow-failure/value matrix; reentrant-constructor address namespacing; reentrant value-reuse cannot drain; and
an invariant (fail_on_revert = true, guarded bounds) asserting the aggregator never retains ETH across any
value split or failure mix. Suite: 35 -> 41 tests. Both contracts remain well under the EIP-170 24,576-byte
runtime limit (Create2Factory ~1.1 KB, Multicall3 ~3.1 KB).