// SPDX-License-Identifier: UNLICENSED pragma solidity 0.8.28; import {ECDSA} from "solady/utils/ECDSA.sol"; interface IERC1967Factory { function deployDeterministicAndCall( address impl, address admin, bytes32 salt, bytes calldata data ) external payable returns (address); function predictDeterministicAddress(bytes32 salt) external view returns (address); } /// @title ProtocolDeployer — deploys the protocol on any chain, with no key of ours. /// /// THE ADDRESS IS THE IDENTITY. This contract is landed through the canonical /// CREATE2 deployer, so its address is a hash of its own initcode -- which /// carries the publisher set. Nobody can put different code at that address, /// and the same initcode lands at the SAME ADDRESS ON EVERY CHAIN. The four /// protocol proxies then derive from THIS contract's address (solady's factory /// requires the salt's top 20 bytes to equal msg.sender), so the whole protocol /// address space is a pure function of this bytecode. /// /// WHAT AUTHORISES A DEPLOY. Not a key we hold at deploy time: a 2-of-3 /// publisher signature over (this deployer, release, step index, salt, initcode /// hash). Anyone may broadcast it, and pay for it. /// /// THE DIGEST DELIBERATELY OMITS block.chainid. That is the whole point: a /// signature is produced ONCE and is valid on every chain for ever, so /// extending the protocol to a new chain costs us nothing and needs nothing /// from us. A signature cannot be replayed to authorise a DIFFERENT step, /// salt, admin or release, because all of those are inside the digest. /// /// REVOCATION is per chain and that is deliberate -- you cannot un-sign, so /// `setMinRelease` is the retraction, and a chain may need retracting alone. /// It too is publisher-signed and anyone may broadcast it. /// /// NO ADMIN. No owner, no upgrade path, no delegatecall, no selfdestruct. The /// publishers can authorise deploys and retract releases; they cannot change /// this code, because changing it would change this address. contract ProtocolDeployer { /// Same on every chain. A second deployer or factory is a second address space. address internal constant CREATE2_DEPLOYER = 0x4e59b44847b379578588920cA78FbF26c0B4956C; address internal constant ERC1967_FACTORY = 0x0000000000006396FF2a80c067f99B3d2Ab4Df24; bytes32 public constant DOMAIN = keccak256("defiloops.ProtocolDeployer.v1"); uint256 public constant THRESHOLD = 2; uint8 internal constant KIND_CODE = 1; uint8 internal constant KIND_PROXY = 2; uint8 internal constant KIND_REVOKE = 3; /// The publisher set, in this contract's initcode and therefore in its address. /// /// THREE, NOT ONE. A single immutable publisher is a permanent single point /// of failure: lose that key and this deployer is dead on every chain, for /// ever, with no upgrade path to fix it. address public immutable pubA; address public immutable pubB; address public immutable pubC; /// Per release, so a retracted release does not strand the next one midway. mapping(uint64 release => uint64 next) public cursor; uint64 public minRelease; error NotInOrder(uint64 want, uint64 got); error ReleaseRetracted(uint64 release, uint64 min); error SignatureExpired(uint64 validUntil); error NotEnoughPublishers(uint256 got); error SignersNotSorted(address prev, address next); error NotAPublisher(address who); error DeployFailed(bytes32 salt); error NotAnAdvance(uint64 got, uint64 min); error DuplicatePublisher(); event Deployed(uint64 indexed release, uint64 indexed step, address addr, bytes32 codehash); event AlreadyPresent(uint64 indexed release, uint64 indexed step, address addr); event MinReleaseSet(uint64 minRelease); constructor(address a, address b, address c) { if (a == address(0) || b == address(0) || c == address(0)) { revert NotAPublisher(address(0)); } if (a == b || b == c || a == c) revert DuplicatePublisher(); pubA = a; pubB = b; pubC = c; } /* ------------------------------------------------------------ deploying */ /// Deploy one plain CREATE2 step. ANYONE may call this. function step( uint64 release, uint64 index, bytes32 salt, bytes calldata initcode, uint64 validUntil, bytes[] calldata sigs ) external returns (address addr) { _requireOrder(release, index); _requireLive(release, validUntil); _requirePublishers(stepDigest(release, index, salt, keccak256(initcode), validUntil), sigs); cursor[release] = index + 1; addr = predict(salt, keccak256(initcode)); // ALREADY THERE IS NOT A FAILURE. A CREATE2 address is a hash of the // initcode, so code at the predicted address IS this bytecode -- whoever // paid for it. Advancing rather than reverting is what lets a stranger // front-run a step harmlessly. if (addr.code.length != 0) { emit AlreadyPresent(release, index, addr); return addr; } (bool ok,) = CREATE2_DEPLOYER.call(abi.encodePacked(salt, initcode)); if (!ok || addr.code.length == 0) revert DeployFailed(salt); emit Deployed(release, index, addr, addr.codehash); } /// Deploy one of the protocol proxies, at a salt only THIS contract can use. function stepProxy( uint64 release, uint64 index, uint96 suffix, address impl, address admin, bytes calldata initcall, uint64 validUntil, bytes[] calldata sigs ) external returns (address proxy) { _requireOrder(release, index); _requireLive(release, validUntil); _requirePublishers( proxyDigest(release, index, suffix, impl, admin, keccak256(initcall), validUntil), sigs ); cursor[release] = index + 1; bytes32 salt = proxySalt(suffix); proxy = IERC1967Factory(ERC1967_FACTORY).predictDeterministicAddress(salt); if (proxy.code.length != 0) { emit AlreadyPresent(release, index, proxy); return proxy; } IERC1967Factory(ERC1967_FACTORY).deployDeterministicAndCall(impl, admin, salt, initcall); if (proxy.code.length == 0) revert DeployFailed(salt); emit Deployed(release, index, proxy, proxy.codehash); } /* ----------------------------------------------------------- retracting */ /// Retract every release below `newMin`. Publisher-signed; anyone broadcasts. function setMinRelease(uint64 newMin, uint64 validUntil, bytes[] calldata sigs) external { if (newMin <= minRelease) revert NotAnAdvance(newMin, minRelease); if (validUntil != 0 && block.timestamp > validUntil) revert SignatureExpired(validUntil); _requirePublishers( keccak256(abi.encode(DOMAIN, KIND_REVOKE, address(this), newMin, validUntil)), sigs ); minRelease = newMin; emit MinReleaseSet(newMin); } /* --------------------------------------------------------------- digests * * NO block.chainid ANYWHERE BELOW. `address(this)` is here instead, and it * is the same on every chain while differing between two deployers with * different publishers -- which is exactly the scope a signature should * have. */ function stepDigest( uint64 release, uint64 index, bytes32 salt, bytes32 initcodeHash, uint64 validUntil ) public view returns (bytes32) { return keccak256( abi.encode( DOMAIN, KIND_CODE, address(this), release, index, salt, initcodeHash, validUntil ) ); } function proxyDigest( uint64 release, uint64 index, uint96 suffix, address impl, address admin, bytes32 initcallHash, uint64 validUntil ) public view returns (bytes32) { return keccak256( abi.encode( DOMAIN, KIND_PROXY, address(this), release, index, suffix, impl, admin, initcallHash, validUntil ) ); } /* ----------------------------------------------------------- derivations */ /// solady's factory refuses a salt whose top 20 bytes are neither zero nor /// the caller. Prefixing with this contract makes these salts ours alone. function proxySalt(uint96 suffix) public view returns (bytes32) { return bytes32(uint256(uint160(address(this))) << 96 | uint256(suffix)); } function predict(bytes32 salt, bytes32 initcodeHash) public pure returns (address) { return address( uint160( uint256( keccak256(abi.encodePacked(bytes1(0xff), CREATE2_DEPLOYER, salt, initcodeHash)) ) ) ); } /* ---------------------------------------------------------------- checks */ function _requireOrder(uint64 release, uint64 index) internal view { uint64 want = cursor[release]; if (index != want) revert NotInOrder(want, index); } function _requireLive(uint64 release, uint64 validUntil) internal view { if (release < minRelease) revert ReleaseRetracted(release, minRelease); if (validUntil != 0 && block.timestamp > validUntil) revert SignatureExpired(validUntil); } /// STRICTLY INCREASING SIGNER ADDRESSES. That is what makes the threshold a /// count of PUBLISHERS rather than a count of signatures -- without it, the /// same publisher's signature twice reaches two-of-three. function _requirePublishers(bytes32 digest, bytes[] calldata sigs) internal view { if (sigs.length < THRESHOLD) revert NotEnoughPublishers(sigs.length); address prev; for (uint256 i; i < sigs.length; i++) { address signer = ECDSA.recoverCalldata(digest, sigs[i]); if (signer <= prev) revert SignersNotSorted(prev, signer); if (signer != pubA && signer != pubB && signer != pubC) revert NotAPublisher(signer); prev = signer; } } }