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 Contribution Function

Hello! Welcome back to the course.

In our last lesson, we created the architectural blueprint for our crowdsale contract. We identified the key state variables needed—like the token's address and the price—and outlined the core logic for initializing the contract and handling purchases.

Today, we move from design to implementation. Your goal is to implement a function to accept ETH from investors and calculate the corresponding token amounts. This is the heart of your token sale. For you, as a backend developer, this is analogous to building a critical API endpoint that receives a payment and calculates the goods to be delivered.


1. Enabling Your Contract to Receive ETH

In standard programming, a function simply runs its code. In Solidity, if you want a function to be able to receive cryptocurrency (Ether), you must explicitly mark it as such. This is a fundamental security feature of the language.

To do this, we use the payable keyword. When a function is marked payable, it gains access to a special global variable: msg.value.

  • payable: A modifier that allows a function to accept Ether sent along with the function call.
  • msg.value: A property of the global msg object that holds the amount of Ether sent with the call, measured in wei (the smallest unit of ETH, where ).

To see this in action, let's watch a short, focused video that explains payable functions clearly.

Ethereum Smart Contracts In Solidity 2 - Payable Functions | Blockchain

The video "Ethereum Smart Contracts In Solidity 2 - Payable Functions" from the What The Func? channel provides an excellent, concise demonstration of how payable functions work and how to use msg.value.

Watch the video from 0:00-1:22 and then from 5:46-8:15. Pay close attention to how the buyTickets function is declared as payable and how it uses msg.value to check the amount of Ether sent by the user.

As the video demonstrated, simply adding the payable keyword allows your function to receive ETH and gives you the msg.value variable to work with. Any attempt to send ETH to a non-payable function will be automatically rejected by the Ethereum Virtual Machine (EVM).


2. Implementing the buyTokens() Function

Now let's create the central function for our crowdsale: buyTokens(). This function will be public (so anyone can call it) and payable (to accept ETH).

Its primary job is to calculate how many tokens an investor should receive based on the ETH they send. In the last lesson, we designed a state variable for this, often called rate or tokensPerEth.

Let's assume we have a state variable: uint256 public tokensPerEth = 100;
This means for every 1 ETH an investor sends, they should receive 100 of our tokens.

The Calculation

The formula seems simple: amountOfTokens = amountOfEth * tokensPerEth. However, we must be precise about units.

  • msg.value is in wei ( wei = 1 ETH).
  • Our token likely also has 18 decimals, so its base unit is the "token-wei".
  • Our tokensPerEth rate connects the whole units (ETH and Tokens).

Fortunately, if both your token and ETH use 18 decimals, the math simplifies nicely.

Create an ERC20 Token and Vendor Contract in Solidity

Let's look at a practical guide from Speedrun Ethereum. It has a section on implementing buyTokens() that includes a crucial hint about handling decimals.

Read the section titled "Implementing buyTokens()". Focus on the explanation under the 'Hint' and the final solution code. Notice the formula used: tokensToBuy = msg.value * tokensPerEth.

As the resource shows, when the decimals match, the calculation uint256 amountOfTokens = msg.value * tokensPerEth; works perfectly.

Let's walk through why:

  • If an investor sends 1 ETH, msg.value will be 1 * 10^18.
  • The calculation becomes (1 * 10^18) * 100.
  • The result, amountOfTokens, is 100 * 10^18. This is exactly 100 of our tokens, represented in their smallest unit (like wei for ETH).

The Function Code

Now we can assemble the buyTokens function. It will:

  1. Be external (more gas-efficient than public if only called from outside the contract) and payable.
  2. Reject transactions that send 0 ETH.
  3. Calculate the amountOfTokens to be sent to the buyer (msg.sender).

Here is a basic implementation based on the Speedrun Ethereum guide:

// In your Crowdsale contract...

// Assume you have these state variables from the previous lesson
IERC20 public yourToken;
uint256 public tokensPerEth = 100; // 100 tokens per 1 ETH

// Custom errors are more gas-efficient than require() strings
error InvalidEthAmount();

