Recommended resources
The freely available resources this lesson will direct you to.
- Build a Solidity Crowdsale App (Presale) | Mint and sell ERC20 tokens | Solidity & Reactwww.youtube.com
- Ethereum Smart Contracts In Solidity 2 - Payable Functions | Blockchainwww.youtube.com
- How To Create An ERC-20 Token Sale With Solidity And Deploy ...nzouat.com
- Create an ERC20 Token and Vendor Contract in Solidityspeedrunethereum.com
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 globalmsgobject 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.valueis in wei ( wei = 1 ETH).- Our token likely also has 18 decimals, so its base unit is the "token-wei".
- Our
tokensPerEthrate 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.valuewill be1 * 10^18. - The calculation becomes
(1 * 10^18) * 100. - The result,
amountOfTokens, is100 * 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:
- Be
external(more gas-efficient thanpublicif only called from outside the contract) andpayable. - Reject transactions that send 0 ETH.
- Calculate the
amountOfTokensto 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.
- What will the value of
msg.valuebe inside the function (in wei)? - What will the calculated
amountOfTokensbe (in the token's smallest unit)?
Show answer
- Since 1 ETH = wei,
msg.valuewill be0.2 * 10^18wei, which is200,000,000,000,000,000wei. - The calculation is
amountOfTokens = msg.value * tokensPerEth.
So,amountOfTokens = (0.2 * 10^18) * 500 = 100 * 10^18.
The investor will receive 100 tokens, represented as100,000,000,000,000,000,000in 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
payableto receive Ether. - The
msg.valueglobal variable holds the amount of ETH sent with a call, measured in wei. - The
msg.senderglobal 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.