WinDeveloper Coin Tracker

Smart Contract Deployment Internals

Alexander Zammit

Alexander Zammit Photo

Software Development Consultant. Involved in the development of various Enterprise software solutions. Today focused on Blockchain and DLT technologies.

  • Published: Jun 21, 2023
  • Category: Ethereum, Solidity
  • Votes: 5.0 out of 5 - 22 Votes
Cast your Vote
Poor Excellent

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.

Deploying a Smart Contract


Deploying a Contract

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


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 parameters length    ((bytecodeEx.length -                       20
                                   bytecode.length)/2).toString(16)         

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

contract code offset within      ((bytecode.length - 
bytecode stream                  deployedBytecode.length)/2).toString(16)    a4
      
constructor parameters value     (200).toString(16)                          c8

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

owner state variable storage                                                  0
key   

counter state variable storage                                                1
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 memory location 40
04      52          MSTORE          []
05      60 40       PUSH1 40        [40]
07      51          MLOAD           [80]                Load value at memory location 40
08      61 01ff     PUSH2 01ff      [1ff, 80]           1ff - bytecode length excluding
                                                              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 parameters of size 20
                                                        from 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 location following  
                                                        the 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 the 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 constructor 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]            64 = dec 100, we are 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         stack               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 storage value key 00
                                                        This maps to 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 a0 bytes.
                                     01, 00, 00, c8]    a0 = 20*8 = address length

6e      03          SUB             [fffffffffff...,    We just 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 at slot 1
                                                        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 the contract code length
99      80          DUP1            [15b, 15b]
9a      61 00a4     PUSH2 00a4      [ a4, 15b, 15b]     Push the contract code offset 
                                                        within the bytecode stream

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

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

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

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

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


Copyright 2024 All rights reserved. BlockchainThings.io