Skip to content

Instantly share code, notes, and snippets.

@pcaversaccio
Last active June 16, 2025 09:56
Show Gist options
  • Save pcaversaccio/0411f521eb923dd1159ed483e2d7d564 to your computer and use it in GitHub Desktop.
Save pcaversaccio/0411f521eb923dd1159ed483e2d7d564 to your computer and use it in GitHub Desktop.
A step-by-step guide to deploy a 1-out-of-1 Safe `1.4.1` contract via `CreateX`.

Safe 1.4.1 Deployment via CreateX

Special thanks go to Richard Meissner for feedback and review.

Important

This approach requires a deep understanding of how the Safe contracts and CreateX mechanisms work. The recommended way to deploy a Safe contract remains through the SafeProxyFactory contract.

We will deploy a 1-out-of-1 1.4.1 Safe contract via CreateX.

setup Function

Note

Ensure that cast is installed locally. For installation instructions, refer to this guide.

Firstly, we need to make sure that the setup function of the Safe contract is called with the correct parameters:

/**
 * @notice Sets an initial storage of the Safe contract.
 * @dev This method can only be called once.
 *      If a proxy was created without setting up, anyone can call setup and claim the proxy.
 * @param _owners List of Safe owners.
 * @param _threshold Number of required confirmations for a Safe transaction.
 * @param to Contract address for optional delegate call.
 * @param data Data payload for optional delegate call.
 * @param fallbackHandler Handler for fallback calls to this contract
 * @param paymentToken Token that should be used for the payment (0 is ETH)
 * @param payment Value that should be paid
 * @param paymentReceiver Address that should receive the payment (or 0 if tx.origin)
 */
function setup(
    address[] calldata _owners,
    uint256 _threshold,
    address to,
    bytes calldata data,
    address fallbackHandler,
    address paymentToken,
    uint256 payment,
    address payable paymentReceiver
) external {
    // setupOwners checks if the Threshold is already set, therefore preventing that this method is called twice
    setupOwners(_owners, _threshold);
    if (fallbackHandler != address(0)) internalSetFallbackHandler(fallbackHandler);
    // As setupOwners can only be called if the contract has not been initialized we don't need a check for setupModules
    setupModules(to, data);

    if (payment > 0) {
        // To avoid running into issues with EIP-170 we reuse the handlePayment function (to avoid adjusting code of that has been verified we do not adjust the method itself)
        // baseGas = 0, gasPrice = 1 and gas = payment => amount = (payment + 0) * 1 = payment
        handlePayment(payment, 0, 1, paymentToken, paymentReceiver);
    }
    emit SafeSetup(msg.sender, _owners, _threshold, to, fallbackHandler);
}
  • _owners = [0xYourSignerAddress],
  • _threshold = 1,
  • to = 0xBD89A1CE4DDe368FFAB0eC35506eEcE0b1fFdc54 (SafeToL2Setup),
  • data = cast calldata "setupToL2(address)" "0x29fcB43b46531BcA003ddC8FCB67FFE91900C762" (SafeL2) = 0xfe51f64300000000000000000000000029fcb43b46531bca003ddc8fcb67ffe91900c762,
  • fallbackHandler = 0xfd0732Dc9E303f09fCEf3a7388Ad10A83459Ec99 (CompatibilityFallbackHandler),
  • paymentToken = 0x0000000000000000000000000000000000000000,
  • uint256 = 0,
  • paymentReceiver = 0x0000000000000000000000000000000000000000.

Example:

cast calldata "setup(address[],uint256,address,bytes,address,address,uint256,address)" \
    "[0x9F3f11d72d96910df008Cfe3aBA40F361D2EED03]" "1" "0xBD89A1CE4DDe368FFAB0eC35506eEcE0b1fFdc54" \
    "0xfe51f64300000000000000000000000029fcb43b46531bca003ddc8fcb67ffe91900c762" \
    "0xfd0732Dc9E303f09fCEf3a7388Ad10A83459Ec99" "0x0000000000000000000000000000000000000000" \
    "0" "0x0000000000000000000000000000000000000000"

