Intro To Foundry: The Kickstarter Smart Contract

Ifeoluwaolubo
Coinmonks
12 min readOct 25, 2023

--

According to their official documentation here, Foundry is a smart contract development toolchain. This simply means that with foundry we can manage our dependencies, compile our projects, run extensive testing, carry out deployments and interact with the blockchain from the Command Line, and all of these is done with Solidity Scripts 😋. Interesting right? Wait till you hear the best part.

Foundry is blazingly fast (probably the most fastest smart contract development tool right now), written in Rust and is also the framework of choice for smart contract security engineers and auditors.

In previous articles we’ve written some very basic smart contracts to help us understand Solidity, but from here on, we’re going to be solving real problems and also solve it like its done in the real world using standard tools and technologies. Writing our contracts in remix is great since we can easily test and deploy the contracts, but remix has its limitations when it comes to developing production grade applications which is why we’re using a more suitable tool called foundry. I’ll also be using VS Code as my text editor.

Project idea

So what are we building? In this article, we’ll be solving the main issue that’s plagued the kickstarter application. For those that are not familiar with what kickstarter is, it’s a place where people who have ideas for building some products but needs funding goes to and some other people funds those ideas and take shares from the proceedings of the product (In a nutshell 😅). You can find more details on the website here.

I believe we can already perceive the problems that this would potentially bring. People with evil intents can actually just go to the platform claiming to have some product idea, gather fundings and then just take flight with all the money ☹️. Now how can we solve this issue? We know (without a doubt) that the blockchain solves the problem of trust, in the sense that we don’t need to trust a third party before we can make transactions with them or expect them to behave appropriately. Hence the blockchain technology would be a very good way to actually solve the issue of trust that’s plagued the kickstarter application and that’s exactly what this article aims to achieve.

Getting started.

Now that we’ve gotten the administrative stuff out of the way, let’s actually look at how we would go about installing foundry and getting started with writing our smart contracts.
Installing foundry is not overly complicated, the installation process together with the documentation can be found here. Windows users might need to consider using WSL for the installation, but the command to run is

curl -L https://foundry.paradigm.xyz | bash

This would install foundryup, then running foundryup would install forge, cast, anvil and chisel, which are the tools we would use for our entire application development lifecycle.

I’ve installed foundry and ran the foundryup command as below

foundryup command

I’ll be working in an empty directory called kickstarter.

Like I said initially, the foundryup command installed 4 tools that have different purposes when working with foundry. The first one we would be working with is forge. We can run the command below to initialise a bare bones foundry project.

forge init

This creates a few folders and files as seen below in VS Code

forge init command

By default, we have the src, script and test folders each containing a counter related smart contract. Go ahead to delete these 3 files and create a new solidity file in the src folder called kickstarter.sol. We won’t be doing any testing in this article as that would be covered extensively in my next article. The only content in the kickstarter file is just the SPDF configuration, solidity version and our actual contract declaration called Campaign.

By default VSCode does not provide intellisense to our solidity files, for this we’ll have to install an extension from the market place. I prefer the one by Nomic Foundation.

Our Contract Logic

The specifications for our contract would be as follows:

  • A manager who is the one looking for funding would be able to specify the minimum contribution for anyone wanting to fund their idea.
  • Other users can contribute to the idea if they do meet the required amount specified by the manager.
  • Managers won’t get the money paid to them directly, instead it’s stored on the blockchain.
  • Whenever the manager needs some money to fund a specific part of the project, they’ll create a spending request (eg. If the project idea is a car, then the manager might need to buy engines, hence they create spend request for it).
  • The spending request would undergo approval by the funders of the ideas.
  • If most funders approves the spend request, then the amount is sent to the account of whoever needs to receive the payment (eg whoever sells the car engines).
  • A funder that does not partake in the approval process is taken as false. ie they do not approve the spend request.

Note: At no point is the money being sent directly to the manager. This way the funders can also have a clear overview and track how their moneys were spent.

Actual contract code

Now that we’ve understood the clear picture for our contract, let’s get to the actual implementation. Obviously, the first thing to do would be to handle the logic that happens when the contract is created by the manager (emphasis on “created by the manager”).