// The event to log purchases
event BuyTokens(address indexed buyer, uint256 amountOfETH, uint256 amountOfTokens);

/**
 * @dev Function to buy tokens from the vendor.
 */
function buyTokens() external payable {
    // 1. Reject zero ETH purchases
    if (msg.value == 0) {
        revert InvalidEthAmount();
    }

    // 2. Calculate the number of tokens to buy
    uint256 amountOfTokens = msg.value * tokensPerEth;

    // TODO: Check if the contract has enough tokens to sell.
    // TODO: Transfer the tokens to the buyer (msg.sender).
    // TODO: Forward the received ETH to the project wallet.

    // 3. Emit an event to log the transaction on the blockchain
    emit BuyTokens(msg.sender, msg.value, amountOfTokens);
}

Notice the TODO comments. We've successfully implemented the ETH acceptance and calculation part of our goal. The actual token transfer and fund forwarding are complex enough to deserve their own lesson, which we'll tackle next.

Test your understanding!

Your crowdsale contract has tokensPerEth set to 500. An investor decides to buy tokens by sending 0.2 ETH to your buyTokens function.

  1. What will the value of msg.value be inside the function (in wei)?
  2. What will the calculated amountOfTokens be (in the token's smallest unit)?
Show answer
  1. Since 1 ETH = wei, msg.value will be 0.2 * 10^18 wei, which is 200,000,000,000,000,000 wei.
  2. The calculation is amountOfTokens = msg.value * tokensPerEth.
    So, amountOfTokens = (0.2 * 10^18) * 500 = 100 * 10^18.
    The investor will receive 100 tokens, represented as 100,000,000,000,000,000,000 in their base unit.

3. A Simpler Way to Buy: The receive Function

The buyTokens() function works well, but it requires the user to know they have to call that specific function. What if they just send ETH directly to the contract's address from their wallet? By default, this transaction would fail.

To handle this, Solidity provides a special function: receive().

The receive function is unique:

  • It is declared as receive() external payable { ... }.
  • It has no function name and takes no arguments.
  • It is executed automatically when the contract receives a plain ETH transfer with no other data attached.

We can use this to create a more seamless user experience by having it call our main buyTokens logic.

Build a Solidity Crowdsale App (Presale) | Mint and sell ERC20 tokens | Solidity & React

The video "Build a Solidity Crowdsale App" explains how a fallback function can be used to handle direct ETH transfers. While the syntax has been updated in modern Solidity, the concept is the same.

Watch from 7:50 to 9:00. The video discusses the older fallback() function, but the principle it explains—that you can catch plain Ether transfers and redirect them to buyTokens—is key. We will use the modern receive() function for this.

Here's how you would implement it in modern Solidity:

// Inside your Crowdsale contract

function buyTokens() external payable {
    // ... all the logic from before
}

// This function is called when someone just sends ETH to the contract
receive() external payable {
    // Redirect the call to the main buyTokens function
    buyTokens();
}

With this addition, your contract can now accept investments in two ways: a direct call to buyTokens() or a simple ETH transfer to the contract address, both achieving the same result.


Conclusion

Congratulations! You have now implemented the most critical piece of your crowdsale contract. You've written a function that can securely accept payments in ETH and correctly calculate the number of tokens an investor is entitled to.

Key Takeaways:

  • Functions must be marked payable to receive Ether.
  • The msg.value global variable holds the amount of ETH sent with a call, measured in wei.
  • The msg.sender global variable holds the address of the account that initiated the call.
  • The token amount is calculated using the rate and msg.value: amountOfTokens = msg.value * tokensPerEth.
  • A receive() function can be implemented to handle direct ETH transfers to the contract, improving the user experience.

Preview of the Next Lesson:
We have the ETH and we know how many tokens to give, but we haven't actually given them yet! In the next lesson, we will integrate the crowdsale with your ERC-20 token to distribute tokens upon purchase. We will write the code to call your token contract's transfer (or mint) function and securely forward the collected ETH to the project's wallet.

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

Sign up