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


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:
Checks – Begin by verifying that all necessary conditions are met for the function to proceed.
Effects – Next, update the contract’s internal state variables.
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
Step | Action | Attacker's Balance in Contract | Contract’s Total RBTC | Comments |
1 | Attacker deposits 1 RBTC | 1 RBTC | 1 RBTC | Initial deposit |
2 | Attacker calls badWithdraw(1 ether) | 1 RBTC | 1 RBTC | Withdrawal starts, but state is not updated yet |
3 | Contract sends 1 RBTC to attacker (fallback runs) | 1 RBTC | 0 RBTC | Balance not yet updated, fallback function is triggered |
4 | Fallback calls badWithdraw(1 ether) again | 1 RBTC | 0 RBTC | Contract still believes attacker has 1 RBTC, sends again |
... | Reentrancy continues | 1 RBTC | – | Attacker 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.
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.