We created a constructor function which takes in a uint for the minimum amount needed to join the campaign. We created 2 immutable variables called i_manager and i_minimum_contribution, then set the i_manager variable to whoever created the contract and set the i_minimum_contribution to whatever they passed in.
By convention I set the 2 variables as private (this helps to save more gas) and created getter functions for whenever I might need to read the values they stored.

The immutable keyword might be new to us as of now, but it just simply means that the value stored in that variable would not mutated (changed). It is similar to the constant keyword as well except that we have to set the value for a constant variable immediately it is initialised.

From my previous article, I stated that any variable declared in the top level of the contract is a storage variable, but this is not the case for immutable and constant variables.

immutable and constant variables are stored in the bytecode of the contract itself and not on the blockchain. This way, they greatly reduce gas spent from reading their values.

Alright, moving on we’ll need a way for other people to join the campaign and make their different contributions.

We created 2 new storage variables s_funders and s_fundersCount to keep track of the funders of the campaign with their contribution amount. The function is marked as payable. This means that it can accept payment (in this case ethers) and we access the value using the msg.value global variable.

Next we’ll work on the create spend request function.

We have quite some things going on here. First we created a struct for our request type, then created a function for a manager to create the spend request, we require that only the manager can create requests. The recipient passed as the function parameter in the createSpendRequest function is set to payable since we intend to send money to this address if the spend request is approved by the funders.

Notice how the spendRequest variable is set to storage? This indicates that we are trying to mutate the actual value at the specified index stored in the s_spendRequests variable.

In our approve function, we pass in the index of the spend request as a parameter and get the request at that index storing it in a storage variable (again, indicating that it would mutate the value at that index). We then require that the user calling the approval function has actually contributed a valid amount to the campaign and also that they had not voted initially for the spend request.
We then increase the approvalCount variable and set the address of the funder as true to show they have voted.

Since we need a way to track the funders that has approved a spend request, we modified our request struct to include an approvals mapping as below.

The final thing to do would be to finalise the spend request and this would be done only by the manager depending on whether or not the request has been approved by the funders of course.

Here we also require that only the manager can call this function (a little modification to this soon), get the spend request, then require that more than half of the funders of the campaign has actually voted and also require that the spend request has not been completed initially.

Once all the checks above passes, we then transfer the money (ethers in this case) to the recipient and complete the spend request.

Now the modification we can make is to remove the require(msg.sender == i_manager) and put it inside a modifier which we can then use wherever it’s needed. This helps us to reduce repetition since we are doing exactly the same thing in the createRequest and the finalizeRequest function.

We create our modifier as above and then use them in the createRequest and finalizeRequest like this:

Great, now our campaign contract has all the requirements we need. One thing to note though is that the entire contract we’ve written so far only applies to a single campaign. So in essence, anytime someone has an idea that needs funding and decides to use our application, we’ll have to manually deploy another campaign.

Importing files

So far we’ve been working with just one solidity file that contains all of our codes, but usually, we’ll be needing to import some other contract into our application and we can do that by using the import keyword in solidity.

To solve the manual deployment issue stated earlier, one way to go about it is to create another contract that would be responsible for deploying our campaign contract (Yes, a contract can deploy another contract 😅). So we’ll just deploy that single contract and anytime someone wants to use our application, this contract deploys a campaign contract for them.

Inside our src folder, lets create a Deploy.sol file with the following content:

A contract that creates other contract is called a factory. Hence our DeployCampaign contract above is a factory.

This contract is fairly simple, as we only have 2 functions. The first creates the campaign and passes the minimum value specified, we cast the campaign with the address keyword and this returns the address of where the contract was deployed. We then save this in a storage variable for the array of addresses of all the deployed campaigns.
The second function simply just returns all the deployed contracts.

Notice the syntax used to import the campaign from the Kickstarter.sol file since they are in the same directory.

Now we have an issue. In the campaign contract, we are storing the address of whoever creates the contract in the i_manager variable. The issue with our current setup is that now, the DeployCampaign contract is actually what is creating the campaign contract so the address stored in the i_manager variable won’t be the address of the manager but instead it’ll be the address of the DeployCampaign contract. Whew 😮‍💨.
When we get to testing, we’ll see more about this.

