Introduction
Startups are constantly looking for funding opportunities in today’s rapidly changing business landscape to fuel their innovative ideas and drive growth. Simultaneously, investors are looking for promising ventures with attractive returns. Traditional funding models, on the other hand, frequently involve intermediaries, complex processes, and limited accessibility, impeding the speed and efficiency of investment transactions. P2PStartupInvestment is a brilliant smart contract built on the Celo blockchain that is revolutionizing peer-to-peer startup investments by improving transparency, security, and accessibility.
Welcome to our advanced technical tutorial, in which we will examine the inner workings of the P2PStartupInvestment smart contract. We will look at its robust features throughout this guide, such as investment creation, seamless funding, secure repayment, and comprehensive information retrieval.
By the end of this tutorial, you will have a thorough understanding of how to create your own decentralized peer-to-peer startup investment platform based on the powerful P2PStartupInvestment smart contract.
This tutorial is your gateway to unlocking the potential of P2PStartupInvestment and transforming the startup funding landscape, whether you are an aspiring blockchain developer, a visionary startup founder looking for decentralized funding solutions, or an astute investor looking to explore the limitless possibilities of peer-to-peer investments.
Let’s dive in together and realize the full potential of P2PStartupInvestment.
Prerequisite
It is recommended that you have a basic understanding of the following concepts in order to fully grasp and benefit from this advanced technical tutorial on P2PStartupInvestment:
-
Fundamentals of Blockchain: A basic understanding of blockchain technology concepts such as distributed ledgers, transactions, and smart contracts.
-
Web3.js or Similar Libraries: Working knowledge of Web3.js or similar JavaScript libraries used to programmatically interact with the Ethereum blockchain. Connecting to the Ethereum network, querying data, and sending transactions are all part of this.
-
Smart Contract Development: Understand the principles of smart contract development, such as contract structure, data types, functions, events, and modifiers.
-
Development Environment: Knowledge of how to set up a Solidity programming development environment, including compilers, integrated development environments (IDEs), and testing frameworks.
-
Understanding of Investment Concepts: A basic understanding of investment concepts such as equity, funding, repayment, and valuation will help you better understand the functionality of the P2PStartupInvestment smart contract.
With a solid foundation in these prerequisites, you will be well-equipped to follow along with the tutorial and effectively implement the P2PStartupInvestment smart contract. If you feel the need to brush up on any of these topics, we recommend looking into relevant online resources and documentation.
Requirements
Set Up
If you are using Celo Composer, Open up your terminal, type this command, and follow the process as seen in the image below
npx @celo/celo-composer@latest create
Now install the necessary dependencies you want using yarn or npm install
. If you are using remix, just activate the Celo plugin. You can git clone the code repo and start your integration here
Smart Contract
The complete smart contract should look like this
// SPDX-License-Identifier: MIT
pragma solidity >=0.5.0 <0.9.0;
contract P2PStartupInvestment {
// The minimum and maximum amount of ETH that can be invested
uint public constant MIN_INVESTMENT_AMOUNT = 0.1 ether;
uint public constant MAX_INVESTMENT_AMOUNT = 10 ether;
// The minimum and maximum equity percentage that can be offered for an investment
uint public constant MIN_EQUITY_PERCENTAGE = 1;
uint public constant MAX_EQUITY_PERCENTAGE = 10;
struct Investment {
uint amount;
uint equityPercentage;
uint fundingDeadline;
string startupName;
string description;
uint valuation;
address payable investor;
address payable startup;
bool active;
bool repaid;
}
mapping(uint => Investment) public investments;
uint public investmentCount;
event InvestmentCreated(
uint investmentId,
uint amount,
uint equityPercentage,
uint fundingDeadline,
string startupName,
string description,
uint valuation,
address investor,
address startup
);
event InvestmentFunded(uint investmentId, address funder, uint amount);
event InvestmentRepaid(uint investmentId, uint amount);
modifier onlyActiveInvestment(uint _investmentId) {
require(investments[_investmentId].active, "Investment is not active");
_;
}
modifier onlyInvestor(uint _investmentId) {
require(
msg.sender == investments[_investmentId].investor,
"Only the investor can perform this action"
);
_;
}
function createInvestment(
uint _amount,
uint _equityPercentage,
string memory _startupName,
string memory _description,
uint _valuation
) external payable {
require(
_amount >= MIN_INVESTMENT_AMOUNT &&
_amount <= MAX_INVESTMENT_AMOUNT,
"Investment amount must be between MIN_INVESTMENT_AMOUNT and MAX_INVESTMENT_AMOUNT"
);
require(
_equityPercentage >= MIN_EQUITY_PERCENTAGE &&
_equityPercentage <= MAX_EQUITY_PERCENTAGE,
"Equity percentage must be between MIN_EQUITY_PERCENTAGE and MAX_EQUITY_PERCENTAGE"
);
require(bytes(_startupName).length > 0, "Startup name cannot be empty");
require(bytes(_description).length > 0, "Description cannot be empty");
require(_valuation > 0, "Startup valuation must be greater than 0");
uint _fundingDeadline = block.timestamp + (1 days);
uint investmentId = investmentCount++;
Investment storage investment = investments[investmentId];
investment.amount = _amount;
investment.equityPercentage = _equityPercentage;
investment.fundingDeadline = _fundingDeadline;
investment.startupName = _startupName;
investment.description = _description;
investment.valuation = _valuation;
investment.investor = payable(msg.sender);
investment.startup = payable(address(0));
investment.active = true;
investment.repaid = false;
emit InvestmentCreated(
investmentId,
_amount,
_equityPercentage,
_fundingDeadline,
_startupName,
_description,
_valuation,
msg.sender,
address(0)
);
}
function fundInvestment(
uint _investmentId
) external payable onlyActiveInvestment(_investmentId) {
Investment storage investment = investments[_investmentId];
require(
msg.sender != investment.investor,
"Investor cannot fund their own investment"
);
require(investment.amount == msg.value, "Incorrect investment amount");
require(
block.timestamp <= investment.fundingDeadline,
"Investment funding deadline has passed"
);
payable(address(this)).transfer(msg.value);
investment.startup = payable(msg.sender);
investment.active = false;
emit InvestmentFunded(_investmentId, msg.sender, msg.value);
}
function repayInvestment(
uint _investmentId
)
external
payable
onlyActiveInvestment(_investmentId)
onlyInvestor(_investmentId)
{
Investment storage investment = investments[_investmentId];
require(msg.value == investment.amount, "Incorrect repayment amount");
investment.startup.transfer(msg.value);
investment.repaid = true;
investment.active = false;
emit InvestmentRepaid(_investmentId, msg.value);
}
function getInvestmentInfo(
uint _investmentId
)
external
view
returns (
uint amount,
uint equityPercentage,
uint fundingDeadline,
string memory startupName,
string memory description,
uint valuation,
address investor,
address startup,
bool active,
bool repaid
)
{
Investment storage investment = investments[_investmentId];
return (
investment.amount,
investment.equityPercentage,
investment.fundingDeadline,
investment.startupName,
investment.description,
investment.valuation,
investment.investor,
investment.startup,
investment.active,
investment.repaid
);
}
function withdrawFunds(
uint _investmentId
) external onlyInvestor(_investmentId) {
Investment storage investment = investments[_investmentId];
require(!investment.active);
payable(msg.sender).transfer(investment.amount);
}
}
Breakdown
// SPDX-License-Identifier: MIT
pragma solidity >=0.5.0 <0.9.0;
- First, we declared the solidity version.
-
MIN_INVESTMENT_AMOUNT: The minimum amount of Ether (ETH) that can be invested in a startup is represented by this constant. The value is set to 0.1 ether to ensure that investments below this amount are not accepted.
-
MAX_INVESTMENT_AMOUNT: The maximum amount of ETH that can be invested in a startup is represented by this constant. The value is set at 10 ether, imposing a cap on investment amounts to avoid overfunding.
-
MIN_EQUITY_PERCENTAGE: The minimum equity percentage that can be offered to an investor in exchange for their investment is defined by this constant. The value is set to one, ensuring that the investor receives at least a one-third ownership stake in the startup.
-
MAX_EQUITY_PERCENTAGE: The maximum equity percentage that can be offered to an investor is defined by this constant. The value is set to 10, imposing a maximum ownership stake that the investor can receive.
Within the P2PStartupInvestment contract, these constants serve as predefined boundaries and constraints. They ensure that investments and equity percentages remain within acceptable ranges, preserving fairness and avoiding abuse or extreme values.
This struct represents the data structure used to store information about each investment made through the contract. Let’s explore the different fields of the Investment struct:
-
amount: The amount of Ether invested in the startup is stored in this field. It has the uint type and represents an unsigned integer.
-
equityPercentage: The equity percentage offered to the investor in exchange for their investment is stored in this field. It is also of the uint type.
-
fundingDeadline: This field represents the deadline by which the investment funding must be completed. It is an uint value that represents a particular timestamp.
-
startupName: The name of the startup associated with the investment is stored in this field. It is of the string type.
-
This field contains a description of the startup or investment, as well as any additional information. It is also of the string type.
-
valuation: This field contains the startup’s valuation. It is of type uint and represents the startup’s estimated worth or value.
-
investor: This is a payable address that stores the Ethereum address of the investor who is making the investment.
-
startup: This is an address payable field that stores the Ethereum address of the startup receiving the investment.
-
This boolean field indicates whether the investment is active at the moment. If active is true, the investment is still active; otherwise, if active is false, the investment has ended.
-
repaid: This boolean field indicates whether or not the investment was repaid. If repaid is true, it means that the startup has repaid the amount invested; otherwise, repaid is false.
The Investment struct acts as a container for all relevant information pertaining to a specific investment made through the P2PStartupInvestment contract. Each instance of this struct represents a distinct investment and aids in the effective organization and management of investment data.
-
investments: This is a data structure that maps an uint value (investment ID) to an Investment struct. Based on the unique investment ID, it enables efficient storage and retrieval of investment details. This mapping is now accessible to other contracts and external entities thanks to the public visibility modifier.
-
investmentCount: This is a public uint variable that keeps track of the total number of contract investments. It is incremented whenever a new investment is created, making it easy to assign unique investment IDs and keep track of the total investment count.
The smart contract can effectively store and manage multiple investments made by different investors by utilizing the investments mapping and investmentCount variable.
The mapping facilitates the lookup and retrieval of investment details based on their respective investment IDs, while the investmentCount variable provides information about the total number of investments recorded by the contract.
Solidity events emit data that can be captured and logged off-chain, allowing external systems or user interfaces to track and react to specific occurrences within the smart contract. Let’s take a closer look at each event:
- InvestmentCreated:
-
Parameters:
-
investmentId: The unique identifier of the investment.
-
amount: The amount of Ether invested in the startup.
-
equityPercentage: The equity percentage offered to the investor.
-
fundingDeadline: The deadline for funding the investment.
-
startupName: The name of the startup associated with the investment.
-
description: Additional description or information about the startup or investment.
-
valuation: The valuation of the startup.
-
investor: The Ethereum address of the investor.
-
startup: The Ethereum address of the startup.
-
Purpose: When a new investment is created within the contract, this event is emitted. It includes the investment ID, investment amount, equity percentage, funding deadline, startup name, description, valuation, investor address, and startup address. This event enables external systems or user interfaces to track and record investment creation.
- InvestmentFunded:
-
Parameters:
-
investmentId: The identifier of the investment that has been funded.
-
funder: The Ethereum address of the entity funding the investment.
-
amount: The amount of Ether funded for the investment.
-
Purpose:This event is triggered when an investment receives funding from a third party. It indicates that a funder (investor or external party) successfully funded a specific investment identified by investmentId with a specific amount of Ether. This event allows for the monitoring and notification of investment funding activities.
- InvestmentRepaid:
-
Parameters:
-
investmentId: The identifier of the investment that has been repaid.
-
amount: The amount of Ether repaid for the investment.
-
Purpose: When a startup repays an investment, this event is emitted. It denotes that a specific investment identified by investmentId has been fully or partially repaid in Ether. This event makes it easier to monitor and record investment repayment activities.
External systems or user interfaces can observe and react to investment creation, funding, and repayment events by emitting these events at critical points within the smart contract, increasing transparency and providing valuable information about the investment lifecycle.
Modifiers are used in Solidity to add preconditions or conditions to functions, allowing for code reuse and enforcing specific requirements before the function body is executed. Let’s take a closer look at each modifier:
- onlyActiveInvestment:
-
Parameters:
-
_investmentId: The identifier of the investment.
-
Purpose: This modifier only allows a function to be executed when the specified investment is active. It looks in the investments mapping for the active field of the investment with the given _investmentId. If the investment is not active (i.e., active is false), the require statement throws an exception with the error message provided, preventing the function from running. If the investment is not active, the function is executed. This modifier ensures that certain actions are only available on active investments.
- onlyInvestor:
-
Parameters:
-
_investmentId: The identifier of the investment.
-
Purpose: This modifier allows only the investor associated with the specified investment to execute a function. It checks to see if the msg.sender, the caller’s Ethereum address, matches the investor field of the investment with the given _investmentId in the investments mapping. If the caller is not the investor, the require statement raises an exception with the error message provided, preventing the function from being executed. If the caller is an investor, the function body is executed. This modifier ensures that certain actions related to the investor’s specific investment are only performed by the investor.
By applying these modifiers to smart contract functions, specific conditions and permissions are enforced, providing security and access control over critical operations.
- Function Signature:
-
Parameters:
-
_amount: The amount of Ether to be invested in the startup.
-
_equityPercentage: The equity percentage offered for the investment.
-
_startupName: The name of the startup associated with the investment.
-
_description: Additional description or information about the startup or investment.
-
_valuation: The valuation of the startup.
-
Visibility: external
-
Modifiers: None
- Input Validation:
-
The function begins with a series of require statements that validate the function call input parameters.
-
The first require statement ensures that the investment amount _amount is within the permissible range defined by the MIN_INVESTMENT_AMOUNT and MAX_INVESTMENT_AMOUNT variables. If the condition is not met, an exception is thrown with the provided error message.
-
The second require statement ensures that the equity percentage _equityPercentage is within the acceptable limits set by MIN_EQUITY_PERCENTAGE and MAX_EQUITY_PERCENTAGE. An exception is thrown if the condition is not met.
-
The following two require statements verify that the startup name _startupName and description _description are not empty strings. The function throws an exception if either of them is empty.
Finally, the function checks to see if the startup valuation _value is greater than zero. If it’s not, an exception is thrown
- Investment Creation:
-
The function creates a new investment after passing the input validation checks.
-
The funding deadline is calculated by adding 1 day (24 hours) to the current block timestamp using block.timestamp + (1 day).
-
By increasing the investmentCount variable, the investment ID is assigned.
-
In the investments mapping, the investment details, such as amount, equity percentage, funding deadline, startup name, description, valuation, investor address, startup address, and status flags (active and repaid), are stored in the Investment struct associated with the generated investment ID.
-
Finally, the InvestmentCreated event is fired, which contains details about the newly created investment.
The createInvestment function allows users to start a new investment by entering the investment details, and it goes through several validation checks to ensure that the inputs are correct.
.
The createInvestment function creates a new investment with the provided details after validating the input parameters. Let’s take a look at the steps involved:
- Funding Deadline Calculation:
- The funding deadline _fundingDeadline is calculated by adding 1 day (24 hours) to the current block timestamp using the expression block.timestamp + (1 days).
- Investment ID Generation:
- The investmentCount variable is incremented and assigned to investmentId to generate a unique identifier for the new investment.
- Investment Creation:
-
The Investment struct associated with the generated investmentId in the investments mapping is accessed and assigned to the investment variable for modification.
-
The various fields of the investment struct are updated with the provided values:
-
amount: The investment amount _amount.
-
equityPercentage: The equity percentage _equityPercentage.
-
fundingDeadline: The calculated funding deadline _fundingDeadline.
-
startupName: The name of the startup _startupName.
-
description: Additional description or information about the startup or investment _description.
-
valuation: The valuation of the startup _valuation.
-
investor: The Ethereum address of the investor, derived from msg.sender and wrapped in a payable address.
-
startup: Initialized to the zero address (address(0)), indicating that the startup address is not yet assigned.
-
active: Set to true to indicate that the investment is active.
-
repaid: Set to false to indicate that the investment has not been repaid.
-
- Event Emission:
The InvestmentCreated event is emitted with the following details of the newly created investment:
-
investmentId: The unique identifier of the investment.
-
_amount: The investment amount.
-
_equityPercentage: The equity percentage.
-
_fundingDeadline: The funding deadline.
-
_startupName: The name of the startup.
-
_description: Additional description or information about the startup or investment.
-
_valuation: The valuation of the startup.
-
msg.sender: The Ethereum address of the investor.
-
address(0): The zero address is passed as the startup address since it is not yet assigned.
The createInvestment function creates a new investment with the provided details, updates the relevant fields in the Investment struct, and emits an event to notify interested parties of the investment’s creation.
External users can fund a specific investment by providing the investment ID and sending the appropriate amount of Ether using the fundInvestment function. The following is a breakdown of its implementation:
- Function Signature:
-
Parameters:
-
_investmentId: The unique identifier of the investment to be funded.
-
Visibility: external
-
Modifiers: onlyActiveInvestment(_investmentId)
- Input Validation:
-
OnlyActiveInvestment ensures that the investment with the specified ID is active. If not, the function will return an error message.
-
The function verifies that the caller (msg.sender) is not the investor in the investment being funded. If they are identical, the function returns an error message.
-
It checks that the amount of Ether sent with the function call (msg.value) corresponds to the amount required for the investment. An exception is thrown if the amounts do not match.
-
The function also determines whether the current timestamp is earlier than or equal to the investment’s funding deadline. If the deadline has passed, the function throws an error.
- Ether Transfer:
Using the transfer function, the function sends the received Ether (msg.value)
to the contract’s address (address(this))
. This action ensures that the Ether is saved in the contract’s balance and can be used later.
The startup field of the investment is updated with the caller’s address (msg.sender)
, indicating that the startup associated with the investment has received funding.
By setting the active flag to false, the investment is marked as inactive.
- Event Emission:
-
The InvestmentFunded event is emitted, which contains information about the funded investment, such as the investment ID, the funder’s address (msg.sender), and the amount of Ether funded (msg.value).
-
External users can fund an active investment by providing the correct investment ID and the required amount of Ether using the fundInvestment function. When an investment is successfully funded, its status is updated, and an event is triggered to notify interested parties of the funding transaction.
The repayInvestment function allows the investor to return the money they put into an active investment. Let’s break down how this function is implemented:
- Function Signature:
-
Parameters:
-
_investmentId: The unique identifier of the investment to be repaid.
-
Visibility: external
-
Modifiers:
-
onlyActiveInvestment(_investmentId)
-
onlyInvestor(_investmentId)
-
- Input Validation:
-
OnlyActiveInvestment ensures that the investment with the specified ID is active. If not, the function will return an error message.
-
The onlyInvestor modifier confirms that the caller (msg.sender) is the same as the investor of the repaid investment. If they do not match, the function returns with an error message.
-
The function determines whether the amount of Ether sent with the function call (msg.value) corresponds to the original investment amount. The function throws an exception if the amounts do not match.
- Ether Transfer:
-
The function sends the Ether
(msg.value)
received from the investor to the startup associated with the investment. This transfer is carried out by calling the transfer function on the investment’s startup address. -
By setting the repaid flag to true, the investment is marked as repaid.
-
Setting the active flag to false deactivates the investment.
- Event Emission:
The InvestmentRepaid event is fired, and it contains information about the repaid investment, such as the investment ID and the amount of Ether repaid (msg.value)
.
The repayInvestment function allows the investor to completely repay their investment. When the repayment is completed, the investment’s status is updated, and an event is emitted to notify interested parties of the transaction.
External callers can use the getInvestmentInfo function to retrieve information about a specific investment by providing its ID. Let’s take a look at how this function is implemented:
- Function Signature:
-
Parameters:
-
_investmentId: The unique identifier of the investment for which information is requested.
-
Visibility: external
-
Modifiers: None
-
Return Type: Tuple containing various investment details
- Investment Retrieval:
Using the provided _investmentId, the function retrieves investment details from the investments mapping. Memory holds the Investment struct associated with the ID.
- Return Values:
The function returns a tuple containing the following information:
-
amount: The invested amount in Ether.
-
equityPercentage: The equity percentage offered for the investment.
-
fundingDeadline: The deadline until which funding for the investment is allowed.
-
startupName: The name of the startup associated with the investment.
-
description: A description of the investment opportunity.
-
valuation: The valuation of the startup.
-
investor: The address of the investor who made the investment.
-
startup: The address of the startup that received the investment.
-
active: A boolean indicating whether the investment is currently active or not.
-
repaid: A boolean indicating whether the investment has been repaid or not.
The getInvestmentInfo function makes it easy to obtain detailed information about a specific investment. External callers who provide the investment ID will receive a tuple containing all relevant information.
When an investment is no longer active, the withdrawFunds function allows the investor to withdraw their investment amount. Let’s break down how this function is implemented:
- Function Signature:
-
Parameters:
-
_investmentId: The unique identifier of the investment from which the investor wants to withdraw funds.
-
Visibility: external
-
Modifiers: onlyInvestor(_investmentId)
- Investment Retrieval:
Using the provided _investmentId, the function retrieves investment details from the investments mapping. Memory holds the Investment struct associated with the ID.
- Active Investment Check:
The function determines whether or not the investment is active. If the investment is still active, which means it hasn’t been funded or repaid, the function returns an error message.
- Ether Transfer:
The function sends the amount invested (investment.amount) from the smart contract to the address of the investor (msg.sender)
. The transfer function on the investor’s address is used for this transfer.
The withdrawFunds function allows investors to recover their investment amount from a completed or cancelled investment. Before allowing the withdrawal, it ensures that the investment is no longer active.
Deployment
In your deploy.js
file delete everything and add this code:
Then you add your private key to the .env file and deploy by running the following command yarn or npm run deploy
If you are using remix for deployment:
-
To deploy our smart contract successfully, we need the Celo extension wallet, which can be downloaded from here
-
Next, we need to fund our newly created wallet, which can be done using the Celo Alfajores faucet here
-
You can now fund your wallet and deploy your contract using the Celo plugin in Remix.
Conclusion
Finally, the P2PStartupInvestment smart contract provides an innovative solution for blockchain-based peer-to-peer startup investments. This platform, powered by smart contracts, allows individuals to participate in funding promising startup ventures while maintaining transparency, security, and efficiency.
We looked at the key features and functionalities of the P2PStartupInvestment contract throughout this tutorial. We discovered the minimum and maximum investment amounts, as well as the equity percentage ranges permitted for each investment. The “Investment” struct contains critical information about each investment opportunity, such as the funding deadline, startup details, and investor information.
The contract ensures that actions can only be performed by active investments or the respective investors by leveraging modifiers. This protects the platform’s integrity and protects investors from unauthorized operations.
We looked at critical functions like creating investments, funding them, and repaying them when they mature. Furthermore, the contract includes a convenient method for retrieving investment details via the getInvestmentInfo function.
The tutorial also covered the withdrawal process, which allows investors to reclaim their investment amounts after an investment has ceased to be active.
The P2PStartupInvestment smart contract opens up new opportunities for individuals looking to invest in startups and entrepreneurs seeking funding. This contract allows users to participate in the dynamic world of startup financing by combining blockchain technology and transparent investment mechanisms.
You have gained a thorough understanding of the contract’s inner workings and potential applications by following this tutorial. Armed with this information, you are now prepared to dive into the world of peer-to-peer startup investments and explore the numerous opportunities it provides.
Remember, the P2PStartupInvestment contract is just the start of an exciting journey into the worlds of decentralized finance and entrepreneurship. So, seize the opportunity to use blockchain technology to transform the startup investment landscape!