Recommended resources
The freely available resources this lesson will direct you to.
Hello! Welcome back to our journey of building a token sale contract.
In the last lesson, we successfully added a hard cap to our crowdsale using OpenZeppelin's CappedCrowdsale contract. This ensures our sale has a defined fundraising limit. We've controlled how much can be raised, but we haven't yet controlled when.
Today, your goal is to enforce a sale period by defining start and end times for the crowdsale. Just like setting an expiration window for an API key or a session in a backend service, defining a sale period provides predictability for your project and a clear call to action for your investors.
1. The Importance of a Defined Sale Period
A token sale that runs indefinitely is a red flag for investors. It suggests a lack of planning and urgency. By defining a clear start and end time, you achieve several key objectives:
- Predictability: Everyone knows the exact window of opportunity to participate.
- Urgency: A deadline motivates potential contributors to act.
- Project Timeline: It aligns the fundraising phase with your project's development roadmap.
This is managed on the blockchain using timestamps. The Ethereum Virtual Machine (EVM) provides a global variable, block.timestamp, which returns the Unix timestamp of the current block. Our contract will use this to determine if the sale is active.
2. Introducing TimedCrowdsale
Following the same modular pattern from our last lesson, we will use another component from the OpenZeppelin library: TimedCrowdsale. This contract is designed specifically to add time-based constraints to a sale.
It works by introducing two key state variables:
openingTime: A Unix timestamp before which no purchases are allowed.closingTime: A Unix timestamp after which no purchases are allowed.
Just like CappedCrowdsale, TimedCrowdsale overrides the internal _preValidatePurchase function to add its validation logic, automatically ensuring that any purchase attempt happens within the defined time window.
3. Integrating TimedCrowdsale into Your Contract
Integrating this feature is a straightforward extension of what we've already done. It involves three steps:
- Import the
TimedCrowdsale.solcontract. - Add
TimedCrowdsaleto your contract's inheritance list. - Update the constructor to accept
_openingTimeand_closingTimeand pass them to theTimedCrowdsaleparent constructor.
Our MyTokenSale contract will now inherit from three parent contracts. The updated code 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/crowdsale/validation/TimedCrowdsale.sol"; // 1. Import
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
// 2. Add TimedCrowdsale to the inheritance list
contract MyTokenSale is Crowdsale, CappedCrowdsale, TimedCrowdsale {
constructor(
uint256 rate,
address payable wallet,
IERC20 token,
uint256 cap,
// 3. Add time parameters to the constructor
uint256 openingTime,
uint256 closingTime
)
Crowdsale(rate, wallet, token)
CappedCrowdsale(cap)
// Call the TimedCrowdsale constructor
TimedCrowdsale(openingTime, closingTime)
{
// Constructor body remains empty
}
}
This clean, declarative approach is the power of using a framework. We've added a significant feature with just a few lines of code, relying on OpenZeppelin's secure and audited implementation.
To see this pattern in action, let's review a concise example from the OpenZeppelin team themselves.
How To Create Token and Initial Coin Offering Contracts ...
The OpenZeppelin blog provides a clear and direct example of creating a timed crowdsale. This will reinforce the code structure we've just outlined.
Please read the first part of the article, from the start down to the line that says 'Let’s deploy this contract.'. Focus on how GustavoCoinCrowdsale inherits from TimedCrowdsale and how the constructor is defined to accept _openingTime and _closingTime.
The openingTime and closingTime are simply numbers (uint256) representing Unix timestamps. This is the standard way to handle time on the blockchain. The image below shows how these values might look in a deployment UI like Remix, reinforcing that they are just parameters you provide when the contract is created.

