Web3 Security: Demystifying Immutability and Raising Code Standards

Patrick (Barba) Carneiro
Coinmonks
4 min readMar 19, 2024

--

This is the translation of the article “Segurança na Web3: Desmistificando Imutabilidade e Elevando Padrões de Código” from Bellum Galaxy.

As we move towards a new market cycle in the web3 ecosystem, we realize that for a vast portion of the world’s population, some basic concepts of this ecosystem are still obscure, confusing, and even unknown. Therefore, this and the next week, we will focus on more technical articles. Today, we will address a key point: Security.

Unfortunately, it’s common to hear web3 enthusiasts confuse immutability with security. Immutability ensures that the smart contract you interact with will not be altered to benefit third parties. However, this does not guarantee that a contract is free of bugs, logic errors, or vulnerabilities that could harm users at unopportune moments.

It is the developer’s responsibility to do their utmost to enhance the security of their code. And, as Patrick Collins rightly pointed out, “It is the auditor’s responsibility to do their best to find and report any vulnerabilities in a code”.

Focusing on accidental vulnerabilities, numerous vectors can lead to exploits. An exploit may not always occur in a malicious manner, such as in an attack. This means that an immutable contract may be susceptible to vulnerabilities. Therefore, immutability does not equate to security, and that’s why clear codes and audits are crucial in web3.

Let’s explore a simple scenario:

  1. You are verifying a contract on Etherscan;
  2. A function catches your attention, you interact with this function, and everything proceeds normally.
  3. Then you identify a function called “Kill” and execute it.
  4. To your surprise, the function also executes correctly.

What’s the result? You “delete” the contract! This seems absurd, but it happened, and the loss was significant. There’s a context behind this event, but the main point is:

How can you minimize risks and facilitate the identification of vulnerabilities during code reviews and audits?

Audits are extremely necessary processes and have a high cost. Therefore, we need to facilitate the auditors’ work as much as possible so that they can focus on finding vulnerabilities instead of trying to decipher complex and outdated codes.

The following content is a result of two basic principles: Discipline and Organization.

Layout

If you develop, what is the layout of your Smart Contracts? As developers, our task is to keep codes clean, clear, objective, and well-documented, facilitating understanding by anyone who accesses them. Additional information about this can be found in the Solidity documentation. In this link, you can access the layout that I use to develop projects for Bellum Galaxy and partners.

Bellum Galaxy Security & Auditing logo.

Nomenclature

Errors

Errors are an efficient way to indicate that an action cannot be performed for some reason, in addition to aiding in debugging the code. Errors need to be descriptive to facilitate understanding and save gas.

Errors are commonly declared as follows:

error SomethingWentWrong();

Observing this custom error, we can identify:

  1. Something
  2. Went
  3. Wrong

This represents a waste of gas and in some cases may not be so descriptive.

The recommended way to use custom errors is

error ContractName_ValueEnteredIsLessThanAllowed(uint256 enteredValue, uint256 allowedValue);

Now, the information we can identify is:

  • The name of the contract where the error was triggered;
  • The reason why the error was triggered;
  • The value you entered;
  • The minimum allowed value.

Variables

The naming of variables should reflect their storage type, adopting clear standards that facilitate the identification and organization of the code. Let’s look at an example that should not be followed:

contract Example {
uint256 public oneNumber;
uint256 public immutable twoNumbers;
uint256 public constant threeNumbers = 3;

constructor(uint256 twoNumbersOne){
twoNumbers = twoNumbersOne;
}

function oneFunction(uint256 anotherNumber) public returns(uint256){
uint256 oneMoreNumber = anotherNumber + 50;
return oneMoreNumber;
}
}

While in this contract it’s easy to identify which variable is which, what would happen if it had 600 lines of code?

To facilitate understanding, organization, and even your own life when reviewing the code, why not adopt this method:

contract Example {
// Starts with s_, for storage
uint256 public s_oneNumber;
// Starts with i_, for immutable, followed by the variable's name
uint256 public immutable i_twoNumbers;
// Uppercase, separated by _
uint256 public constant THREE_NUMBERS = 3;
// Removal of "magic number"
uint256 public constant FUNCTION_BONUS = 50;

// Include a _ before the variable name
// indicating it's a function parameter.
constructor(uint256 _twoNumbersOne){
i_twoNumbers = _twoNumbersOne;
}

// Include a _ before the variable name
// indicating it's a function parameter.
function oneFunction(uint256 _anotherNumber) public returns(uint256){
// Replace the "magic number", 50, with a descriptive name.
uint256 oneMoreNumber = _anotherNumber + FUNCTION_BONUS;
return oneMoreNumber;
}
}

Many adjustments can still be made to save gas, etc. However, our focus is on the basics.

Events

Just like errors, events should be descriptive and always emitted after changes in storage, increasing the transparency and traceability of operations.

contract oneContract{
uint256 public s_oneVariable;

event oneContract_OneVariableWasUpdated(uint256 previousValue, uint256 currentValue);

function oneFunction(uint256 _oneParameter) public {
uint256 oneVariableInMemory = s_oneVariable;
s_oneVariable = _oneParameter;

emit OneContract_OneVariableWasUpdated(oneVariableInMemory, _oneParameter);
}
}

Functions

The naming of functions should indicate their visibility, clearly differentiating those that are accessible externally from those that are internal or private.

// Functions that can be accessed externally, whether public or external
function publicOrExternalFunction() public{}
// Functions that are accessed only internally, whether internal or private
function _internalOrPrivateFunction() private{}

Conclusion

The security level of a smart contract is not the final stage of development, but an aspect that should be considered from file creation to the completion of all documentation, audits, and post-deployment contingency plans. As we have seen, with organization and discipline, it is possible to follow basic standards that directly reflect the project’s security.

Connect With Us:

Visit our website, join our Discord, and follow us on X, GitHub, and LinkedIn to stay updated on our adventures and insights.

--

--

Patrick (Barba) Carneiro
Coinmonks

Solidity Developer | Security Researcher | Chainlink Developer Expert | @bellumgalaxy Founder