returns:

0xb63e800d00000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000bd89a1ce4dde368ffab0ec35506eece0b1ffdc540000000000000000000000000000000000000000000000000000000000000140000000000000000000000000fd0732dc9e303f09fcef3a7388ad10a83459ec9900000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000009f3f11d72d96910df008cfe3aba40f361d2eed030000000000000000000000000000000000000000000000000000000000000024fe51f64300000000000000000000000029fcb43b46531bca003ddc8fcb67ffe91900c76200000000000000000000000000000000000000000000000000000000

CreateX Deployment

We will call the CreateX function deployCreate2AndInit(bytes32,bytes,bytes,tuple(uint256,uint256)):

/**
 * @dev Deploys and initialises a new contract via calling the `CREATE2` opcode and using the
 * salt value `salt`, creation bytecode `initCode`, the initialisation code `data`, the struct for
 * the `payable` amounts `values`, and `msg.value` as inputs. In order to save deployment costs,
 * we do not sanity check the `initCode` length. Note that if `values.constructorAmount` is non-zero,
 * `initCode` must have a `payable` constructor, and any excess ether is returned to `msg.sender`.
 * @param salt The 32-byte random value used to create the contract address.
 * @param initCode The creation bytecode.
 * @param data The initialisation code that is passed to the deployed contract.
 * @param values The specific `payable` amounts for the deployment and initialisation call.
 * @return newContract The 20-byte address where the contract was deployed.
 * @custom:security This function allows for reentrancy, however we refrain from adding
 * a mutex lock to keep it as use-case agnostic as possible. Please ensure at the protocol
 * level that potentially malicious reentrant calls do not affect your smart contract system.
 */
function deployCreate2AndInit(
    bytes32 salt,
    bytes memory initCode,
    bytes memory data,
    Values memory values
) public payable returns (address newContract) {
    // Note that the safeguarding function `_guard` is called as part of the overloaded function
    // `deployCreate2AndInit`.
    newContract = deployCreate2AndInit({
        salt: salt,
        initCode: initCode,
        data: data,
        values: values,
        refundAddress: msg.sender
    });
}

Important

Do NOT skip the following section! Read it carefully otherwise you put funds at risk on other chains.

It's important that we configure a permissioned deploy protection to prevent (malicious) actors from frontrunning and deploying a differently configured Safe contract on another chain:

  • salt = 0xYourDeployerAddress || 00 || random11Bytes = 0x9F3f11d72d96910df008Cfe3aBA40F361D2EED0300a6e4f5165b067e44956c9b (example).

The next step is to retrieve the initCode parameter:

  • initCode = abi.encodePacked(type(SafeProxy).creationCode, uint256(uint160(_singleton))) (SafeProxyFactory)

To compute the initCode, we first obtain the contract creation code via (0x4e1DCf7AD4e460CfD30791CCC4F9c8a4f820ec67 is the 1.4.1 SafeProxyFactory):

cast call 0x4e1DCf7AD4e460CfD30791CCC4F9c8a4f820ec67 "proxyCreationCode()(bytes)" --rpc-url https://1rpc.io/sepolia

which returns:

0x608060405234801561001057600080fd5b506040516101e63803806101e68339818101604052602081101561003357600080fd5b8101908080519060200190929190505050600073ffffffffffffffffffffffffffffffffffffffff168173ffffffffffffffffffffffffffffffffffffffff1614156100ca576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825260228152602001806101c46022913960400191505060405180910390fd5b806000806101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff1602179055505060ab806101196000396000f3fe608060405273ffffffffffffffffffffffffffffffffffffffff600054167fa619486e0000000000000000000000000000000000000000000000000000000060003514156050578060005260206000f35b3660008037600080366000845af43d6000803e60008114156070573d6000fd5b3d6000f3fea264697066735822122003d1488ee65e08fa41e58e888a9865554c535f2c77126a82cb4c0f917f31441364736f6c63430007060033496e76616c69642073696e676c65746f6e20616464726573732070726f7669646564