4. Setting the Sale Period in Your Deployment Script
Now for the practical part: how do we generate and pass these timestamps from our Hardhat deployment script? As a backend developer, you're likely familiar with handling timestamps, and the process here is very similar.
We'll use JavaScript's Date object to define our times and then convert them to the Unix timestamp format (seconds since epoch) that Solidity expects.
The OpenZeppelin article you just reviewed shows how to do this in a Truffle environment. Let's look at the same concept within our Hardhat project.
How To Create Token and Initial Coin Offering Contracts ...
Now let's see how this is put into practice in a deployment script. The same OpenZeppelin article shows how to calculate and pass these timestamps.
Read the next section of the article, focusing on the migrations/2_deploy_contracts.js file. Notice how openingTime and closingTime are calculated. The example uses web3.eth.getBlock('latest').timestamp to get the current time, which is a reliable method. We will do the equivalent using ethers.js in Hardhat.
Adapting for Hardhat and Ethers.js
Here is a complete example of a Hardhat deployment script for our MyTokenSale contract, now including the cap and time parameters.
const { ethers } = require("hardhat");
async function main() {
// --- Assume these are already defined ---
const [deployer] = await ethers.getSigners();
const tokenAddress = "0x..."; // Address of your deployed ERC-20 token
const walletAddress = deployer.address; // Or a dedicated project wallet
// --- Crowdsale Parameters ---
const rate = 1000; // 1000 tokens per 1 ETH
const cap = ethers.utils.parseEther("150"); // 150 ETH cap from last lesson
// --- Time Parameters ---
// Get the current block's timestamp from the provider
const latestBlock = await ethers.provider.getBlock("latest");
const now = latestBlock.timestamp;
// Set opening time to 1 hour from now
const openingTime = now + 3600; // 3600 seconds in an hour
// Set closing time to 14 days after opening
const saleDurationInSeconds = 86400 * 14; // 86400 seconds/day * 14 days
const closingTime = openingTime + saleDurationInSeconds;
// --- Deployment ---
const MyTokenSale = await ethers.getContractFactory("MyTokenSale");
console.log("Deploying MyTokenSale with the following parameters:");
console.log(` Rate: ${rate} tokens per ETH`);
console.log(` Cap: ${ethers.utils.formatEther(cap)} ETH`);
console.log(` Opening Time (timestamp): ${openingTime}`);
console.log(` Closing Time (timestamp): ${closingTime}`);
const tokenSale = await MyTokenSale.deploy(
rate,
walletAddress,
tokenAddress,
cap,
openingTime,
closingTime
);
await tokenSale.deployed();
console.log("\nMyTokenSale deployed to:", tokenSale.address);
}
main().catch((error) => {
console.error(error);
process.exitCode = 1;
});
This script reliably sets the sale to open one hour after deployment and run for exactly two weeks.
Test your understanding!
Your project team wants the token sale to start at exactly 9:00 AM UTC on September 1st, 2024, and run for exactly 30 days. How would you define the openingTime and closingTime constants in your JavaScript deployment script to meet this requirement?
Show answer
You would use the Date object to create the specific start date and then convert it to a Unix timestamp in seconds.
// Create a Date object for the specific start time in UTC
// The 'Z' at the end signifies UTC time.
const startDate = new Date("2024-09-01T09:00:00Z");
// Convert the date's millisecond timestamp to seconds for the EVM
const openingTime = Math.floor(startDate.getTime() / 1000);
// Calculate closing time 30 days later
const thirtyDaysInSeconds = 86400 * 30; // 86400 seconds/day
const closingTime = openingTime + thirtyDaysInSeconds;
console.log(`Opening Timestamp: ${openingTime}`);
console.log(`Closing Timestamp: ${closingTime}`);
This approach is more precise for fixed calendar dates than calculating from block.timestamp.
Conclusion
Excellent work today! You've added another critical layer of control to your token sale. By defining a strict sale period, you make the crowdsale more professional, predictable, and effective.
Key Takeaways:
- A defined sale period is crucial for creating predictability and urgency.
- OpenZeppelin's
TimedCrowdsalecontract provides a secure, reusable implementation for this feature. - The sale period is enforced by checking
block.timestampagainstopeningTimeandclosingTimevalues. - These timestamps are set once during deployment by passing them as arguments to the contract's constructor.
- You can calculate these timestamps precisely in your Hardhat deployment script using JavaScript's
Dateobject.
Preview of the Next Lesson:
Our sale now has limits on how much can be raised and when. But right now, anyone can participate. In our next lesson, we will explore how to add an exclusive access list by learning how to implement a whitelist to restrict participation to specific addresses.