This project contains two crowdsale implementations with advanced features for managing token sales.
Location: packages/contracts/src/TimedCrowdsale.sol
A fully-featured crowdsale contract with the following capabilities:
- ✅ Dynamic Pricing: Owner can update token price during the crowdsale
- ✅ Whitelist System: Only whitelisted addresses can purchase tokens
- ✅ Timed Sales: Opening and closing times control when purchases are allowed
- ✅ Contribution Limits: Minimum and maximum token purchase amounts per transaction
- ✅ Direct ETH Purchases: Users can send ETH directly to the contract
- ✅ Owner Controls: Finalize sale and withdraw funds
whitelistAddress(address)- Add address to whitelist (owner only)setPrice(uint256)- Update token price (owner only)buyTokens(uint256)- Purchase tokens with ETHisOpen()- Check if crowdsale is currently activefinalize()- End sale and transfer remaining tokens + ETH to owner
Location: packages/contracts/src/RefundableCrowdsale.sol
An advanced crowdsale using OpenZeppelin libraries with goal-based refund mechanism.
All features from TimedCrowdsale, plus:
- ✅ Funding Goal: Minimum ETH goal that must be reached
- ✅ Refund Mechanism: Investors can claim refunds if goal not reached
- ✅ Goal Tracking: Real-time goal progress monitoring
- ✅ Batch Whitelisting: Add multiple addresses at once
- ✅ OpenZeppelin Integration: Uses battle-tested ERC20 and security patterns
- ✅ Reentrancy Protection: Enhanced security with ReentrancyGuard
- ✅ Ownable Pattern: Secure ownership management
addToWhitelist(address)- Add single address to whitelistaddToWhitelistBatch(address[])- Add multiple addresses to whitelistremoveFromWhitelist(address)- Remove address from whitelistsetPrice(uint256)- Update token pricebuyTokens(uint256)- Purchase tokens with ETHfinalize()- End sale (transfers funds if goal reached, enables refunds if not)claimRefund()- Claim refund if goal not reachedgoalReachedStatus()- Check if funding goal was reached
-
Deploy TimedCrowdsale
forge script script/DeployCrowdsale.s.sol:DeployCrowdsaleScript --rpc-url <RPC_URL> --broadcast
-
Deploy RefundableCrowdsale
forge script script/DeployRefundableCrowdsale.s.sol:DeployRefundableCrowdsaleScript --rpc-url <RPC_URL> --broadcast
-
Manage Whitelist
CROWDSALE_ADDRESS=<ADDRESS> forge script script/ManageWhitelist.s.sol:ManageWhitelistScript --rpc-url <RPC_URL> --broadcast
-
Update Price
CROWDSALE_ADDRESS=<ADDRESS> NEW_PRICE=<PRICE_IN_WEI> forge script script/UpdatePrice.s.sol:UpdatePriceScript --rpc-url <RPC_URL> --broadcast
All features are thoroughly tested with 35 comprehensive test cases.
Run all tests:
forge testRun specific test suite:
forge test --match-path test/TimedCrowdsale.t.sol -vv
forge test --match-path test/RefundableCrowdsale.t.sol -vvTimedCrowdsale Tests (14 tests):
- Deployment validation
- Whitelist management
- Purchase validations (timing, whitelist, min/max)
- Price updates
- Finalization
- Direct ETH purchases
RefundableCrowdsale Tests (21 tests):
- All TimedCrowdsale tests
- Batch whitelist operations
- Goal tracking
- Finalization with/without goal reached
- Refund mechanism
- Multiple users refund scenarios
// Deploy token
Token token = new Token("MyToken", "MTK", 1000000);
// Deploy crowdsale
uint256 openingTime = block.timestamp + 1 hours;
uint256 closingTime = block.timestamp + 30 days;
TimedCrowdsale crowdsale = new TimedCrowdsale(
token,
0.001 ether, // price per token
500000 * 10**18, // max tokens for sale
openingTime,
closingTime,
1, // min contribution (1 token)
10000 // max contribution (10,000 tokens)
);
// Transfer tokens to crowdsale
token.transfer(address(crowdsale), 500000 * 10**18);// Add single address
crowdsale.whitelistAddress(investorAddress);
// Add multiple addresses (RefundableCrowdsale only)
address[] memory investors = new address[](3);
investors[0] = address1;
investors[1] = address2;
investors[2] = address3;
crowdsale.addToWhitelistBatch(investors);// Method 1: Direct ETH send
(bool success,) = address(crowdsale).call{value: 1 ether}("");
// Method 2: Call buyTokens function
uint256 tokenAmount = 1000 * 10**18;
uint256 cost = (tokenAmount / 10**18) * price;
crowdsale.buyTokens{value: cost}(tokenAmount);// After closing time
crowdsale.finalize();
// For RefundableCrowdsale, if goal not reached:
crowdsale.claimRefund(); // Investors call thisBoth implementations include:
- ✅ Owner-only administrative functions
- ✅ Reentrancy protection (RefundableCrowdsale)
- ✅ Input validation on all parameters
- ✅ Safe ETH transfers
- ✅ Proper access controls
After deployment, save your contract addresses:
Token: 0x...
Crowdsale: 0x...
- ✅ Add function to update price (owner only)
- ✅ Create script to update price
- ✅ Require investors be whitelisted
- ✅ Store whitelisted users in smart contract
- ✅ Function for owner to add people to whitelist
- ✅ Only whitelisted can buy tokens
- ✅ Add open/closed status with timestamp
- ✅ Restrict purchases to after opening time
- ✅ Require date is in past before purchase
- ✅ Add minimum contribution amount
- ✅ Add maximum contribution amount
- ✅ Create crowdsale with OpenZeppelin
- ✅ Use ERC20 token library
- ✅ Import crowdsale contracts
- ✅ Create refundable crowdsale with minimum funding goal
- ✅ Allow refunds if goal not reached by deadline
- ✅ Adapt contracts to use Foundry
- ✅ Cleanup old, unused contracts
- Deploy contracts to testnet
- Verify contracts on Etherscan
- Add frontend integration
- Create user documentation
- Conduct security audit before mainnet deployment