Let’s modify our campaign contract so that it now accepts not just the minimum contribution, but the address of who creates the contract as well.
The updated campaign constructor looks like this:

while the DeployCampaign contract now looks like this:

Now we’re all set. Complete code for the kickstarter and deploy files are now:

// SPDX-License-Identifier: MIT

pragma solidity ^0.8.21;

contract Campaign {
address private immutable i_manager;
uint private immutable i_minimum_contribution;
mapping(address funder => uint amount) private s_funders;
uint private s_fundersCount;

struct Request {
string description;
uint value;
address payable recipient;
bool complete;
uint approvalCount;
mapping(address funder => bool approval) approvals;
}

uint private s_numRequests;
mapping(uint index => Request spendRequest) private s_spendRequests;

modifier require_owner() {
require(msg.sender == i_manager);
_;
}

constructor(uint minimum, address manager) {
i_manager = manager;
i_minimum_contribution = minimum;
}

function contribute() public payable {
require(msg.value >= i_minimum_contribution);
s_funders[msg.sender] += msg.value;
s_fundersCount++;
}

function createSpendRequest(
string memory description,
uint value,
address payable recipient
) public require_owner {
Request storage spendRequest = s_spendRequests[s_numRequests++];
spendRequest.description = description;
spendRequest.value = value;
spendRequest.recipient = recipient;
spendRequest.complete = false;
spendRequest.approvalCount = 0;
}

function approveRequest(uint index) public {
Request storage request = s_spendRequests[index];

require(s_funders[msg.sender] > 0);
require(!request.approvals[msg.sender]);

request.approvalCount++;
request.approvals[msg.sender] = true;
}

function finalizeRequest(uint index) public require_owner {
Request storage request = s_spendRequests[index];

require(request.approvalCount > (s_fundersCount / 2));
require(!request.complete);

request.recipient.transfer(request.value);
request.complete = true;
}

function getManager() public view returns (address) {
return i_manager;
}

function getMinimumContribution() public view returns (uint) {
return i_minimum_contribution;
}

function getfundersCount() public view returns (uint) {
return s_fundersCount;
}
}
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.21;

import {Campaign} from "./Kickstarter.sol";

contract DeployCampaign {
address[] private s_deployedCampaigns;

function createCampaign(uint minimum) public {
address campaign = address(new Campaign(minimum, msg.sender));
s_deployedCampaigns.push(campaign);
}

function getDeployedCampaigns() public view returns (address[] memory) {
return s_deployedCampaigns;
}
}

Compiling and Deploying our contracts

Deploying our contract locally is very easy once we have foundry setup.
We’ve briefly seen how to run a command with forge, and there are still a few others, but for now we are going to spin up a local blockchain. We can do that by just running the anvil command like so

anvil

This should print out the following on your terminal

By default (and similar to remix) anvil has some accounts created for us to use and these accounts has all been pre-funded, in this case 10,000 ethers each. We also see our private keys and our account Mnemonic.

Note: The private key at index 0 corresponds to the account at index 0 and so on.

We also see that our local blockchain network is running on port 8545
To compile and deploy our contract using the private key at index 0, the command to run is

 forge create DeployCampaign --rpc-url http://localhost:8545 --private-key 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80

The rpc-url specifies where we want to deploy our contract to, in this case it’s our locally running anvil chain and the private-key specifies the account (at index 0) that is actually making the deployment. We should get the output as below:

Notice how the deployer corresponds to the account at index 0

Ideally, the way we’ll want to deploy our contracts using foundry would be to write deployment scripts, but we’ll talk more about that in the next article.

Alright then, we’ve definitely covered quite a lot in this article and got our hands dirty with working with foundry and the tools it provides, but there are still more that needs to be covered in terms of deployment and testing, all of which would be covered in the next article.

If you enjoy this article please give a clap, share, comment and follow. I’ll see you in the next. 🚀🤩

You can find the next article here ⬇️

--

--

Ifeoluwaolubo
Coinmonks

A software developer with love for the blockchain