and uint256(uint160(_singleton)) is simply:

cast abi-encode "fn(address)" "0x41675C099F32341bf84BFc5382aF534df5C7461a"
0x00000000000000000000000041675c099f32341bf84bfc5382af534df5c7461a

Thus, we have:

  • initCode = 0x608060405234801561001057600080fd5b506040516101e63803806101e68339818101604052602081101561003357600080fd5b8101908080519060200190929190505050600073ffffffffffffffffffffffffffffffffffffffff168173ffffffffffffffffffffffffffffffffffffffff1614156100ca576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825260228152602001806101c46022913960400191505060405180910390fd5b806000806101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff1602179055505060ab806101196000396000f3fe608060405273ffffffffffffffffffffffffffffffffffffffff600054167fa619486e0000000000000000000000000000000000000000000000000000000060003514156050578060005260206000f35b3660008037600080366000845af43d6000803e60008114156070573d6000fd5b3d6000f3fea264697066735822122003d1488ee65e08fa41e58e888a9865554c535f2c77126a82cb4c0f917f31441364736f6c63430007060033496e76616c69642073696e676c65746f6e20616464726573732070726f766964656400000000000000000000000041675c099f32341bf84bfc5382af534df5c7461a,
  • data = YourEncodedSetupFunctionCall = 0xb63e800d00000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000bd89a1ce4dde368ffab0ec35506eece0b1ffdc540000000000000000000000000000000000000000000000000000000000000140000000000000000000000000fd0732dc9e303f09fcef3a7388ad10a83459ec9900000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000009f3f11d72d96910df008cfe3aba40f361d2eed030000000000000000000000000000000000000000000000000000000000000024fe51f64300000000000000000000000029fcb43b46531bca003ddc8fcb67ffe91900c76200000000000000000000000000000000000000000000000000000000 (example),
  • values = cast abi-encode "Values((uint256,uint256))" "(0,0)" = 0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 (optional, can be directly encoded in the next step).

For context, values is a struct defined in CreateX that encapsulates the payable amounts required for the contract deployment and subsequent initialisation call:

/**
 * @dev Struct for the `payable` amounts in a deploy-and-initialise call.
 */
struct Values {
    uint256 constructorAmount;
    uint256 initCallAmount;
}

At this point, we can fully encode the deployCreate2AndInit(bytes32,bytes,bytes,tuple(uint256,uint256)) function call (example):

cast calldata "deployCreate2AndInit(bytes32,bytes,bytes,(uint256,uint256))" \
    "0x9F3f11d72d96910df008Cfe3aBA40F361D2EED0300a6e4f5165b067e44956c9b" \
    "0x608060405234801561001057600080fd5b506040516101e63803806101e68339818101604052602081101561003357600080fd5b8101908080519060200190929190505050600073ffffffffffffffffffffffffffffffffffffffff168173ffffffffffffffffffffffffffffffffffffffff1614156100ca576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825260228152602001806101c46022913960400191505060405180910390fd5b806000806101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff1602179055505060ab806101196000396000f3fe608060405273ffffffffffffffffffffffffffffffffffffffff600054167fa619486e0000000000000000000000000000000000000000000000000000000060003514156050578060005260206000f35b3660008037600080366000845af43d6000803e60008114156070573d6000fd5b3d6000f3fea264697066735822122003d1488ee65e08fa41e58e888a9865554c535f2c77126a82cb4c0f917f31441364736f6c63430007060033496e76616c69642073696e676c65746f6e20616464726573732070726f766964656400000000000000000000000041675c099f32341bf84bfc5382af534df5c7461a" \
    "0xb63e800d00000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000bd89a1ce4dde368ffab0ec35506eece0b1ffdc540000000000000000000000000000000000000000000000000000000000000140000000000000000000000000fd0732dc9e303f09fcef3a7388ad10a83459ec9900000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000009f3f11d72d96910df008cfe3aba40f361d2eed030000000000000000000000000000000000000000000000000000000000000024fe51f64300000000000000000000000029fcb43b46531bca003ddc8fcb67ffe91900c76200000000000000000000000000000000000000000000000000000000" "(0,0)"

