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 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:
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.
Under the ContractBytecode
folder rename the file:
From: ./BlockchainThings/ContractBytecode/.env_template
To: ./BlockchainThings/ContractBytecode/.env
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:
- Takes one input parameter.
- Includes a require clause that could abort deployment.
- 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.