The nitty-gritty of Ethereum and Solidity: Smart contracts deployment.

Alberto Molina
Coinmonks
5 min readSep 24, 2023

--

Ethereum smart contracts can be deployed in two different ways: by using an EOA to submit a smart contract deployment transaction or by employing a smart contract that has already been deployed on the blockchain. In both cases, a specific type of message must be used; however, if we opt to use a smart contract to deploy another one, we will gain a bit more flexibility. In this blog, we will delve into both approaches and explore how they function.

Off-chain Deployment

Anyone can deploy a smart contract on Ethereum as long as they have an EOA (Externally Owned Account) with a public/private key pair and some ethers in it to cover the deployment fees. These fees depend on the size of the contract being deployed and the cost of executing its constructor.

The bytecode added to the transaction data field is referred to as the ‘deployment bytecode’ and can be deconstructed as follows :

  • CREATION CODE (Constructor) : Starting at instruction 0 within the deployment transaction code, its purpose is to generate and return the runtime code for the smart contract being deployed. This process involves the following steps.
    - Setting the Free Memory Pointer: It pushes the value 0x80 to the memory position 0x40.
    - Non-payable check : check that the transaction value field is 0, This step is executed only if constructor is not payable.
    - Retrieve constructor parameters: Input arguments are added at the very end of the bytecode (*).
    - Constructor Body: This section contains the constructor opcodes (which includes any internal or private method that is only called from the constructor) responsible for executing the constructor’s logic.
    -
    Copy runtime code to memory: It copies all the runtime code (located below) into memory.
    - Return runtime code : his action halts execution and returns the runtime code
  • RUNTIME CODE : Bytecode that stays on the blockchain.
  • METADATA HASH : This portion represents unreachable code and is NOT actual bytecode. Instead, it consists of bytes that form a hash of the contract’s metadata, including details like the compiler version and source code. The contract’s metadata can be stored in various locations, such as IPFS or Swarm, enabling anyone to download it, calculate its hash, and verify that it matches the Metadata Hash appended at the end of the bytecode.
  • (*) CONSTRUCTOR INPUT ARGUMENTS (if any) : Each parameter takes 32 bytes. It is encoded like any other input parameter in calldata.

The smart contract deployment transaction must be sent to address(0), the EVM will then automatically start executing the transaction calldata content, as if it were smart contract bytecode. The output of this execution will be stored in the blockchain as the new contract’s runtime bytecode (it will include the actual runtime bytecode and the metadata hash).

The smart contract address will be calculated using the address of the account submitting the transaction and its nonce so that it will always be predictable and unique.

Example

Let’s check the deployment bytecode of the following contract (providing “7” to the constructor method):

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.16;

contract DeploymentContract{
uint256 private value;

constructor(uint256 _value){
value = _value;
}

function setValue(uint256 _value) external{
value = _value;
}

function getValue() external view returns(uint256){
return value;
}
}

It will look like this:

This bytecode does not have any metadata hash, we only have the first three part : creation code, runtime code, input parameter.

You can do it yourself using Remix and setting the “Enable optimization” to 1.

On-chain Deployment

Smart contracts can also be deployed by other smart contracts; this is commonly referred to as “on-chain deployment.”

In order for a smart contract to deploy another one, it must have access to the deployment bytecode. It will append the encoded constructor input parameters at the end of it and then send an internal transaction to address 0 (just like it has to be done during an off-chain deployment). The EVM will then execute the deployment bytecode and create the new contract.

There are different ways to deploy a contract using Solidity:

Contract Code

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract DeployedContract{
uint256 private value;

constructor(uint256 _value){
value = _value;
}

function get()external view returns(uint256){
return value;
}
}

contract Deployer{
function deployNewContract(uint256 _value)external returns(address){
DeployedContract dc = new DeployedContract(_value);
return address(dc);
}
}

In the above example, “Deployer” contract deploys a new “DeployedContract” everytime the “deployNewContract” method is invoked.

“Deployer” has access to the “DeployedContract” deployment bytecode since they are both implemented in the same file, then it simply has to append the “value” recieved as an input parameter. Everything is done automatically by Solidity.

Create

We can also deploy contracts on-chain without having direct access to their deployment bytecode. Instead, we can ask the user to provide it, and then proceed with the deployment. To achieve this, we can utilize the “create” opcode, which is actually the opcode used behind the scenes in the previous example (‘Contract Code’).

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;


contract Deployer_Create {

function deployNewContract(bytes memory bytecode, uint256 value) external returns(address) {
bytes memory deploymentBytecode = abi.encodePacked(bytecode, abi.encode(value));

address newContractAddress;

assembly {
newContractAddress := create(0, add(deploymentBytecode, 0x20), mload(deploymentBytecode))
}

return newContractAddress;
}
}

It is important to note that when using “create” (as demonstrated in the above two examples), the address of the newly deployed contract is calculated using the address of the deploying contract and its current nonce, which increments right after the deployment. This implies that, similar to off-chain deployment, the new address is predictable and unique.

Create2

The “Create2” opcode provides the capability to deploy a smart contract on-chain to a specific pre-determined address. “Create2” operates similarly to the “Create” opcode, but the address of the newly deployed contract is not determined by the sender’s address and nonce. Instead, it is calculated using the sender’s address, the deployed bytecode, and a “salt” value provided by the sender. This unique feature allows us to redeploy the same contract to the same address.

contract Deployer_Create_2 {

function deployNewContract(bytes memory bytecode, uint256 value, bytes32 salt) external returns(address) {
bytes memory deploymentBytecode = abi.encodePacked(bytecode, abi.encode(value));

address newContractAddress;

assembly {
newContractAddress := create2(0, add(deploymentBytecode, 0x20), mload(deploymentBytecode), salt)
}

return newContractAddress;
}
}

If we reuse the same “salt,” “bytecode,” and “value,” the “deployNewContract” method would attempt to deploy a new contract to the same address. If a contract is already deployed at that address, the deployment will fail (return address 0); otherwise, it will succeed.

“Create2” is particularly useful when we need to deploy the same contract with the same address in different chains, like some ethereum testnet (Goerli, Sepolia, …) and the ethereum mainnet.

--

--