Blockchain Developer Path from ground zero #4

Alejo Lovallo
Think and Dev
Published in
5 min readJun 14, 2023

— Part 4

Hello Padawan!

It´s been an incredible journey so far. If you have reached this point, you are really nailing it. I hope you keep walking the path along with me.

Today we will deep dive into some add-ons that Solidity provides us like CREATE2 and Hooks.

CREATE2 — Know contract address before deployment

Yes, with CREATE2 this is possible. But first, let´s first talk about opcodes.

Quoting Bernard Peh in his article Solidity Bytecode and Opcode Basics:

Opcodes are the low level human readable instructions of the program.

So basically, opcodes are the names of the instructions that the EVM (Ethereum Virtual Machine) could understand.

When a contract is compiled and deployed, its outputs are the bytecode and the ABI. The bytecode then, is full of OPCODES. In fact, there are two major opcodes available when creating a smart contract: CREATE and CREATE2 opcodes.

Originally (with CREATE opcode) when smart contracts are created, the address for the new contract is computed as a function of the sender’s own address and a nonce.

Here comes the main concept:

Nonces cannot be reused, and they must be sequential.

So, as OpenZeppelin states:

“It is possible to predict the address where the next created contract will be deployed, but only if no other transactions happen”

Mmmm… only if no other transactions happen, this does not sound so good. For instance, if I want to deploy the same contract to three different networks and remain with the same address for each one, I have to guarantee that somehow my wallet nonce does not change from transaction one (deploy to first network) to transaction three (deploy to third and last network). However, this is not possible. Until CREATE2 opcode.

This particular opcode rewrites the sentence on how smart contracts addresses are computed:

New addresses are a function of a constant value, the sender´s own address, a salt value provided by the sender, and the contract bytecode.

By not depending on the sender´s nonce, and always providing the same salt value, it is completely possible to deploy with the same address on every network we would like to do it.

Deploying with CREATE2

Oz provides us with a library to make use of the CREATE2 opcode by using just three functions:

  • deploy(amount, salt, bytecode)
  • computeAddress(salt, bytecodeHash)
  • computeAddress(salt, bytecodeHash, deployer)

ComputeAddress functions are just view functions to get the pre-computed create2 address.

We will have then, two smart contracts:

  • Test.sol: The contract that we want to deploy using CREATE2.
  • Create2Deployer.sol: Contract that uses the create2 Oz library.
pragma solidity ^0.8.18;

contract Test {

uint256 value;

event ValueUpdated(uint256 newValue);

constructor(){}

function changeValue(uint256 _newValue) external {
value = _newValue;
emit ValueUpdated(_newValue);
}

pragma solidity ^0.8.18;

import {Create2} from "@openzeppelin/contracts/utils/Create2.sol";

contract Create2Deployer {

/**
* @dev Deploys a contract using `CREATE2`. The address where the
* contract will be deployed can be known in advance via {computeAddress}.
*
* The bytecode for a contract can be obtained from Solidity with
* `type(contractName).creationCode`.
*
* Requirements:
* - `bytecode` must not be empty.
* - `salt` must have not been used for `bytecode` already.
* - the factory must have a balance of at least `value`.
* - if `value` is non-zero, `bytecode` must have a `payable` constructor.
*/
function deploy(uint256 value, bytes32 salt, bytes memory code) public {
Create2.deploy(value, salt, code);
}

/**
* @dev Returns the address where a contract will be stored if deployed via {deploy}.
*/
function computeAddress(bytes32 salt, bytes32 codeHash) public view returns (address) {
return Create2.computeAddress(salt, codeHash);
}

/**
* @dev Returns the address where a contract will be stored if deployed via {deploy} from a
* contract located at `deployer`. If `deployer` is this contract's address, returns the
* same value as {computeAddress}.
*/
function computeAddressWithDeployer(
bytes32 salt,
bytes32 codeHash,
address deployer
) public pure returns (address) {
return Create2.computeAddress(salt, codeHash, deployer);
}

receive() external payable {}

}

Finally, it is just a matter of deploying the Create2Deployer contract and using it.

An example of a create2 smart contract deployer could be found in this repository

Hooks

If you come from front-end development, you are probably familiar with hooks. They are nothing more than a function that gets executed when an event is triggered, for instance, when a component variable changes (useState), or when a component mounts in a view (useEffect).

Solidity is not an exception.

Hooks have been “created” by Open Zeppelin for the ERC20 and ERC721 standards contracts. Specifically for the transfer and transferFrom functions. However, the concept behind it can be applied to almost any function.

Let´s start then with the ERC20 smart contract and take a look at the transfer function:

    function _transfer(address from, address to, uint256 amount) internal virtual {
require(from != address(0), "ERC20: transfer from the zero address");
require(to != address(0), "ERC20: transfer to the zero address");

_beforeTokenTransfer(from, to, amount);

uint256 fromBalance = _balances[from];
require(fromBalance >= amount, "ERC20: transfer amount exceeds balance");
unchecked {
_balances[from] = fromBalance - amount;
// Overflow not possible: the sum of all balances is capped by totalSupply, and the sum is preserved by
// decrementing then incrementing.
_balances[to] += amount;
}

emit Transfer(from, to, amount);

_afterTokenTransfer(from, to, amount);
}

If you pay attention, you will see two functions that are being called:

  • _beforeTokenTransfer(from,to,amount)
  • _afterTokenTokenTransfer(from,to,amount)

And if we check what these functions are doing, you will find a surprise:

function _beforeTokenTransfer(address from, address to, uint256 amount) internal virtual {}

function _afterTokenTransfer(address from, address to, uint256 amount) internal virtual {}

They do nothing!

The reason is by the way, pretty obvious. Open Zeppelin leaves these functions unimplemented because they expect you to add whatever functionality you might like.

For instance, you could develop a fee logic sub-system using just these two functions. An excellent and also recent live example is the Uniswap V4-Core protocol release:

There´s always more to come!

Next article, we will cover signature verification and how it has been the kickoff for decentralized identity and zero-knowledge proofs.

Also, we will start a sub-journey in this blockchain path, the DEFI path. Besides learning the economic details that make the protocols work like lending and borrowing and yield farming protocols, we will be covering a detailed walkthrough on how the protocols technically work.

--

--

Alejo Lovallo
Think and Dev

Sr. Blockchain Developer || WIP Software engineer || DevOps Consultant.