Deployment
A deploy declaration runs once, when the contract is created. It plays the part of a Solidity constructor:
error FeeTooHigh of uint;
storage is record {
owner : address;
feeBps : uint;
}
event Configured of record {
indexed owner : address;
feeBps : uint;
}
deploy(const owner : address; const feeBps : uint)
raises FeeTooHigh
is {
assert(feeBps <= 1000, FeeTooHigh(feeBps));
storage.owner := owner;
storage.feeBps := feeBps;
emit Configured {
owner = owner;
feeBps = feeBps;
};
}
external view function owner() : address is
storage.owner- A contract has at most one
deploy. Without one, the contract starts with every field at zero. deploycan read its arguments and theEvmcontext, write storage and immutables, emit events, and raise errors. A raised error reverts the deployment.- It cannot
queryordefer. The contract has no code untildeployreturns, so no one can call it back, and a continuation would have nothing to return to. - It declares no return type. The deployment returns the contract's code.
- It cannot be called afterwards, and it is not in the ABI as a function.
- An imported file cannot have a
deploy. It belongs to the contract being compiled.
Arguments
The arguments are ABI-encoded and appended to the creation code, as with a Solidity constructor. The ABI lists them as a constructor entry, so Foundry, ethers, and viem deploy a Lithe contract as they would a Solidity one:
forge create Vault --constructor-args 0xOwner 30Malformed or missing arguments revert the deployment. To deploy from another contract, pass the arguments to defer create.
Ether
payable deploy accepts ether sent with the deployment, and may read Evm.value(). A plain deploy rejects ether:
import "std/evm";
storage is record {
seeded : wei;
}
payable deploy() is {
storage.seeded := Evm.value();
}
external view function seeded() : wei is
storage.seededimport "std/evm";
storage is record {
seeded : wei;
}
deploy() is {
storage.seeded := Evm.value();
}
external view function seeded() : wei is
storage.seeded`deploy` reads `Evm.value()`; write `payable deploy`Upgradeable contracts
An upgradeable contract lives behind a proxy. Its creation code only creates the implementation, whose storage nobody uses. So in an upgradeable contract, deploy sets up the proxy instead:
import "std/evm";
upgradeable
error NotOwner;
storage is record {
owner : address;
cap : uint;
}
upgrade guard is {
assert(Evm.sender() = storage.owner, NotOwner);
}
deploy(const owner : address; const cap : uint) is {
storage.owner := owner;
storage.cap := cap;
}
external view function cap() : uint is
storage.capHere deploy compiles to an entrypoint, deploy(address,uint256). The proxy's constructor calls it, as OpenZeppelin's ERC1967Proxy(implementation, data) does with data. It runs against the proxy's storage, in the same transaction that creates the proxy, so no one can initialize the proxy first:
new ERC1967Proxy(implementation, abi.encodeCall(Vault.deploy, (owner, cap)));Its immutables are kept in the proxy's storage, so deploy writes them there too.
It runs only while the proxy has no code yet, which is only during the proxy's own creation. Called any other way, through a deployed proxy or on the implementation itself, it reverts. It needs no "initialized" flag in storage.
Later versions keep their deploy for new proxies. To set up new state in proxies that already exist, use a migrate instead.

