Skip to main content

Someone wanted to learn this too, so Grasp built them a personal learning path.

You can keep exploring this one or create your own

Setting a Funding Cap

Hello! Welcome back to our course on building your own token.

In our last lesson, we built a fully functional buyTokens function. It accepts Ether, calculates the token amount, delivers the tokens to the investor, and forwards the funds to the project wallet. Our crowdsale works, but it has no limits. An enthusiastic whale could buy up the entire supply, or the sale could theoretically run forever.

To create a professional and predictable token sale, we need to introduce constraints. Today, your goal is to add a funding cap to limit the total investment amount. This is a crucial step in defining your project's scope and providing transparency to your investors.

1. The "Why" of a Hard Cap

Before we dive into the code, let's discuss why a "hard cap" is essential. In the context of a crowdsale, a hard cap is the maximum amount of money a project intends to raise. Once this total is reached, the sale automatically concludes.

  • For Investors: It provides clarity on the maximum possible token supply and their potential ownership percentage. It also signals that the project has a clear financial plan and isn't just trying to raise as much money as possible.
  • For Founders: It enforces financial discipline, preventing the project from becoming over-funded, which can lead to inefficiencies. It also helps define the initial valuation and tokenomics of your project.

Think of this like setting a resource quota in a backend system. You limit a user's storage or API calls to ensure system stability and fair usage. Here, we limit the total investment to ensure the economic stability and fairness of the token launch.

2. Adopting the OpenZeppelin Crowdsale Framework

So far, we've built our crowdsale logic from scratch. This was great for understanding the fundamentals. However, as we add more complex features like caps, time limits, and vesting, building everything manually increases the risk of introducing security vulnerabilities.

A much safer and more efficient approach is to use a battle-tested framework. For smart contracts, the industry standard is OpenZeppelin. We will refactor our contract to use OpenZeppelin's modular crowdsale contracts. This is analogous to using a framework like Express.js or Django in backend development; you don't build the entire HTTP server from scratch, you build your application logic on top of a secure and reliable foundation.

The OpenZeppelin framework provides a base Crowdsale contract that handles the core logic we've already written (accepting payments, calculating tokens, etc.). We can then extend it with "feature" contracts. The one we need today is CappedCrowdsale.

3. Implementing the Funding Cap with CappedCrowdsale

CappedCrowdsale is a contract designed to be inherited by your main crowdsale contract. It automatically adds the logic to enforce a funding cap.

Here’s how we integrate it:

  1. Import the necessary contracts from the OpenZeppelin library.
  2. Inherit from both Crowdsale and CappedCrowdsale in your contract's definition.
  3. Update the constructor to accept the cap value and pass it to the CappedCrowdsale parent constructor.

Let's look at the code. A crowdsale contract with a hard cap would look like this:

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

import "@openzeppelin/contracts/crowdsale/Crowdsale.sol";
import "@openzeppelin/contracts/crowdsale/validation/CappedCrowdsale.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";

// Note the multiple inheritance: Crowdsale and CappedCrowdsale
contract MyTokenSale is Crowdsale, CappedCrowdsale {
    constructor(
        uint256 rate, // e.g., 1000; 1 ETH = 1000 tokens
        address payable wallet, // Your project's wallet
        IERC20 token, // The ERC-20 token being sold
        uint256 cap // The hard cap in wei (e.g., 100 ETH)
    )
        // Call the constructors of the parent contracts
        Crowdsale(rate, wallet, token)
        CappedCrowdsale(cap)
    {
        // The constructor body can be empty!
        // All initialization is handled by the parent contracts.
    }
}

Notice how clean this is. By inheriting from CappedCrowdsale and calling its constructor, our MyTokenSale contract automatically gains the ability to reject any transaction that would push the total weiRaised above the cap.

4. How It Works Under the Hood

How does this work automatically? The Crowdsale contract is designed with internal "hook" functions that can be overridden by child contracts. One such function is _preValidatePurchase.

The CappedCrowdsale contract provides its own version of _preValidatePurchase that contains the essential check:

require(weiRaised() + _weiAmount <= cap(), "CappedCrowdsale: cap exceeded");

Because our contract inherits from CappedCrowdsale, this check is automatically executed every time someone calls buyTokens, ensuring the cap is never breached.

The following article was instrumental in popularizing this modular approach. While the code is for an older Solidity version, the concepts are foundational to what we are doing today.

Build a Real World ICO - The Complete Walkthrough

The article "Build a Real World ICO" from DappUniversity demonstrates how to combine OpenZeppelin's modular crowdsale contracts. It's a great practical example of the pattern we're implementing.

Please read section 5, "Capped Crowdsale". Pay close attention to how the DappTokenCrowdsale contract inherits from CappedCrowdsale and how the cap is passed into the constructor. The article also implements per-investor caps, which is a good example of how to further customize the validation logic by extending the _preValidatePurchase function.

As you saw in the article, the constructor chain is critical. When you inherit from multiple contracts, you must call each parent's constructor with the correct arguments. Getting this wrong is a common source of errors.

Remix IDE: Capped Crowdsale Implementation with Errors
An example of constructor errors in the Remix IDE. When inheriting from multiple contracts like `CappedCrowdsale`, each parent constructor must be called with the correct arguments. Missing or incorrect calls, as indicated by the errors, will prevent the contract from compiling.
Test your understanding!

Your MyTokenSale contract now requires a cap value in its constructor. How would you modify your Hardhat deployment script (e.g., deploy.js) to deploy this contract with a hard cap of 150 ETH? Assume you already have the rate, walletAddress, and token contract address.

Show answer

You would need to use ethers.utils.parseEther to convert "150" into its wei equivalent and pass it as the last argument to the deploy function.

const { ethers } = require("hardhat");

// ... inside your async main function ...

const MyTokenSale = await ethers.getContractFactory("MyTokenSale");

// Define the cap in ETH
const capInEth = "150";

// Deploy the contract with the new 'cap' argument
const tokenSale = await MyTokenSale.deploy(
    rate,
    walletAddress,
    token.address,
    ethers.utils.parseEther(capInEth) // Convert 150 ETH to wei
);

await tokenSale.deployed();

console.log("TokenSale deployed to:", tokenSale.address);

Conclusion

Fantastic progress! You've taken a significant step in making your crowdsale more professional and secure. By setting a hard cap, you provide crucial certainty for both your project and your future investors.

Key Takeaways:

  • A hard cap defines the maximum amount of funds a crowdsale will raise.
  • Using a standard framework like OpenZeppelin for complex features is more secure and maintainable than building from scratch.
  • You can add a funding cap by inheriting from OpenZeppelin's CappedCrowdsale contract.
  • The logic is enforced automatically through multiple inheritance and function overrides (_preValidatePurchase).
  • Correctly calling parent constructors is essential when using this pattern.

Preview of the Next Lesson:
Our crowdsale now has a limit on how much it can raise, but not when. In the next lesson, we will continue adding constraints by learning how to enforce a sale period by defining start and end times for the crowdsale.

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

Sign up