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

Crowdsale Contract Design: State Variables & Logic

Hello! Welcome to your next lesson in the "Building a Token Sale Contract" module.

In our last session, we explored different token supply models and decided that a mintable token would provide the best flexibility for our project. This allows us to create new tokens as needed for things like a token sale and future staking rewards.

Now, we will move from that economic design decision to the architectural design of the sale itself. Your learning outcome for today is to design the state variables and logic for a crowdsale contract. Think of this as creating the blueprint for our application. Before writing a single line of implementation code, a good developer first defines the data the system needs to manage and the core behaviors it must exhibit.


1. The Role of a Crowdsale Contract

A crowdsale, or token sale, contract is a specialized smart contract with a single primary purpose: to sell your ERC-20 token to investors in exchange for Ether (ETH). It acts as an automated, trustless vendor.

It's important to understand that the crowdsale contract is separate from your ERC-20 token contract. The two contracts will interact with each other, but they are deployed as distinct pieces of your application's architecture. The crowdsale contract needs to know about your token, but your token doesn't necessarily need to know about the crowdsale.

To get a high-level overview of what this contract needs to do, let's watch the beginning of a tutorial that sketches out the core requirements.

Coding a Crowd Sale Smart Contract · #8 Code Your Own Cryptocurrency on Ethereum

This video, "Coding a Crowd Sale Smart Contract" from Dapp University, gives a great introduction to the main components and business logic we need to consider.

Watch from 01:19 to 06:01. Focus on the four main pieces of functionality the video outlines for the token sale contract.

As the video explained, our crowdsale contract needs to handle several key responsibilities. Let's formalize these into a design plan by first identifying the data we need to store on the blockchain.


2. Designing State Variables: The Contract's Memory

State variables are the persistent memory of a smart contract. They store the essential information that defines the contract's current state and are written permanently to the blockchain. To design our crowdsale, we must ask: "What information does our contract need to remember to function correctly?"

Based on the requirements, we can identify several key pieces of data:

  • A reference to the token being sold: The crowdsale contract must know the address of our ERC-20 token contract to interact with it.
  • A price for the tokens: How much ETH does an investor need to pay to receive a certain number of tokens?
  • A wallet to collect funds: When investors send ETH, where should that money be stored securely?
  • A way to track the amount raised: The contract should keep a running total of the ETH it has collected.

The OpenZeppelin library, a community-vetted and secure set of base contracts, provides an excellent starting point. Their base Crowdsale contract defines these core state variables.

OpenZeppelin Crowdsale Documentation

Let's look at the official OpenZeppelin documentation for their base Crowdsale contract. This will show us the standard, battle-tested implementation of these core state variables.

Read the 'Contract Members' section, focusing on the four private variables listed: _token, _wallet, _rate, and _weiRaised.

Let's break these down:

  1. contract IERC20 private _token;

    • Purpose: Stores the address of the ERC-20 token contract being sold. Using the IERC20 interface type ensures our contract can call standard token functions like transfer or mint.
    • Our Design: We will need a variable to hold the address of the token we created in Module 4.
  2. address private _wallet;

    • Purpose: This is the address where the collected ETH will be forwarded. It should be a secure wallet, ideally a multi-signature wallet, that the project owners control.
    • Our Design: We will define a beneficiary address that receives the funds from the sale.
  3. uint256 private _rate;

    • Purpose: Defines the price. Specifically, it's the number of token units a buyer receives for 1 wei of ETH. (Wei is the smallest denomination of Ether, wei = 1 ETH). Using a rate simplifies the math and avoids floating-point numbers, which are not supported in Solidity.
    • Example: If you want 1 token (with 18 decimals) to cost 0.001 ETH, the rate would be 1000. An investor sending 0.001 * 10^18 wei would receive (0.001 * 10^18 * 1000) / (1 * 10^18) = 1000 tokens (if rate was defined per whole token). A simpler way is to define rate as how many whole tokens you get for 1 ETH, and then do the division. Let's stick with the OpenZeppelin convention: tokens per wei. If 1 token costs 0.001 ETH, then 1 ETH buys 1000 tokens. So the rate is 1000. An investor sending 1 wei would get 1 * 1000 base units of the token. Let's re-read the doc. Wait, rate is token units a buyer gets per wei. If the rate is 500, then 1 wei buys 500 token units.
    • Our Design: We will set a rate that determines the price. We'll pass this value in when we deploy the contract.
  4. uint256 private _weiRaised;

    • Purpose: A counter that tracks the total amount of wei received from investors. This is essential for features like a funding cap.
    • Our Design: Our contract will include logic to increment this counter with every successful purchase.

This image shows how these parameters are configured when deploying a crowdsale contract.

Ethereum Wallet Crowdsale Contract Deployment
This screenshot of the Ethereum Wallet interface shows how constructor parameters like the beneficiary wallet, funding goal, and token price are set during the deployment of a crowdsale contract.

3. Designing Core Logic: The Contract's Actions

With our state variables defined, we can now outline the core logic. Your experience with backend APIs is relevant here; we're essentially defining the "endpoints" and internal business logic of our contract.

