Skip to main content
Create your own

Secure Dividend Management with ReentrancyGuard

Welcome back. In our previous lesson, we established the design for our security token's dividend distribution mechanism. We chose a "pull-over-push" pattern to ensure efficiency and security, and outlined a pro-rata accounting model based on token ownership.

Today, we transition from design to implementation. This lesson will guide you through writing the Solidity code for this mechanism within our SecurityToken contract. We will implement the depositDividends and withdrawDividends functions and, most critically, secure the withdrawal process against reentrancy attacks using OpenZeppelin's ReentrancyGuard. This will put into practice the security principles we discussed and give you hands-on experience with a crucial pattern for any contract that handles value distribution.

1. State Variables for Dividend Accounting

Before we can write the functions, we need to add the necessary state variables to our SecurityToken contract to track dividends. Our model, similar to patterns used in production systems, won't track every individual's share directly. Instead, it uses an accumulator to track the total dividends distributed per token. This is a highly efficient accounting method for a distributed environment.

We will add the following state variables:

// The ERC20 token used for paying dividends (e.g., USDC)
IERC20 public dividendToken;

// An accumulator for dividends paid out per token, scaled for precision
uint256 public dividendsPerToken;

// Tracks the last `dividendsPerToken` value at which a user claimed their share
mapping(address => uint256) public dividendsClaimedPerToken;
  • dividendToken: This will store the address of the ERC-20 contract used for dividend payments. We'll set this in the constructor.
  • dividendsPerToken: This is the core of our accounting. It accumulates the value of dividends distributed for each single token in circulation. We scale this value to maintain precision, as Solidity does not support floating-point numbers.
  • dividendsClaimedPerToken: This mapping records, for each user, the value of dividendsPerToken at the time of their last withdrawal. This prevents double-claiming. A user's pending dividend is proportional to the difference between the current global dividendsPerToken and their personal dividendsClaimedPerToken value.

The article below provides an excellent walkthrough of a similar differential dividend model. Pay close attention to its explanation of dividendPerToken.

Write your own simple dividend ERC20 Token - Codementor

This article from Codementor explains a common pattern for on-chain dividend distribution that we will be adapting.

In the "Approach" section, read the explanation of the two variables, dividendPerToken and xDividendPerToken. Our dividendsClaimedPerToken serves the same purpose as their xDividendPerToken. In the "Deposit" section, review the logic for how the contract updates dividendPerToken when new funds arrive. We will implement a similar logic for ERC-20 tokens.

2. Implementing depositDividends

The depositDividends function is the administrative entry point for funding the dividend pool. It will be restricted to an address holding a specific role, which we'll call DIVIDEND_ADMIN_ROLE.

The function's logic is as follows:

  1. It accepts one argument: the amount of dividend tokens to be deposited.
  2. It transfers this amount from the administrator's wallet to the SecurityToken contract. This requires the administrator to first call approve() on the dividend token contract, granting our SecurityToken contract a spending allowance.
  3. It updates the dividendsPerToken accumulator.

The calculation to update the accumulator is crucial:

dividendsPerToken += (amount * 1e18) / totalSupply();

We multiply amount by 1e18 (a common practice for precision) before dividing by totalSupply() to avoid losing precision due to integer division.

Here is the implementation. You would add this function to your SecurityToken.sol file.

// Define a role for the dividend administrator
bytes32 public constant DIVIDEND_ADMIN_ROLE = keccak256("DIVIDEND_ADMIN_ROLE");

// ... inside your contract ...

function depositDividends(uint256 amount)
    external
    onlyRole(DIVIDEND_ADMIN_ROLE)
{
    require(totalSupply() > 0, "Cannot deposit dividends with zero total supply");
    
    // Transfer dividend tokens from the admin to this contract
    // The admin must have approved this contract to spend 'amount'
    dividendToken.transferFrom(msg.sender, address(this), amount);
    
    // Update the accumulator
    dividendsPerToken += (amount * 1e18) / totalSupply();
    
    emit DividendsDeposited(msg.sender, amount);
}

(Note: You would also need to define the DividendsDeposited event: event DividendsDeposited(address indexed depositor, uint256 amount);)

3. Implementing withdrawDividends and Preventing Reentrancy

This function allows any token holder to pull their rightful share of the dividends from the contract. Its implementation must be precise and, above all, secure.

The Reentrancy Vulnerability