returns:

0xe96deee49f3f11d72d96910df008cfe3aba40f361d2eed0300a6e4f5165b067e44956c9b00000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000002e0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000206608060405234801561001057600080fd5b506040516101e63803806101e68339818101604052602081101561003357600080fd5b8101908080519060200190929190505050600073ffffffffffffffffffffffffffffffffffffffff168173ffffffffffffffffffffffffffffffffffffffff1614156100ca576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825260228152602001806101c46022913960400191505060405180910390fd5b806000806101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff1602179055505060ab806101196000396000f3fe608060405273ffffffffffffffffffffffffffffffffffffffff600054167fa619486e0000000000000000000000000000000000000000000000000000000060003514156050578060005260206000f35b3660008037600080366000845af43d6000803e60008114156070573d6000fd5b3d6000f3fea264697066735822122003d1488ee65e08fa41e58e888a9865554c535f2c77126a82cb4c0f917f31441364736f6c63430007060033496e76616c69642073696e676c65746f6e20616464726573732070726f766964656400000000000000000000000041675c099f32341bf84bfc5382af534df5c7461a000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001a4b63e800d00000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000bd89a1ce4dde368ffab0ec35506eece0b1ffdc540000000000000000000000000000000000000000000000000000000000000140000000000000000000000000fd0732dc9e303f09fcef3a7388ad10a83459ec9900000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000009f3f11d72d96910df008cfe3aba40f361d2eed030000000000000000000000000000000000000000000000000000000000000024fe51f64300000000000000000000000029fcb43b46531bca003ddc8fcb67ffe91900c7620000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000

Now we are finally ready to deploy it (the canonical CreateX address is 0xba5Ed099633D3B313e4D5F7bdc1305d3c28ba5Ed):

cast send 0xba5Ed099633D3B313e4D5F7bdc1305d3c28ba5Ed \
    0xe96deee49f3f11d72d96910df008cfe3aba40f361d2eed0300a6e4f5165b067e44956c9b00000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000002e0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000206608060405234801561001057600080fd5b506040516101e63803806101e68339818101604052602081101561003357600080fd5b8101908080519060200190929190505050600073ffffffffffffffffffffffffffffffffffffffff168173ffffffffffffffffffffffffffffffffffffffff1614156100ca576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825260228152602001806101c46022913960400191505060405180910390fd5b806000806101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff1602179055505060ab806101196000396000f3fe608060405273ffffffffffffffffffffffffffffffffffffffff600054167fa619486e0000000000000000000000000000000000000000000000000000000060003514156050578060005260206000f35b3660008037600080366000845af43d6000803e60008114156070573d6000fd5b3d6000f3fea264697066735822122003d1488ee65e08fa41e58e888a9865554c535f2c77126a82cb4c0f917f31441364736f6c63430007060033496e76616c69642073696e676c65746f6e20616464726573732070726f766964656400000000000000000000000041675c099f32341bf84bfc5382af534df5c7461a000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001a4b63e800d00000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000bd89a1ce4dde368ffab0ec35506eece0b1ffdc540000000000000000000000000000000000000000000000000000000000000140000000000000000000000000fd0732dc9e303f09fcef3a7388ad10a83459ec9900000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000009f3f11d72d96910df008cfe3aba40f361d2eed030000000000000000000000000000000000000000000000000000000000000024fe51f64300000000000000000000000029fcb43b46531bca003ddc8fcb67ffe91900c7620000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 \
    --rpc-url https://1rpc.io/sepolia --private-key $PRIVATE_KEY

Example deployment: 0xa066d99c4b98544f1c27e8bcfb9643c94ec2908e50459e25795d4ed29261d47b.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment