Smart Contract Deployment Internals

Alexander Zammit
9 min readJun 21, 2023

Contract deployment transactions are unique in a number of ways. In this article we dissect such a transaction taking a close look at the bytecode giving birth to a new contract.

With whom are we Talking?

Transactions are an exchange between two parties.

On transacting the native cryptocurrency, the from/to addresses identify the initial and final currency owners. On running a smart contract, the transaction sender asks a smart contract to execute a function. Thus, the contract becomes the transaction recipient.

Likewise, on contract deployment, the transaction sender is the one creating the new contract on-chain. But who is the recipient? It is the blockchain itself! The blockchain designates a special address for anyone to ask for such a service. This special address is the null address.

Here is the Code, Deploy It

Having a deployment counterparty, we could just send the smart contract code to the null address, right? Not exactly! The deployment transaction data payload is a little bit more complicated, and for a good reason.

The reason is the constructor, a piece of code tasked with deployment validation and contract initialization. A constructor can…

  • …abort deployment if validation fails.
  • …provide one-time initialization for state variables.

Furthermore, the constructor only runs on deployment. Hence there is no point in saving it on-chain.

We are now ready to define the deployment transaction data payload, which clearly must integrate the constructor logic.

Indeed, the deployment payload is a slightly modified version of the constructor code. It includes all the constructor logic, but when executed successfully, it returns the smart contract portion to be written on-chain.

I talk of a smart contract portion, since as already stated the constructor is not written on-chain. The figure depicts the blockchain receiving a deployment transaction and executing the constructor. Finally, this either successful returns the contract code or reverts aborting deployment.

Smart Contract Deployment
Smart Contract Deployment

Deploying a Contract

It is now time to investigate the transaction deployment payload. I prepared a hardhat project with a simple contract.

Articles are fuelled by Coffee

Clone and Install Dependencies

To follow along, clone and install the project dependencies:

git clone git@github.com:kaxxa123/BlockchainThings.git
cd ./BlockchainThings/ContractBytecode

npm install

Configure and Compile

Next, we need to slightly customize the project:

  1. This code will run on any EVM compatible chain. Here hardhat was configured to run against the Avalanche Fuji testnet. Fuji was chosen because of its easy-to-use faucet that gives us 2 AVAX without any fuss. So, start by requesting some testnet AVAX.
  2. Under the ContractBytecode folder rename the file:
    From: ./BlockchainThings/ContractBytecode/.env_template
    To: ./BlockchainThings/ContractBytecode/.env
  3. Edit the .env to set the private key for the account to which the AVAX was sent. Once ready the content should look something like this:
    PRIVATE_KEY_1=”0x1234567890abcd…..”

We are now ready to compile the project:

npx hardhat compile

The Code

Next, check the contract code we will be playing with.

pragma solidity 0.8.18;

contract Demo {
address public owner;
uint public counter;

constructor(uint start) payable {
require (start > 100, "Too small");
owner = msg.sender;
counter = start;
}

function increase() external {
++counter;
}
}

The constructor:

  1. Takes one input parameter.
  2. Includes a require clause that could abort deployment.
  3. Initializes two state variables.

To simplify our example, the constructor is marked as payable. Otherwise, the compiler would inject a second require clause ensuring that no crypto amount is included with the deployment transaction. This would make the bytecode harder to follow.

Deploy

Before delving into the bytecode, let us see what data is required when deploying it using sendTransaction.

Earlier we compiled the smart contract, look for the resulting output under ./artifacts/contracts/Demo.sol/Demo.json

We are especially interested in:
bytecode - the complete contract code including the constructor.
deployedBytecode - the contract code portion that excludes the constructor.

Next, we fire a node.js console connected to Avalanche Fuji:

npx hardhat console --network fuji

Check that the .env file was configured correctly by retrieving your account address:

accounts = await ethers.getSigners()
accounts[0].address

Load the compilation output file:

fs = require("fs")
fs.readFile('./artifacts/contracts/Demo.sol/Demo.json', 'utf8',
(err, data) => compile = JSON.parse(data))

This will include the bytecode and deployedBytecode values:

compile.bytecode
compile.deployedBytecode

These values are formatted as follows:

compile.bytecode = 
Initialization Code (aka constructor) | Contract On-Chain Portion

compile.deployedBytecode =
Contract On-Chain Portion

If the constructor did not require any input parameters, we could send a transaction with compile.bytecode as payload. Since we do have a parameter, this must be appended to the bytecode. I will let ethers.js do this.

paramIn = 200
DemoFactory = await ethers.getContractFactory("Demo")
complete = await DemoFactory.getDeployTransaction(paramIn)
complete.data

The value in complete.data has everything we need and is formatted as follows:

complete.data = 
Initialization Code | Contract On-Chain Portion | Constructor Parameters

Let us confirm the values just discussed are truly formatted as described…

//Remove leading "0x" from the strings
bytecode = compile.bytecode.slice(2)
deployedBytecode = compile.deployedBytecode.slice(2)
bytecodeEx = complete.data.slice(2)

//Confirm that the bytecode ends with the deployedBytecode
assert(bytecode.length - bytecode.indexOf(deployedBytecode) ==
deployedBytecode.length)

//Confirm that bytecodeEx starts with bytecode
assert(bytecodeEx.indexOf(bytecode) == 0)

//Confirm that the constructor input parameter matches our input
//'00000000000000000000000000000000000000000000000000000000000000c8'
param = bytecodeEx.slice(bytecode.length)
assert(parseInt(param, 16) == paramIn)

Ok we are now ready to deploy the contract using sendTransaction with complete.data payload:

trn = await accounts[0].sendTransaction({to: null, data: complete.data})
receipt = await trn.provider.getTransactionReceipt(trn.hash)

receipt.contractAddress

And we verify the deployment, by running its functions:

abi = DemoFactory.interface.fragments
addr = receipt.contractAddress
demo = new ethers.Contract(addr, abi, accounts[0])

await demo.owner()
await demo.counter()
await demo.increase()
await demo.counter()

The Bytecode

The best way to see how the initialization code includes the constructor logic, is by stepping through the bytecode. Bytecode is not easy to read, but if we prepare ourselves with some key reference values, it gets easier. Here is a table of values that we will see popping while stepping through the code.

  Description                       Computation                    Hex Value

bytecode length (bytecodeEx.length/2).toString(16) 21f

bytecode length excluding (bytecode.length/2).toString(16) 1ff
constructor parameters

constructor parameter ((bytecodeEx.length - 20
length bytecode.length)/2).toString(16)

contract code length (deployedBytecode.length/2) 15b
.toString(16)

contract code offset ((bytecode.length -
within bytecode stream deployedBytecode.length)/2).toString(16) a4

constructor parameter (200).toString(16) c8

condition value in (100).toString(16) 64
require (start > 100, ...)

owner state variable 0
storage key

counter state variable 1
storage key

Next, we will step through the individual bytes, convert each opcode using a table like this, and for each opcode work out the stack state. I do not show the values held in memory, but the code is simple enough not to require this.

The bytecode order was also adjusted so that this can be read sequentially. Basically, my dump shows bytecode index 7c right after the jump at index 21 then goes back to index 22 when the code jumps back.

idx  bytecode   opcodes     stack               description

00 60 80 PUSH1 80 [80]
02 60 40 PUSH1 40 [40, 80] Save 80 to mem location 40
04 52 MSTORE []
05 60 40 PUSH1 40 [40]
07 51 MLOAD [80] Load memory location 40
08 61 01ff PUSH2 01ff [1ff, 80] 1ff - bytecode length
except ctr parameters
0b 38 CODESIZE [21f, 1ff, 80] push bytecode length 21f
0c 03 SUB [ 20, 80] Subtracting gives the ctr
parameters length
0d 80 DUP1 [20, 20, 80]
0e 61 01ff PUSH2 01ff [1ff, 20, 20, 80]
11 83 DUP4 [80, 1ff, 20, 20,
80]
12 39 CODECOPY [20, 80] Copy ctr param of size 20
@ 1ff to memory location 80

This code just copied the constructor input parameter to memory.

Stack Values:
20 — size of the ctr parameter
80—memory location of the ctr parameter

idx   bytecode    opcodes     stack               description

13 81 DUP2 [80, 20, 80]
14 01 ADD [a0, 80] Get memory pointer
following ctr parameter
15 60 40 PUSH1 40 [40, a0, 80]
17 81 DUP2 [a0, 40, a0, 80]
18 90 SWAP1 [40, a0, a0, 80]
19 52 MSTORE [a0, 80] Store memory pointer a0
to memory location 40

1a 61 0022 PUSH2 0022 [22, a0, 80]
1d 91 SWAP2 [80, a0, 22]
1e 61 007c PUSH2 007c [7c, 80, a0, 22]
21 56 JUMP [80, a0, 22] Jump to 7c

Stack Values:
a0 — next memory location following the ctr parameter
22 — “jump back” location to continue from where the code left
80 — memory location of the ctr parameter

idx   bytecode    opcodes     stack               description

7c 5b JUMPDEST [80, a0, 22]
7d 60 00 PUSH1 00 [00, 80, a0, 22]
7f 60 20 PUSH1 20 [20, 00, 80, a0,
22]
81 82 DUP3 [80, 20, 00, 80,
a0, 22]
82 84 DUP5 [a0, 80, 20, 00,
80, a0, 22]
83 03 SUB [20, 20, 00, 80, Get ctr parameter size
a0, 22]
84 12 SLT [00, 00, 80, a0, Is (top < top-1)?
22]
85 15 ISZERO [01, 00, 80, a0, Is (top == 00)?
22]
86 61 008e PUSH2 008e [8e, 01, 00, 80,
a0, 22]
89 57 JUMPI [00, 80, a0, 22] If (top-1!=0) Jump to 8e
8a 60 00 PUSH1 00
8c 80 DUP1
8d fd REVERT

This code verified the expected ctr parameters size.

Stack Values:
80 — memory location of the ctr parameter
a0 — memory location following the ctr parameter
22 — “jump back” location

idx   bytecode    opcodes     stack               description

8e 5b JUMPDEST [00, 80, a0, 22]
8f 50 POP [80, a0, 22]
90 51 MLOAD [c8, a0, 22] Load ctr parameter from
memory location 80
91 91 SWAP2 [22, a0, c8]
92 90 SWAP1 [a0, 22, c8]
93 50 POP [22, c8]
94 56 JUMP [c8] Jump back to location 22

Stack Values:
c8 — ctr input parameter value

idx   bytecode    opcodes     stack             description

22 5b JUMPDEST [c8]
23 60 64 PUSH1 64 [64, c8] 0x64 = 100, processing...
require (start > 100, ...)
25 81 DUP2 [c8, 64, c8]
26 11 GT [01, c8] Is (c8 > 64)?
27 61 0062 PUSH2 0062 [62, 01, c8]
2a 57 JUMPI [c8] If (top-1 != 0) Jump to 62

This code checked the require condition in:
require (start > 100, “Too small”)

If the check failed, the code would not jump and the code sequence that follows reverts.

Stack Values:
c8 — ctr input parameter value

idx     bytecode    opcodes                   description

2b 60 40 PUSH1 40 Following
2d 51 MLOAD this
2e 62 461bcd PUSH3 461bcd code
32 60 e5 PUSH1 e5 sequence
34 1b SHL it ultimately
35 81 DUP2 reverts
36 52 MSTORE as expected
37 60 20 PUSH1 20 from a
39 60 04 PUSH1 04 failed
3b 82 DUP3 require
3c 01 ADD clause
3d 52 MSTORE
3e 60 09 PUSH109
40 60 24 PUSH1 24
42 82 DUP3
43 01 ADD
44 52 MSTORE
45 68 151bdb PUSH9 151bdb
c81cdb c81cdb
585b1b 585b1b
4f 60 ba PUSH1 ba
51 1b SHL
52 60 44 PUSH1 44
54 82 DUP3
55 01 ADD
56 52 MSTORE
57 60 64 PUSH1 64
59 01 ADD
5a 60 40 PUSH1 40
5c 51 MLOAD
5d 80 DUP1
5e 91 SWAP2
5f 03 SUB
60 90 SWAP1
61 fd REVERT If require failed revert here.

When the require condition is satisfied, the code continues from here…

idx   bytecode    opcodes      stack               description

62 5b JUMPDEST [c8]
63 60 00 PUSH1 00 [00, c8]
65 80 DUP1 [00, 00, c8]
66 54 SLOAD [00, 00, c8] Load state value key 00
i.e. the owner address
67 60 01 PUSH1 01 [01, 00, 00, c8]
69 60 01 PUSH1 01 [01, 01, 00, 00,
c8]
6b 60 a0 PUSH1 a0 [a0, 01, 01, 00,
00, c8]
6d 1b SHL [10000000000..., Shift (top-1) left by
01, 00, 00, c8] a0=20*8=address length

6e 03 SUB [fffffffffff..., Created a 20 byte mask!
00, 00, c8]

6f 19 NOT [fff...00000000, Invert the top value
00, 00, c8] !(top)

70 16 AND [00, 00, c8] top AND (top-1)
71 33 CALLER [addr, 00, 00, c8] get caller address
72 17 OR [addr, 00, c8]
73 90 SWAP1 [00, addr, c8]
74 55 SSTORE [c8] Store address at slot 0
owner = msg.sender
75 60 01 PUSH1 01 [01, c8]
77 55 SSTORE [] Store ctr parameter
counter = start
78 61 0095 PUSH2 0095 [95]
7b 56 JUMP [] Jump to location 95

Storage statements:
owner = msg.sender
counter = start

idx   bytecode    opcodes      stack               description

95 5b JUMPDEST []
96 61 015b PUSH2 015b [15b] Push contract length
99 80 DUP1 [15b, 15b]
9a 61 00a4 PUSH2 00a4 [ a4, 15b, 15b] Push contract offset
within bytecode stream

9d 60 00 PUSH1 00 [00, a4, 15b, 15b] Push memory offset where
the code is to be copied

9f 39 CODECOPY [15b] Copy code length 15b
to memory offset 00 from
stream at offset a4

a0 60 00 PUSH1 00 [00, 15b] Return code to deploy
a2 f3 RETURN [] from memory offset 00
with length 15b.

a3 fe INVALID INVALID marks end of
initialization code.
contract code next

This code handles the case of a successful consturctor execution returning the smart contract code to be written on-chain.

--

--