Writing Secure Smart Contracts on Rootstock: The Importance of the CEI Pattern

Aapsi KhairaAapsi Khaira
6 min read

Introduction

Security is a top priority when building decentralized applications on Rootstock, which is a Bitcoin sidechain that enables smart contracts using Ethereum’s EVM. One of the most reliable strategies for defending your smart contracts against vulnerabilities is the Checks-Effects-Interactions (CEI) Pattern. This approach helps prevent reentrancy attacks, a well-known exploit that has led to millions in losses across EVM-compatible chains. In this guide, we’ll explore how CEI strengthens contract security on Rootstock and how to use it effectively in your smart contracts.

What Is a Reentrancy Attack?

To understand the need for CEI, let’s first unpack reentrancy.

A reentrancy attack occurs when an external contract called by your contract re-enters the original function before it finishes executing. This can allow the external contract to manipulate the flow and state of your application, often draining funds.

At the heart of this vulnerability is misordered logic. If a contract interacts with an external address before updating its own state, that address could re-enter the function and exploit the outdated state.

Real-World Analogy: The Honest Shopkeeper Trap

Imagine a small shop in a village that allows people to purchase goods on credit. When a customer buys an item, the shopkeeper hands over the goods and notes down the transaction at the end of the day. One clever (and dishonest) customer, Mira, figures this out. She buys one item, then comes back within the hour and buys another, before her previous purchase is recorded. She keeps doing this all day, effectively getting a week’s worth of supplies without actually having credit for it. Only at day’s end does the shopkeeper realize the stock is gone and Mira’s credit is overdrawn.

This mirrors a reentrancy attack—the attacker exploits a delay in state updates to abuse the system multiple times.

The CEI Pattern: A Simple Yet Powerful Defense

The Checks-Effects-Interactions pattern refers to a structured order in which the logic inside your contract functions should be written:

  1. Checks – Begin by verifying that all necessary conditions are met for the function to proceed.

  2. Effects – Next, update the contract’s internal state variables.

  3. Interactions – Finally, make any external calls or send Ether.

This sequence is designed to reduce the risk of unexpected behavior caused by external contract interactions, especially when those external calls might attempt to re enter your contract.

Secure Withdrawal Example on Rootstock

This is a simple smart contract that allows users to deposit and withdraw RBTC safely. It follows the Checks-Effects-Interactions (CEI) pattern to defend against reentrancy attacks.

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

contract RSKVault {
    error InsufficientFunds();

    mapping(address => uint256) private vault;

    // Function to deposit RBTC
    function deposit() external payable {
        vault[msg.sender] += msg.value;
    }

    // Secure withdraw function following CEI
    function withdraw(uint256 amount) external {
        // Checks: Make sure the user has enough RBTC
        if (vault[msg.sender] < amount) {
            revert InsufficientFunds();
        }

        // Effects: Update internal accounting before sending RBTC
        vault[msg.sender] -= amount;

        // Interactions: Transfer RBTC to the user
        (bool sent, ) = msg.sender.call{value: amount}("");
        require(sent, "Withdrawal failed");
    }

    // View function to check balance
    function getBalance(address user) external view returns (uint256) {
        return vault[user];
    }
}

Understanding the RSKVault Contract (CEI Applied)

Function 1: deposit()

function deposit() external payable {
    vault[msg.sender] += msg.value;
}

This function lets users send RBTC to the contract. The amount sent (msg.value) is added to their balance stored in the vault mapping.

Function 2: withdraw(uint256 amount)

function withdraw(uint256 amount) external {
    // Check
    if (vault[msg.sender] < amount) {
        revert InsufficientFunds();
    }

    // Effects
    vault[msg.sender] -= amount;

    // Interactions
    (bool sent, ) = msg.sender.call{value: amount}("");
    require(sent, "Withdrawal failed");
}

Let’s understand each CEI step here:

1. Check:

if (vault[msg.sender] < amount) {
    revert InsufficientFunds();
}

Ensures the caller has enough balance. If not, the function stops immediately.

2. Effects:

vault[msg.sender] -= amount;

Updates the contract’s state by reducing the caller’s balance before any RBTC is transferred.