a. Initialization (constructor)

Just like a class constructor in object-oriented programming, the contract constructor runs once upon deployment. Its job is to initialize the immutable state of the contract.

For our crowdsale, the constructor will set the initial values for the rate, the wallet address, and the token contract address. These are the fundamental parameters of our specific sale. The video from Dapp University demonstrates this process well.

Coding a Crowd Sale Smart Contract · #8 Code Your Own Cryptocurrency on Ethereum

Let's return to the Dapp University video to see how these state variables are initialized in the constructor and how the token contract is linked.

Watch from 14:01 to 28:18. Pay attention to how msg.sender is used to set an admin, how the token contract's address is passed in, and how the tokenPrice (rate) is configured.

b. Purchase Logic (buyTokens)

This is the heart of the contract. The logic must handle an incoming payment and execute the token sale. The process involves several steps:

  1. Receive ETH: The contract must have a payable function to accept incoming Ether.
  2. Validate the Purchase: The contract should run checks. For example: Is the sale currently active? Has the funding cap been reached? We will cover these advanced validation rules in future lessons.
  3. Calculate Token Amount: Based on the msg.value (amount of ETH sent) and the rate, calculate the corresponding number of tokens to be delivered. The formula is: tokenAmount = msg.value * rate.
  4. Deliver Tokens: The contract must transfer the calculated tokenAmount to the investor.
  5. Forward Funds: The received ETH (msg.value) must be securely transferred to the project's wallet.
  6. Update State: The weiRaised counter must be increased by msg.value.

c. Token Delivery Model: Minting vs. Allowance

A critical design choice is how the crowdsale contract gets the tokens it sells. There are two primary patterns:

  1. Allowance Model: The project owner first mints the total supply of tokens to their own address. Then, they call the approve() function on the token contract to grant the crowdsale contract an "allowance" to spend a portion of those tokens. When an investor buys, the crowdsale uses transferFrom() to move tokens from the owner's address to the investor.
  2. Minting Model: The project owner makes the crowdsale contract a "minter" by granting it the minting role in the token contract. When an investor buys, the crowdsale contract mints new tokens directly into existence and sends them to the investor.

Since we chose a mintable token supply in the last lesson, the Minting Model is the natural fit for our project. It's clean and efficient.

This approach is captured in OpenZeppelin's MintedCrowdsale contract.

Build a Real World ICO - The Complete Walkthrough

The Dapp University article "Build a Real World ICO" demonstrates how to use OpenZeppelin's MintedCrowdsale to implement this logic.

Read section 4, 'Minted Crowdsale'. Notice how the contract inherits from both Crowdsale and MintedCrowdsale. The key action required is transferring ownership (or the minter role) of the token to the crowdsale contract.


4. A Modular Design with OpenZeppelin

Instead of writing all this logic from scratch, we will use OpenZeppelin's modular contracts. This is analogous to using a mature library or framework in backend development—it saves time, reduces risk, and ensures you're following best practices.

The base Crowdsale contract can be extended by inheriting from other specialized contracts to add features like:

  • CappedCrowdsale: Adds a maximum funding goal (cap).
  • TimedCrowdsale: Adds a start and end time for the sale.
  • WhitelistedCrowdsale: Restricts purchases to pre-approved addresses.

This inheritance structure allows you to compose a crowdsale with the exact features you need.

Crowdsale Contract Inheritance Diagram
This diagram illustrates how a final crowdsale contract can be built by inheriting functionalities from various modular OpenZeppelin contracts like CappedCrowdsale, TimedCrowdsale, and MintedCrowdsale, all built upon the base Crowdsale contract.
Test your understanding!

You've been asked to add a new feature: each investor should only be allowed to contribute a maximum of 5 ETH in total. Based on our discussion of state variables, what new piece of data would your contract need to store to enforce this rule? How would you structure this data?

Show answer

To enforce an individual cap, the contract needs to remember how much each investor has contributed so far.

The best way to store this is with a mapping:
mapping(address => uint256) public contributions;

This creates a key-value store where the key is the investor's address and the value is the total uint256 amount they have contributed in wei. With every purchase, you would update this value and check that the new total does not exceed the 5 ETH limit.


Conclusion

We have now designed the complete blueprint for a basic crowdsale contract. We focused on the essential state and logic required, mirroring the design process used in professional software development.

Key Takeaways:

  • A crowdsale contract's primary state variables are the token it sells, the wallet that collects funds, the rate (price), and the total weiRaised.
  • The core logic involves initializing these parameters in the constructor and a purchase function that validates the sale, calculates the token amount, delivers tokens, and forwards the ETH.
  • For our project, we will use a Minting Model, where the crowdsale contract has the power to mint new tokens for investors.
  • Leveraging modular libraries like OpenZeppelin (Crowdsale, MintedCrowdsale) provides a secure and efficient foundation, much like using a framework in backend development.

Preview of the Next Lesson:
With our design complete, it's time to start coding. In the next lesson, you will implement a function to accept ETH from investors and calculate token amounts. We will translate the purchase logic we designed today into a working Solidity function.

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

Sign up