Recently, I completed the Foundry Solidity Smart Contract course by Patrick Collins. Throughout the course, I gained hands-on experience with generating randomness on blockchains using Chainlink VRF, working with ERC20s and NFTs, exploring DeFi, building upgradeable smart contracts, and diving into DAOs—all using the powerful tools provided by Foundry.
To cement some of the concepts I had learned, I decided to get my hands dirty by building a Crowdfunding dApp which will be discussed in this article.
This article will cover a range of beginner-friendly concepts, from using struct
to define complex data types, utilizing mappings
to store and access campaign data efficiently, and implementing require
statements to enforce critical conditions and ensure contract integrity. You'll also learn how to send ETH to a function securely and declare custom error types in Solidity to handle exceptions. Additionally, I'll walk you through writing comprehensive tests in Solidity to validate your contract’s functionality.
Finally, we'll generate an RPC URL with Alchemy to interact with the blockchain seamlessly and explore how to deploy your contracts on the Sepolia testnet.
Better yet, we'll put all of this into practice using a real code editor—preferably VSCode.
Prerequisites
- A solid understanding of fundamental blockchain concepts, particularly smart contracts, and their functionality.
- Basic familiarity with Solidity for writing smart contracts, including key concepts such as functions, modifiers, events, and error handling (though I will briefly explain them throughout the article).
- A working knowledge of
Makefiles
and their use in automating project setups.
When you're ready, let's dive in! 🚀
Foundry installation & project setup
Foundry is a comprehensive toolchain for smart contract development. It handles your dependencies, compiles your project, runs tests, deploys contracts, and enables interaction with the blockchain via the command line and Solidity scripts.
Note: If you're new to Foundry and using Windows, you'll need to use the WSL as your terminal, since Foundry doesn't currently support Powershell or the command prompt.
For first-time Foundry users, open your terminal and run the following command:
curl -L https://foundry.paradigm.xyz | bash
This will install Foundryup
. Simply follow the on-screen instructions to make the Foundryup
command available in your CLI.
Next, run the Foundryup
command in your terminal. This will install the latest (nightly) precompiled binaries: forge
, cast
, anvil
, and chisel
.
Once installation is complete, open your code editor, navigate to your projects folder, and create a directory for the crowdfunding dApp project we're about to build. For this tutorial, I'll name mine crowdfunding-dApp
.
Finally, to initialize a Foundry application, run the following command in your terminal:
forge init
If successful, your project structure should appear like this:
Inside the src
folder, you'll find boilerplate code for a counter
contract, with corresponding scripts and tests in the script
and test
folders. Delete these files so we can start our project from scratch.
This takes us to the next section.
Developing the Crowdfunding Smart Contract
Before diving into the implementation, let’s outline the key features of this project:
- Create a function that allows users to launch campaigns with a specific funding goal and deadline. This function should
revert
with an error if the specified deadline is in the past. - Develop a function that enables donors to contribute to a campaign by sending funds to the campaign creator’s address. This function should
revert
with an error in the following scenarios:- If the donor sends zero ETH.
- If the campaign’s deadline has already passed.
- If the campaign’s funding target has already been reached.
For each of these conditions, we’ll define custom errors.
To get started, create a new file in your src
folder named crowdfunding.sol
. This file will contain the smart contract code for our crowdfunding dApp.
Begin by pasting the following code, which specifies the Solidity version and defines the contract. Notice that we have also defined the custom errors we'll use throughout the contract:
//SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
contract Crowdfunding {
error Crowdfunding__DeadlineMustBeInTheFuture();
error Crowdfunding__CantSendZeroEth();
error Crowdfunding__CampaignDeadlineElapsed();
error Crowdfunding__TargetMetForCampaign();
}
Next, we’ll outline the campaign structure using the struct
keyword and declare the necessary state variables. Inside the contract, add the following code:
struct Campaign {
address owner;
string title;
string description;
uint256 target;
uint256 deadline;
uint256 amountCollected;
string image;
address[] donators;
uint256[] donations;
}
mapping(uint256 => Campaign) public campaigns;
uint256 public campaignId = 0;
In this snippet, we define a struct
to represent each campaign, and we declare a mapping
variable that links a uint256
type to the Campaign
struct. If you’re familiar with JavaScript, you can think of a mapping as an object that stores data in key-value pairs.
Lastly, we introduced a campaignId
variable, which will serve as the key for our mappings as we add new campaigns.
With this foundation in place, we’re ready to start creating the contract functions.
To begin, we'll create a function that allows users to launch a campaign. Add the following code snippet to your file. Comments have been included to clarify each part:
function createCampaign(
address _owner,
string memory _title,
string memory _description,
uint256 _target,
uint256 _deadline,
) public returns (uint256) {
Campaign storage campaign = campaigns[campaignId]; // Assign a key to the mapping
if (_deadline < block.timestamp) {
revert Crowdfunding__DeadlineMustBeInTheFuture();
}
// Populate the campaign details using the provided input
campaign.owner = _owner;
campaign.title = _title;
campaign.description = _description;
campaign.target = _target;
campaign.deadline = _deadline;
campaign.amountCollected = 0;
campaignId++; // Increment the state value after a new campaign is added
return campaignId - 1; // Return the index of the newly created campaign
}
The createCampaign
function is designed to allow users to create new crowdfunding campaigns on the blockchain. It takes several parameters, including the campaign owner
's address, the title
and description
of the campaign, the funding target, and the deadline for the campaign. The function first checks if the provided deadline is in the future; if it's not, the function reverts with an error, Crowdfunding__DeadlineMustBeInTheFuture()
. If the deadline
is valid, the function then stores the campaign details in a mapping using the current campaignId
as the key.
After storing the campaign information, the function increments the campaignId
to ensure each new campaign gets a unique identifier. It finally returns the id
of the newly created campaign. This id
can be used to reference the campaign for future actions, like making donations. The function essentially sets up the campaign's foundation, ensuring it has all the necessary information and a unique identifier for tracking.
Next, we'll implement the function for donating to the campaign. Add the below function to your contract.
function donateToCampaign(uint256 _campaignId) public payable {
// revert if donor isnt sending anything
if (msg.value <= 0) {
revert Crowdfunding__CantSendZeroEth();
}
// revert if deadline is in the past
if (campaigns[_campaignId].deadline < block.timestamp) {
revert Crowdfunding__CampaignDeadlineElapsed();
}
// revert if target is met
if (campaigns[_campaignId].amountCollected == campaigns[_campaignId].target) {
revert Crowdfunding__TargetMetForCampaign();
}
Campaign storage campaign = campaigns[_campaignId];
uint256 remainingFundsNeeded = campaign.target - campaign.amountCollected;
// Handle contributions based on the remaining funds needed
// next code block optimized for CEI
if (msg.value <= remainingFundsNeeded) {
campaign.amountCollected += msg.value;
campaign.donators.push(msg.sender);
campaign.donations.push(msg.value);
(bool callSuccess,) = payable(getCampaign(_campaignId).owner).call{value: msg.value}("");
// reupdate state variables
if (!callSuccess) {
campaign.amountCollected -= msg.value;
campaign.donators.pop();
campaign.donations.pop();
}
} else {
// Handle excess contributions and refunds
uint256 excessAmount = msg.value - remainingFundsNeeded;
uint256 amountToDonate = msg.value - excessAmount;
// Refund the excess amount to the contributor
payable(msg.sender).transfer(excessAmount);
// Update the total contributions with the amount that was supposed to be donated
campaign.amountCollected += amountToDonate;
campaign.donators.push(msg.sender);
campaign.donations.push(amountToDonate);
(bool callSuccess,) = payable(getCampaign(_campaignId).owner).call{value: amountToDonate}("");
if (!callSuccess) {
campaign.amountCollected -= amountToDonate;
campaign.donators.pop();
campaign.donations.pop();
}
}
}
function getCampaign(uint256 _campaignId) public view returns (Campaign memory) {
return campaigns[_campaignId];
}
In the above code block, the donateToCampaign
function is designed to facilitate secure contributions to a specific crowdfunding campaign on the blockchain. It first performs several checks to ensure the donation is valid: it verifies that the donor is sending a non-zero amount of ETH, ensures that the campaign’s deadline hasn’t passed, and confirms that the campaign hasn’t already met its funding target. If any of these conditions are not met, the function reverts the transaction, preventing the donation from going through.
Once the initial checks are passed, the function handles the donation by updating the campaign's total funds collected and recording the donor’s contribution. If the donation exceeds the amount needed to meet the campaign’s target, the excess is refunded to the donor, and only the necessary amount is added to the campaign’s funds. The function also attempts to transfer the donation directly to the campaign owner, and if this transfer fails, it reverts all state changes to maintain the integrity of the campaign’s data.
We also implemented a getter function, getCampaign
, which allows users to view the details of any campaign using its unique id
. It provides transparency by giving access to all relevant campaign information without altering any data on the blockchain. This ensures that potential donors or interested parties can easily check the status and details of a campaign before deciding to contribute.
To complete our contract, we’ll add two important getter functions: getDonators
and getCampaigns
. These functions are essential for retrieving key information about the campaigns and will be particularly useful when writing tests.
In your contract, include the following code:
function getDonators(uint256 _campaignId) public view returns (address[] memory) {
return campaigns[_campaignId].donators;
}
function getCampaigns() public view returns (Campaign[] memory) {
Campaign[] memory allCampaigns = new Campaign[](campaignId); // create a new array of length campaignId
require(allCampaigns.length > 0, "No campaign has been created yet");
for (uint256 i = 0; i < campaignId; i++) {
allCampaigns[i] = campaigns[i];
}
return allCampaigns;
}
The getDonators
function returns a list of all addresses that have donated to a specific campaign, allowing us to easily track contributors. The getCampaigns
function, on the other hand, compiles all the campaigns into an array, providing a comprehensive view of every campaign created so far.
In the next section, we'll move on to writing tests for the contract.
Testing the Crowdfunding Smart Contract
In Foundry, each time a test is run, the contract is deployed on the local Anvil chain, typically through a setup
configuration. This ensures that a new contract is deployed for each test, providing a clean state for every test case.
However, I like to separate the deployment logic from the tests themselves by implementing a deployment script independently of the tests—though this isn't strictly necessary. If this seems unclear now, don't worry—you’ll get more insights in the next sub-section.
Implementing the deploy script
First, create a new file in the script
folder of your project and name it DeployCrowdfunding.s.sol
. This follows the standard naming convention for script files and is where we'll write our deployment logic.
Paste the following code into it:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import {Script} from "forge-std/Script.sol";
import {Crowdfunding} from "../src/Crowdfunding.sol";
contract DeployCrowdfunding is Script {
function run() external returns (Crowdfunding) {
vm.startBroadcast();
Crowdfunding crowdfunding = new Crowdfunding();
vm.stopBroadcast();
return crowdfunding;
}
}
The script above begins by importing necessary modules, including the Script
from forge-std, a standard utility for writing scripts in Foundry. The DeployCrowdfunding
contract, which inherits the Script
contract, defines a run function that is automatically executed when the script is run. Within this function, the deployment process is bracketed by vm.startBroadcast()
and vm.stopBroadcast()
, which signal the beginning and end of the broadcasted deployment on the blockchain. The Crowdfunding
contract is then instantiated and deployed, and the resulting contract instance is returned.
With this, we have handled the deployment of the Crowdfunding
contract, we can now go ahead to write tests.
Writing Tests
Here's a quick breakdown of the tests we will be writing; these tests will cover a range of scenarios to guarantee the robustness of our contract.
testCampaignCreationRevertsIfDeadlineIsNotInTheFuture: When a user attempts to create a campaign with a date that's in the past—this test ensures that such attempts are blocked, safeguarding against illogical campaign setups.
testCreateCampaign: This is the core test that verifies if a campaign can be successfully created. We'll check that all the campaign details, like the title, description, target, and deadline, are stored correctly and accessible through the contract.
testDonationRevertsWhenZeroEthIsDonated: This test ensures our contract correctly rejects any attempts to donate without sending ETH, preventing empty contributions from slipping through.
testDonationRevertsWhenDeadlinePasses: Donations should only be accepted within the campaign's active period. This test ensures that the contract stops accepting donations once the deadline has passed.
testDonationRevertsWhenTargetIsMet: When a campaign hits its target, there's no need for further contributions. This test verifies that the contract prevents overfunding and respects the campaign's goals.
testDonateToCampaign: Here, we'll ensure that donations are processed correctly—funds are received, campaign balances are updated, and the donor’s details are recorded accurately.
testReverseExcessAmountToSender: Sometimes, a donor might send more ETH than needed to reach the target. This test ensures that any excess funds are returned to the sender, maintaining fairness and transparency.
Each of these tests ensures that our crowdfunding platform works securely, and ultimately, as intended. Now, let's start implementing the test contract.
Navigate to the test
folder and create a new file named CrowdfundingTests.t.sol
, this follows the standard naming convention for test files. Paste the following code into the newly created file:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import {Test} from "forge-std/Test.sol";
import {Crowdfunding} from "../src/Crowdfunding.sol";
import {DeployCrowdfunding} from "../script/DeployCrowdfunding.s.sol";
contract CrowdfundingTests is Test {
}
Here, we've laid the foundation for our test suite. The Test
contract from the Foundry standard library provides us with a range of tools to simulate blockchain transactions and verify that our contract behaves as expected.
We've also imported our Crowdfunding
contract from the src
folder, which will serve as the focus of our tests. Additionally, we imported the DeployCrowdfunding
contract from the script
folder to handle the deployment of our Crowdfunding
contract when we run tests. Finally, we declared the CrowdfundingTests
contract, which inherits the Test
contract.
To effectively simulate the creation of a campaign and the process of making donations, we need to set up some initial parameters that will be used across our test functions. These parameters are crucial for mimicking real-world scenarios where multiple users interact with our contract.
Paste the code below inside the CrowdfundingTests
contract.
Crowdfunding crowdFunding;
address owner = makeAddr("user");
address donor1 = makeAddr("donor1");
address donor2 = makeAddr("donor2");
uint256 campaignId;
string constant TITLE = "Help Fund My Project";
string constant DESCRIPTION = "A project that will change the world!";
uint256 constant TARGET = 5 ether;
uint256 deadline = block.timestamp + 7 days;
In the first line, we declared an instance of the Crowdfunding
contract. This instance will allow us to directly interact with the contract’s external functions during our tests.
Next, we define addresses for the owner and two donors. The makeAddr
function is a utility from the Foundry framework that generates Ethereum addresses for testing purposes. These addresses represent the different actors in our crowdfunding scenario, with the owner
being the campaign creator and the donors
being contributors to the campaign.
We then introduce a campaignId
, which will store the unique identifier of the campaign created during the tests. This id
is essential for referencing specific campaigns, especially when testing multiple campaign creations.
Lastly, we implement parameters needed to simulate a realistic campaign which are passed as arguments when creating a campaign.
With the initial setup complete, we can start writing our actual tests. However, before diving into the individual test cases, it's essential to configure a setUp
function. This function, which we mentioned earlier, ensures that each test begins with a fresh and consistent state, making our testing process more reliable.
Paste the code snippet below into your CrowdfundingTest
contract to configure the setup
function.
function setUp() public {
DeployCrowdfunding deployCrowdFunding = new DeployCrowdfunding();
crowdFunding = deployCrowdFunding.run();
vm.deal(owner, 10 ether);
vm.deal(donor1, 5 ether);
vm.deal(donor2, 5 ether);
}
The first line within the setUp
function creates a new instance of the DeployCrowdfunding
contract.
Next, we call the run
function within the DeployCrowdfunding
contract, which handles the actual deployment of the Crowdfunding
contract as we implemented in the script.
The last three lines use a Foundry cheatcode
to simulate funding the owner
and donor
addresses with specific amounts of ETH. The vm.deal function is used to assign these addresses a starting balance, ensuring that they have sufficient funds to participate in the campaign creation and donation processes during the tests.
With the foundational setup in place, it's time to start writing our tests. We'll begin with the first test on our list: testCampaignCreationRevertsIfDeadlineIsNotInTheFuture
. This test is designed to ensure that the contract correctly handles scenarios where a campaign is created with a deadline that has already passed. The goal is to confirm that the contract will revert the transaction, preventing the creation of invalid campaigns.
To implement this test, paste the following code:
function testCampaignCreationRevertsIfDeadlineIsNotInTheFuture() public {
vm.warp(block.timestamp + deadline + 1);
vm.expectRevert(Crowdfunding.Crowdfunding__DeadlineMustBeInTheFuture.selector);
vm.prank(owner);
crowdFunding.createCampaign(owner, TITLE, DESCRIPTION, TARGET, deadline);
}
The test starts by using vm.warp(block.timestamp + deadline + 1);
, which advances the blockchain's internal clock to a point just beyond the intended deadline. This simulates a scenario where the current time is already past the campaign's deadline. Learn more about vm.warp
here.
Next, the vm.expectRevert(Crowdfunding.Crowdfunding__DeadlineMustBeInTheFuture.selector);
line expects a revert when the next function is called. Specifically, it expects the revert reason to match the custom error Crowdfunding__DeadlineMustBeInTheFuture
.
Finally, vm.prank(owner);
simulates a transaction sent from the owner's address, followed by the call to crowdfunding.createCampaigns(owner, TITLE, DESCRIPTION, TARGET, deadline);
. This line attempts to create a campaign with an expired deadline. Since the deadline is in the past, the contract should revert, confirming that our validation logic is working as intended.
Note: To check if every test runs successfully, run the following command in your terminal:
forge test
Since we have only written one test so far, a successful run will look like this:
The next test, testCreateCampaign
, will simulate the behavior of the createCampaign
function. Since creating a campaign and retrieving the campaignId
will be necessary for several upcoming tests, let's streamline the process by creating a modifier that we can reuse throughout the test suite.
Add this modifier to your test contract:
modifier createCampaign() {
vm.prank(owner);
campaignId = crowdFunding.createCampaign(owner, TITLE, DESCRIPTION, TARGET, deadline);
_;
}
Above, we created a modifier named createCampaign
that helps us automate the creation of a campaign by calling the createCampaign
function with the predefined values owner
, TITLE
, DESCRIPTION
, TARGET
, and deadline
. It uses vm.prank(owner)
to simulate transactions as the owner account. The campaignId
generated from this creation is then stored and can be reused in subsequent tests. Lastly, the _;
in the modifier allows the test function to execute after the campaign is created.
Now that the modifier is in place, let's proceed with the actual testCreateCampaign
function, which ensures that campaigns are created as expected. Add the following code:
function testCreateCampaign() public createCampaign {
string memory campaignTitle = crowdFunding.getCampaign(campaignId).title;
string memory campaignDescription = crowdFunding.getCampaign(campaignId).description;
uint256 campaignTarget = crowdFunding.getCampaign(campaignId).target;
uint256 campaignDeadline = crowdFunding.getCampaign(campaignId).deadline;
uint256 campaignAmountCollected = crowdFunding.getCampaign(campaignId).amountCollected;
assertEq(owner, owner);
assertEq(campaignTitle, TITLE);
assertEq(campaignDescription, DESCRIPTION);
assertEq(campaignTarget, TARGET);
assertEq(campaignDeadline, deadline);
assertEq(campaignAmountCollected, 0);
}
As you'll notice, the function above uses the createCampaign
modifier to create a campaign, ensuring that we don't manually repeat the process in every test.
It then retrieves the created campaign's details title
, description
, target
, deadline
, and amount collected
using the getCampaign function defined in the Crowdfunding
contract. Each of these values is stored in memory to be compared with the expected values (predefined in the test).
Finally, the assertEq
statements verify that the campaign’s actual values match the expected inputs. The assertions ensure the owner is correct, confirm the title
, description
, target
, and deadline
match the provided values during creation, and lastly, verify that the initial amount collected is 0, as no donations have been made yet.
The next test we'll implement is the testDonationRevertsWhenZeroEthIsDonated
; which ensures that zero eth isn't donated to a campaign. Here's the code you need to add to your test contract:
function testDonationRevertsWhenZeroEthIsDonated() public createCampaign {
vm.expectRevert(Crowdfunding.Crowdfunding__CantSendZeroEth.selector);
// Donate to the campaign
vm.prank(donor1);
crowdFunding.donateToCampaign{value: 0}(campaignId);
}
You should already be acquainted with the Foundry cheatcodes used in this test from the first one. The main distinction here is that we're invoking the donateToCampaign
function while sending zero ETH.
Now, let's proceed to the next test, testDonationRevertsWhenDeadlinePasses
, which ensures that donations are rejected once the campaign deadline has expired.
Paste the following code in your contract to implement this:
function testDonationRevertsWhenDeadlinePasses() public createCampaign {
vm.warp(block.timestamp + deadline + 1);
vm.expectRevert();
// Donate to the campaign
vm.prank(donor1);
crowdFunding.donateToCampaign{value: 2 ether}(campaignId);
}
In this case, donor1
is unable to send the 2 ether
to the campaign owner because the campaign deadline has already passed.
Next up is the testDonationRevertsWhenTargetIsMet
test, which checks that a donation is rejected once the campaign's funding target has been reached, as its name suggests.
Add the following code to implement this function:
function testDonationRevertsWhenTargetIsMet() public createCampaign {
vm.prank(donor1);
crowdFunding.donateToCampaign{value: 5 ether}(campaignId);
vm.expectRevert(Crowdfunding.Crowdfunding__TargetMetForCampaign.selector);
vm.prank(donor2);
crowdFunding.donateToCampaign{value: 1 ether}(campaignId);
}
In this test, we simulate two donors contributing to the same campaign. The first donor meets the campaign's funding target by sending the required amount of ETH. The second donor's attempt to donate should therefore trigger a revert since the target has already been reached.
That concludes the tests for the revert scenarios. Next, we'll move on to testing another core functionality of the contract with the testDonateToCampaign
test. This test verifies that donations to a campaign are handled correctly. Below is the code that implements this:
function testDonateToCampaign() public createCampaign {
// Donate to the campaign
vm.prank(donor1);
crowdFunding.donateToCampaign{value: 2 ether}(campaignId);
// Validate campaign donation data
uint256 campaignAmountCollected = crowdFunding.getCampaign(campaignId).amountCollected;
uint256[] memory donations = crowdFunding.getCampaign(campaignId).donations;
address[] memory donators = crowdFunding.getCampaign(campaignId).donators;
assert(campaignAmountCollected == 2 ether);
assertEq(donators.length, 1);
assertEq(donations[0], 2 ether);
}
In this test, donor1
donates 2 ether
to the campaign. We then validate that the campaign correctly tracks the amount collected, the donations made, and the donor's address. The assert
statements ensure that 2 ether
is properly reflected in the campaign's data, and that donor1
is listed as the sole contributor with the correct donation amount.
Next, we’ll test the contract's ability to return excess funds to the donor if they send more ether
than is needed to meet the campaign’s target. The code below demonstrates how this is implemented:
function testReverseExcessAmountToSender() public createCampaign {
vm.prank(donor1);
crowdFunding.donateToCampaign{value: 2 ether}(campaignId);
vm.prank(donor2);
crowdFunding.donateToCampaign{value: 4 ether}(campaignId);
assertEq(donor2.balance, 2 ether);
}
In this test, donor1
donates 2 ether
to the campaign, and donor2
attempts to donate 4 ether
. However, since the campaign only needs 2 more ether
to meet its target, the contract should accept the necessary amount and return the extra 2 ether
to donor2
. The final assertEq
statement verifies that donor2
's balance reflects the excess ether
refund. This ensures that the contract correctly handles over-contributions.
So far, we’ve tested the key functionalities of our contract. Now, to check how much of our code is covered by these tests, you can run the following command:
forge coverage
The output should resemble something like this:
As you can see, we've already covered a significant portion. However, if you'd like to dive deeper and challenge yourself, you can write additional tests, particularly for the getter functions in the contract.
The final step is deploying the smart contract, which will be discussed in the next section.
Deploying Your Contract Locally and on the Sepolia Testnet
With Foundry, you can deploy your contract either locally using anvil
or directly on a testnet. In this section, I'll guide you through both deployment methods. We will manage both scenarios using a Makefile
, so ensure that make
is installed on your system.
Deploying on a Local Chain
For both local and testnet deployments, you'll need separate private keys and RPC URLs. In this subsection, I'll walk you through deploying the contract locally on a simulated blockchain.
To begin, open your terminal and run the anvil
command. This will generate a list of private keys and a localhost URL (typically https://localhost:8545
), which will serve as your RPC URL for the local deployment.
Once you have your private key and RPC URL, create a Makefile
in the root folder of your project. At this stage, your project folder structure should look something like this:
Now, paste the following into your Makefile:
-include .env
DEFAULT_ANVIL_KEY := your-anvil-private-key
# Default deployment uses anvil
NETWORK_ARGS := --rpc-url http://localhost:8545 --private-key $(DEFAULT_ANVIL_KEY) --broadcast
deploy:
@forge script script/DeployCrowdfunding.s.sol:DeployCrowdfunding $(NETWORK_ARGS)
Make sure to replace your-anvil-private-key
with one of the private keys generated by anvil
.
To deploy the contract, run the following command in your terminal:
make deploy
If the deployment is successful, the output will look something like this:
Deploying to the Sepolia Testnet
Note: To deploy on a testnet, you'll need a virtual wallet like Metamask.
For deployment to the Sepolia testnet, you'll need to generate your private key and RPC URL. You can follow the steps in this guide to set everything up.
After following the guide, you should have:
- Created an account on Alchemy,
- Set up an app and obtained an API key,
- Added Sepolia testnet ETH to your wallet using a faucet,
- Integrated your virtual wallet (such as Metamask) with your Alchemy project.
In addition, if you don't already have an account on Etherscan, create one and retrieve your API key from the "API keys" page. This will allow for contract verification post-deployment. Once these steps are completed, you're ready to proceed.
Now, in your project's root directory, create a .env
file and add the following key-value pairs:
SEPOLIA_RPC_URL = "https://eth-sepolia.g.alchemy.com/v2/your-api-key"
ETHERSCAN_API_KEY = "your-etherscan-api-key"
PRIVATE_KEY = "your-metamask-private-key"
Storing these values in the .env
file helps keep sensitive data secure and prevents exposing it during deployment. Also, make sure the .env
file is added to your .gitignore
to avoid accidentally pushing confidential details to a public repository like GitHub.
Next, you'll need to update your Makefile
to allow deployment to a testnet, such as Sepolia. This will enable switching between local and testnet environments seamlessly.
Update your Makefile
as follows:
-include .env
DEFAULT_ANVIL_KEY := your-anvil-private-key
# Default deployment on anvil
NETWORK_ARGS := --rpc-url http://localhost:8545 --private-key $(DEFAULT_ANVIL_KEY) --broadcast
# Condition to deploy on Sepolia
ifeq ($(findstring --network sepolia,$(ARGS)),--network sepolia)
NETWORK_ARGS := --rpc-url $(SEPOLIA_RPC_URL) --private-key $(PRIVATE_KEY) --broadcast --verify --etherscan-api-key $(ETHERSCAN_API_KEY) -vvvv
endif
deploy:
@forge script script/DeployCrowdfunding.s.sol:DeployCrowdfunding $(NETWORK_ARGS)
To deploy your contract on the Sepolia testnet, use the following command:
make deploy ARGS="--network sepolia"
This setup allows for easy switching between deploying locally using anvil
or deploying to a testnet like Sepolia.
If you have sufficient ETH, the contract should deploy successfully. You’ll then be able to view the deployment details on your Alchemy dashboard. Additionally, once you paste the contract address into Etherscan, you should see that it has been successfully verified.
Conclusion
We've reached the end of this article, where I’ve walked you through the entire process of building, testing, and deploying a crowdfunding smart contract using Solidity, Foundry, and Makefiles. From deploying locally on anvil
to pushing the contract to a testnet like Sepolia, you've learned how to set up a complete development and testing environment to ensure your contract operates securely and efficiently.
You can access the source code for this project here. If you have any suggestions or improvements, feel free to submit a PR or reach out via email at olasunkanmiibalogun@gmail.com. You can also connect with me on LinkedIn.
This article is part of my ongoing journey to landing my first role in the blockchain space. I’ll continue documenting what I build and learn along the way, so stay tuned for more content. Let’s keep building! 👊