3. Interactions:

(bool sent, ) = msg.sender.call{value: amount}("");
require(sent, "Withdrawal failed");

Finally, it sends RBTC to the user. Even if this triggers a fallback or malicious function on the receiver’s side, the contract state is already updated.

What Happens If CEI Is Not Followed?

Let’s imagine a vulnerable version of the withdraw() function that skips the CEI pattern:

function badWithdraw(uint256 amount) external {
    // ❌ Interaction happens before Effects
    (bool sent, ) = msg.sender.call{value: amount}("");
    require(sent, "Transfer failed");

    // ❌ Effect happens after sending Ether
    vault[msg.sender] -= amount;
}

Vulnerability: Reentrancy

If msg.sender is a malicious contract, it can define a receive() or fallback() function that calls badWithdraw() again before the first call finishes.

Reentrancy Exploit Breakdown Without CEI

StepActionAttacker's Balance in ContractContract’s Total RBTCComments
1Attacker deposits 1 RBTC1 RBTC1 RBTCInitial deposit
2Attacker calls badWithdraw(1 ether)1 RBTC1 RBTCWithdrawal starts, but state is not updated yet
3Contract sends 1 RBTC to attacker (fallback runs)1 RBTC0 RBTCBalance not yet updated, fallback function is triggered
4Fallback calls badWithdraw(1 ether) again1 RBTC0 RBTCContract still believes attacker has 1 RBTC, sends again
...Reentrancy continues1 RBTCAttacker repeatedly drains funds

Note: On RSK, 1 ether in Solidity equals 1 * 10¹⁸ wei, which translates to 1 RBTC*. Although RBTC is pegged 1:1 with BTC, it follows Ethereum's standard of **18 decimals*, so the smallest unit is *wei**, just like on Ethereum.*

Conclusion

Smart contracts on Rootstock are powerful tools for building decentralized financial systems. However, with that power comes the responsibility to ensure they are secure. One of the most critical vulnerabilities developers face is reentrancy, a subtle yet dangerous exploit that can drain funds and compromise contract integrity.

At the core of this vulnerability is a common mistake: interacting with external contracts before updating the internal state. The Checks-Effects-Interactions (CEI) pattern provides a reliable and effective structure to prevent this. By checking all conditions first, updating internal variables next, and only then performing external interactions, developers can significantly reduce the risk of reentrancy.

For even stronger protection, CEI can be reinforced using Solidity’s built-in ReentrancyGuard, a modifier that blocks recursive external calls at runtime. By applying the nonReentrant modifier to sensitive functions, developers can ensure that even if the CEI structure is inadvertently bypassed, the contract remains protected. This added safeguard is especially valuable in scenarios involving fund transfers or other critical logic.

The CEI pattern provides the structure. ReentrancyGuard provides the lock. Together, they offer a comprehensive defense against one of the most common and costly vulnerabilities in smart contract development.

On Rootstock, following CEI is not just good practice. It is essential. Security begins with the very first function you write.

0
Subscribe to my newsletter

Read articles from Aapsi Khaira directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Aapsi Khaira
Aapsi Khaira

I am a Blockchain Developer with a core focus on Smart Contract development. My expertise is centered on building secure, efficient, and upgradeable smart contracts using Solidity, Hardhat, and Foundry. With hands-on experience in decentralized finance (DeFi) and tokenization, I have developed a range of blockchain solutions, including staking contracts, onchain games, RWA Tokenization, and NFT marketplaces. Currently, I am heavily involved in Real World Asset (RWA) tokenization and exploring advanced cryptographic techniques like Partially Homomorphic Encryption (PHE) and Fully Homomorphic Encryption( TFHE) to enhance data privacy in smart contracts. My development process prioritizes gas optimization, ensuring transactions are cost-effective and scalable. Additionally, I specialize in integrating smart contracts with decentralized applications (dApps) using ethers.js and have a strong track record of performing thorough security audits to ensure the integrity of blockchain protocols. I thrive in the evolving blockchain ecosystem, constantly refining my skills and contributing to the development of decentralized, transparent, and secure solutions for the future.