The withdrawDividends function will make an external call when it transfers the dividend tokens to the user. This creates a potential attack vector known as a reentrancy attack. If a malicious user calls this function from another smart contract, they could craft their contract to immediately call withdrawDividends again after receiving the funds but before our contract has finished its execution and updated its state.

This recursive call could allow the attacker to drain the contract's entire dividend balance. The diagram below illustrates this cyclical, malicious call flow.

This diagram shows an attacker contract calling a vulnerable contract's `withdraw` function. The vulnerable contract sends funds via an external call, which triggers the attacker's contract to re-enter the `withdraw` function before the first call completes, leading to repeated withdrawals.

To understand this vulnerability in-depth, watch the following video from Dapp University. It clearly explains the attack and introduces the standard solution.

How to Hack Smart Contracts With Reentrancy

This video provides a clear, practical demonstration of a reentrancy attack and how to prevent it.

First, watch the explanation of what a reentrancy attack is at a high level. Next, see how the vulnerable withdraw function is coded in the Bank contract example. Notice the order of operations: it sends Ether before updating the user's balance. This is the flaw. Finally, focus on the solution using OpenZeppelin's ReentrancyGuard. This section explains how to import the guard, inherit from it, and apply the nonReentrant modifier. This is exactly what we will do.

The Secure Implementation

To mitigate this, we will use two layers of defense:

  1. The Checks-Effects-Interactions (CEI) pattern: First check conditions, then update state (effects), and only then interact with external contracts.
  2. OpenZeppelin's ReentrancyGuard: A robust, battle-tested modifier that provides a lock, preventing any re-entrant calls to functions that use it.

First, let's look at the implementation with the CEI pattern:

function withdrawDividends() external {
    address user = msg.sender;
    uint256 userBalance = balanceOf(user);
    require(userBalance > 0, "User has no tokens");

    // Calculate pending dividends
    uint256 pendingDividends = (userBalance * (dividendsPerToken - dividendsClaimedPerToken[user])) / 1e18;

    // --- CHECKS ---
    require(pendingDividends > 0, "No dividends to withdraw");

    // --- EFFECTS ---
    // Mark dividends as claimed BEFORE the transfer
    dividendsClaimedPerToken[user] = dividendsPerToken;

    // --- INTERACTIONS ---
    // Transfer the funds
    dividendToken.transfer(user, pendingDividends);

    emit DividendsWithdrawn(user, pendingDividends);
}

Now, let's add the final layer of protection. To use ReentrancyGuard, you'll first import it at the top of your SecurityToken.sol file and make your contract inherit from it:

import "@openzeppelin/contracts/security/ReentrancyGuard.sol";

contract SecurityToken is ERC20, AccessControl, ReentrancyGuard {
    // ...
}

Then, you simply add the nonReentrant modifier to the withdrawDividends function:

function withdrawDividends() external nonReentrant {
    // ... function body remains the same
}

This simple addition ensures that while the withdrawDividends function is executing, no other function with the nonReentrant modifier can be called until the first call has completed. It's a powerful and standard safeguard.

The following resources provide more context on the pull pattern and the security measures we've just implemented.

How to Automate Dividend & Interest Payouts with Smart Contracts

This article reinforces the architectural and security patterns we've discussed.

Under "Prerequisites and System Architecture," read the paragraph describing the pull-over-push pattern. In the same section, review the paragraph on security, which explicitly mentions the Checks-Effects-Interactions pattern. Finally, under "Frequently Asked Questions," the article provides a concise comparison of push vs. pull models, confirming why our chosen approach is standard practice.

Conclusion

In this lesson, you have successfully translated a high-level design into working, secure Solidity code. You've implemented a complete dividend distribution mechanism using an efficient, differential accounting model.

Key Takeaways:

  • A dividend system can be efficiently implemented using an accumulator (dividendsPerToken) and a mapping to track individual claims (dividendsClaimedPerToken).
  • The depositDividends function serves as a permissioned entry point to fund the dividend pool, updating the central accumulator.
  • The withdrawDividends function allows users to pull their funds, following the secure and gas-efficient pull pattern.
  • Security against reentrancy is non-negotiable. We defended our contract by adhering to the Checks-Effects-Interactions pattern and implementing the industry-standard ReentrancyGuard from OpenZeppelin.

You now have a security token with core administrative features, compliance controls, and a value distribution mechanism. In the next module, we will write a comprehensive suite of tests to verify that every piece of this complex system—including the dividend logic and its security protections—works exactly as intended.

Can't find a good explanation? Sign up and we'll make it for you

Sign up