Salted contract creation uses cryptographic “salt” to add an additional layer of security to a smart contract. Salt is a random string added to the contract code to make it harder for an attacker to reverse engineer the code and gain access to the contract. This makes the contract more secure and helps protect the data stored within it.
In the previous article, we went to some lengths on contract creation. In this article, we’ll continue building on the previous topic by extending the story to create salted contracts.
π Recommended: Smart Contracts β Discover How To Create Them Directly and Indirectly!
It’s part of our long-standing tradition to make this (and other) articles a faithful companion or a supplement to the official Solidity documentation.
A Standard Approach for Creating Contracts
As we’ve previously learned, a common approach for creating contracts is done in two ways:
- either via a contract constructor or
- via a
new
keyword, which enables us to create one contract from the other contract.
Either way, there’s a process that takes place while the contract address is computed, and we’ll go through it in a moment.
Before a contract can be placed on its address, the address must be determined, i.e., computed unambiguously. The address is computed by considering two parameters:
- the address of the originating/creating account, and
- a counter which increases with every created contract, called the nonce.
π‘ Note: A nonce is “a random or semi-random number that is generated for a specific use. It is related to cryptographic communication and information technology (IT). The term stands for “number used once” or “number once” and is commonly referred to as a cryptographic nonce.” (source)
Imagine if, by some wizardry, we could create a contract in a real, non-digital world; its address would be determined by our own country name, postal code, street, house number, and the number of contracts we’ve created so far. The same goes for the digital world, it’s just that the addresses look more exotic.
π The only two types of accounts are the external account (controlled by public-private key pairs, i.e., humans) and the contract account (controlled by the machine code – produced from the bytecode – stored together with the account).
We should make a general and important point: the process mentioned above holds for creating accounts that belong to another contract (a contract account), or an account from which a contract is deployed (an external account).
Remember what we said about transactions an article or two ago: a transaction is a message sent from one account (the source, origin) to another account (the destination). When the destination account in a message is unset or explicitly set to null, we’re signaling we want to create a new contract. In either case, the origin/source account address is always known.
π‘ Note: When a contract is created from an external account, an implicitly triggered creation via a constructor is used. On the other hand, when a contract is created from a contract account, the new
keyword is used, so the contract creation is explicitly triggered from another contract. Regardless of a trigger type (implicit or explicit), a contract constructor is always used.
Now that we’ve got these few tiny details out of our way, we can delve deeper into the creation of a salted contract.
Putting Some Salt In the Pot
The first thing coming to my mind is a pirate ship full of salty, toothless, foul-smelling, rum-loaded, sea-dog contracts, just waiting to plunder the first unsuspecting passer-by. Unfortunately, the context is somewhat less adventurous but still interesting to take a look at, as we’ll see for ourselves in the following chapter.
π Note: Salt is a cryptographic term representing a string of bits systematically added to a password instance before being hashed. The purpose of salt is to guarantee a unique output, the hash, even when the input is constant. “Consequently, the unique hash produced by adding the salt can protect us against different attack vectors, such as hash table attacks, while slowing down the dictionary and brute-force offline attacks”.
By specifying the salt
option, which is bytes32
type, we instruct the contract creation process to use a more secure mechanism for computing the contract address.
In particular, the contract creation mechanism will use
- the contract-creating account address,
- the salt value,
- the creation bytecode (the message payload used to generate the machine code for our deployed contract), and
- the constructor arguments.
We should remember that the nonce is not used in this process, meaning that we can compute the address of the deployed contract even before it’s created.
If our contract-creating account is a contract account (possibly creating other contracts), we can trust this address to be stable.
At last, it makes sense to ask ourselves, what’s the point of using the same salt, and producing the same contract address?
Of course, there’s a reason behind it: there are cases when we need a stable, known address to host a special-purpose contract. One such case is a judge contract for off-chain interactions used for resolving disputes outside the blockchain. We’d first calculate the judge contract’s address and then use it to create the judge contract itself.
The syntax to create such a contract is very simple: new D{salt: salt}(arg)
.
π‘ Note: Off-chain smart contracts run outside the blockchain, i.e., not by the miners, requesters, or other types of participants, but only by the client.
π Warning: Salted contract creation has some interesting properties, some of which we could’ve already anticipated, so let’s list some of them.
- A contract can be destroyed, recreated, and end up at the same address. This is due to the deterministic nature of the salted contract creation.
- The re-deployed contract’s bytecode can differ from the first (or other redeployed) contracts’ bytecodes regardless of the same creation bytecode. We should underline that the creation bytecode must be the same to ensure the same salted contract address.
- The difference stems from the creation process, in which the contract constructor might query an external state (a state variable of some other contract) modified between two contract creations. The difference in the external state variable then ends up implemented in the deployed bytecode, changing the deployed code, and by extension, the contract.
In the following example from the docs, we’ll see how a contract is created using salt
.
// SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.7.0 <0.9.0; contract D { uint public x; constructor(uint a) { x = a; } } contract C { function createDSalted(bytes32 salt, uint arg) public { // This complicated expression just tells you how the address // can be pre-computed. It is just there for illustration. // You actually only need ``new D{salt: salt}(arg)``. address predictedAddress = address(uint160(uint(keccak256(abi.encodePacked( bytes1(0xff), address(this), salt, keccak256(abi.encodePacked( type(D).creationCode, abi.encode(arg) )) ))))); D d = new D{salt: salt}(arg); require(address(d) == predictedAddress); } }
Let’s dive into this code step by step:
// SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.7.0 <0.9.0;
Defines the target contract C
. It will be created in a salty way via contract C
.
contract D { uint public x; constructor(uint a) { x = a; } }
Deploys contract C
(with an implicit default constructor).
contract C { function createDSalted(bytes32 salt, uint arg) public {
This first part of the function is optional and used only to show how to precompute/”predict” the contract address.
address(uint160(uint(keccak256(abi.encodePacked( bytes1(0xff), address(this), salt, keccak256(abi.encodePacked( type(D).creationCode, abi.encode(arg) )) )))));
This is the core and mandatory part of the function, showing the creation of an instance of D
type contract.
D d = new D{salt: salt}(arg);
This part of the function uses the require(...)
function and compares it with the predictedAddress
variable. The two should be the same and the require(...)
function will allow the transaction if the check shows they are the same; otherwise, it will revert the transaction.
require(address(d) == predictedAddress); } }
Conclusion
This article taught us what a salted contract is, when to use it, and how to create it.
First, we learned about contract address computation arguments, reminded ourselves of the account types, and contract creation triggering.
Second, we better understood salt as a cryptographic element and its role in contract creation. We also examined an example of salted contract creation and better understood how salt is used in contract creation.
What’s Next?
This tutorial is part of our extended Solidity documentation with videos and more accessible examples and explanations. You can navigate the series here (all links open in a new tab):