ERC-3448: MetaProxy
ERC-3448 extends ERC-1167 to support immutable per-clone constructor arguments without runtime initialiser calls. The proxy's runtime bytecode reads the trailing bytes of its own code (an arbitrary ABI-encoded blob) and exposes them via a fixed selector. This eliminates the "deploy then initialize()" two-step that a vanilla ERC-1167 needs when each clone has a different config.
Required Pattern
// MetaProxy runtime: ERC-1167 prefix + delegatecall to impl + appended args
function deployProxy(address impl, bytes memory metadata) internal returns (address proxy) {
bytes memory creation = abi.encodePacked(
hex"363d3d373d3d3d3d60368038038091363936013d73",
impl,
hex"5af43d3d93803e603457fd5bf3",
metadata
);
assembly { proxy := create(0, add(creation, 0x20), mload(creation)) }
}
// Inside the implementation contract:
function getMetadata() public pure returns (bytes memory data) {
assembly {
let pos := sub(calldatasize(), 0x20)
let size := calldataload(pos)
data := mload(0x40)
mstore(data, size)
calldatacopy(add(data, 0x20), sub(pos, size), size)
mstore(0x40, add(add(data, 0x20), size))
}
}The proxy's appended bytes are part of its address derivation under CREATE2 — different metadata yields a different deterministic address. This is what makes MetaProxy fully drop-in for ERC-1167 use cases that previously needed a separate factory-side init call.
Neo Equivalent: Constant-slot Manifest Deploy
NeoVM doesn't have appended-runtime-bytes calldata semantics. The equivalent on Neo is to deploy a parameterised contract whose NEF constants are baked at deploy time. Same end-state (per-clone immutable args, deterministic address that includes the args) achieved by a different mechanism.
// The factory builds a per-clone NEF whose constant slots hold `metadata`,
// then deploys via ContractManagement.Deploy. Address determinism comes
// from sender + nonce + manifest name (which can encode the metadata hash).
public static UInt160 DeployMetaProxy(UInt160 impl, ByteString metadata)
{
var (nef, manifest) = BuildNefWithMetadata(impl, metadata);
var deployed = (Contract)ContractManagement.Deploy(nef, manifest);
OnDeployed(deployed.Hash, impl, metadata);
return deployed.Hash;
}| ERC-3448 (Ethereum) | Neo Equivalent | Notes |
|---|---|---|
MetaProxy.deploy(impl, metadata) | Factory.DeployMetaProxy(impl, metadata) with constants baked into NEF | One-shot deploy; no separate initialize() |
| Runtime trailing-bytes calldata trick | Constants in NEF reserved slots | Different mechanism, same affordance |
| Address determinism includes metadata | Manifest name / sender nonce includes metadata hash | Predictable address; different inputs → different address |
getMetadata() reads calldatasize() tail | GetMetadata() reads constant slot directly | Simpler; no assembly required |
| Init-free clone (no follow-up tx) | Init-free clone (constants applied at deploy) | Same UX |
When To Use vs Plain ERC-1167
- Plain ERC-1167 (see Pattern A on the ERC-1167 page) when every clone is identical and any per-clone config can be held in storage set by a follow-up
initialize()call. - MetaProxy when each clone has immutable config (vault token address, NFT collection name, multisig threshold) that should be unchangeable post-deploy and visible at the proxy's address.
The Neo factory pattern collapses both into one: the parameterised NEF deploy handles plain clones (no metadata) and meta-proxies (with metadata) the same way. The only difference is whether the factory injects metadata into the constant slots.
Migration Notes
Solidity contracts using OpenZeppelin's Clones.cloneDeterministicWithImmutableArgs or Vectorized's LibClone.deployERC1967WithImmutableArgs map directly to the Neo factory pattern above. The getImmutableArgs() getter on the implementation becomes a simple GetMetadata() returning a constant — no assembly tricks needed.
