The Freelancer's Smart Contract
This is the 3rd part of a 4-part series that document my process of building a Decentralized Application (DApp) for freelancers to receive multiple partial payments for a project that he undertakes with a client.
For a step-by-step guide on the business logic of this process, refer to part 1 of this series. For a demonstration of how the Decentralized App works, refer to part 2. The source codes for the project can be found in the project's Github repository.
In the third part of this series, I will walk through the Solidity codes behind the Freelancer Smart Contract.
States
enum ScheduleState {planned, funded, started, approved, released}
enum ProjectState {initiated, accepted, closed}
There are 2 state enum
- one for schedules and another for the project. Every schedule in the smart contract (e.g. Design Phase, Development Phase, Implementation Phase) has 5 states, namely - planned
, funded
, started
, approved
, and released
.
The project has 3 states, namely initiated
, accepted
, and closed
.
Structure & Global Variables
struct schedule{
string shortCode;
string description;
uint256 value;
ScheduleState scheduleState;
}
int256 public totalSchedules = 0;
address payable public freelancerAddress;
address public clientAddress;
ProjectState public projectState;
mapping(int256 => schedule) public scheduleRegister;
The schedule structure contains the following variables, namely:
shortCode
: e.g. "DSG" for Design Phasedescription
: e.g. "Design Phase"value
: 1e.g. ETHscheduleState
: one of these states that this schedule is currently in, i.e.planned
,funded
,started
,approved
, andreleased
The variable totalSchedule
keeps track of the total number of schedules that this project has. The variable freelancerAddress
stores the wallet address of the freelancer. The variable clientAddress
stores the wallet address of the client. The variable projectState
stores the current state of the project.
A map is used to store all the schedules for the project.
Modifiers
modifier condition(bool _condition) {
require(_condition);
_;
}
modifier onlyFreelancer() {
require(msg.sender == freelancerAddress);
_;
}
modifier onlyClient() {
require(msg.sender == clientAddress);
_;
}
modifier bothClientFreelancer(){
require(msg.sender == clientAddress || msg.sender == freelancerAddress);
_;
}
These are used in the Smart Contract's functions to validate if the client and/or the freelancer are allowed to call them. Some functions, such as releaseFunds()
(which allows funds to be released at the end of a schedule) can only be called by the freelancer. Other functions, such as approveTask()
(which approves the addition of a new task to the schedule) can only be called by the client. There are also functions such as endProject()
which can be called by both the client and/or the freelancer.
modifier inProjectState(ProjectState _state) {
require(projectState == _state);
_;
}
The inProjectState()
modifier checks if the project is currently in a specific state (e.g. closed). Certain functions can only be executed when the project is in a certain state. For example, new schedules can only be added when the project is in its initiated state, and not when the project has already been closed.
modifier inScheduleState(int256 _scheduleID, ScheduleState _state){
require((_scheduleID <= totalSchedules - 1) && scheduleRegister[_scheduleID].scheduleState == _state);
_;
}
The inScheduleState()
modifier checks if the schedule in question is in a specific state (e.g. funded). This allows the functions to test if a schedule is ready to move on to the next state - for example, a schedule can only move on to the funded
state if it is currently in the planned
state.
modifier ampleFunding(int256 _scheduleID, uint256 _funding){
require(scheduleRegister[_scheduleID].value == _funding);
_;
}
modifier noMoreFunds(){
require(address(this).balance == 0);
_;
}
The ampleFunding()
modifier checks if a schedule's funding is equivalent to the funding that it is supposed to receive. For example, "Design Phase" cost 1 ETH. This modifier checks to ensure that the client has truly funded 1 ETH into this schedule.
The noMoreFunds()
modifier checks if the Smart Contract still holds custody of any ETH.
Functions
constructor()
{
freelancerAddress = payable(msg.sender);
projectState = ProjectState.initiated;
}
The constructor()
function saves the wallet address of the person who launched this smart contract in the freelancerAddress
variable and sets the projectState
to initiated.
function addSchedule(string memory _shortCode, string memory _description, uint256 _value)
public
inProjectState(ProjectState.initiated)
onlyFreelancer
{
schedule memory s;
s.shortCode = _shortCode;
s.description = _description;
s.scheduleState = ScheduleState.planned;
s.value = _value;
scheduleRegister[totalSchedules] = s;
totalSchedules++;
emit scheduleAdded(_shortCode);
}
addSchedule()
can only be called when the project is in the initiated state. It can only be executed by the person who initiated this smart contract (onlyFreelancer
). This function initializes a new schedule and saves the shortCode
, description
and value
of this schedule into the scheduleRegister
mapping. It increments the totalSchedule
variable to keep track of the total number of schedules in this project.
function acceptProject()
public
inProjectState(ProjectState.initiated)
{
clientAddress = msg.sender;
projectState = ProjectState.accepted;
emit projectAccepted(msg.sender);
}
When acceptProject()
is called, it saves the wallet address of the person who executes it into the clientAddress
variable. It then sets the projectState
to accepted
. The person who calls acceptProject()
becomes the client of this project.
function fundTask(int256 _scheduleID)
public
payable
inProjectState(ProjectState.accepted)
inScheduleState(_scheduleID, ScheduleState.planned)
ampleFunding(_scheduleID, msg.value)
onlyClient
{
scheduleRegister[_scheduleID].scheduleState = ScheduleState.funded;
emit taskFunded(_scheduleID);
}
When the client is ready to allow the freelancer to begin working on a particular schedule (e.g. the design phase), he funds the task by executing fundTask()
. fundTask()
is callable only if it meets the following criteria:
payable
- ETH is deposited when fundTask() is calledinProjectState
: accepted - The project must be accepted by the client.inScheduleState
: planned - The schedule to be funded is in a planned stateampleFunding
- The ETH to be deposited into the smart contract is equal to the value of the schedule to be funded (i.e. if the design phase cost 1 ETH, then 1 ETH must be deposited)onlyClient
- this function is to be executed by the client only
When executed, the state of this schedule changes from planned
to funded
.
function startTask(int256 _scheduleID)
public
inProjectState(ProjectState.accepted)
inScheduleState(_scheduleID, ScheduleState.funded)
onlyFreelancer
{
scheduleRegister[_scheduleID].scheduleState = ScheduleState.started;
emit taskStarted(_scheduleID);
}
The startTask()
function is executed by the freelancer to indicate that he has started working on a particular task. startTask()
is callable only if it meets the following criteria:
inProjectState
: accepted - the client must have accepted this projectinScheduleState
: funded - the client must have funded this scheduleonlyFreelancer
: only the freelancer can call this function.
When executed, the state of this schedule changes from funded
to started
.
function approveTask(int256 _scheduleID)
public
inProjectState(ProjectState.accepted)
inScheduleState(_scheduleID, ScheduleState.started)
onlyClient
{
scheduleRegister[_scheduleID].scheduleState = ScheduleState.approved;
emit taskApproved(_scheduleID);
}
The approveTask()
function is executed by the client to indicate that he has seen and approved a task. approveTask()
is callable only if it meets the following criteria:
inProjectState
: accepted - the client must have accepted this projectinScheduleState
: started - the freelancer must have started work on this taskonlyClient
- only the client can approve a task.
When executed, the state of this schedule changes from started
to approved
.
function releaseFunds(int256 _scheduleID)
public
payable
inProjectState(ProjectState.accepted)
inScheduleState(_scheduleID, ScheduleState.approved)
onlyFreelancer
{
freelancerAddress.transfer(scheduleRegister[_scheduleID].value);
scheduleRegister[_scheduleID].scheduleState = ScheduleState.released;
emit fundsReleased(_scheduleID, scheduleRegister[_scheduleID].value);
}
The releaseFunds()
function is executed by the freelancer to release to his wallet address, the funds that the smart contract has held in custody. This signifies the completion of a task, and thus, a payment to be made to the freelancer. releaseFunds()
is callable only if it meets the following criteria:
inProjectState
: accepted - the client must have accepted this projectinScheduleState
: approved - the client must have accepted the work that the freelancer did for this task.onlyFreelancer
: only the freelancer can call this function to release funds to himself
When executed, releaseFunds()
will transfer ETH that the smart contract holds in custody for this task to the wallet address of the freelancer. It also changes the state of the schedule from approved
to released
.
function endProject()
public
bothClientFreelancer
noMoreFunds
{
projectState = ProjectState.closed;
emit projectEnded();
}
The endProject()
function marks the completion of a project. This function is callable only if the following criteria are met:
noMoreFunds
: The project no longer holds any ETH in custody. All ETH have been released to the freelancer.
When executed, endProject()
changes the state of the project to closed.
function getBalance()
public
view
returns (uint256 balance)
{
return address(this).balance;
}
getBalance()
returns the total balance held by the smart contract to the caller. Anyone who knows the address of the smart contract can call getBalance()
.
What's Next?
The source codes for this project can be found in my Github repository.
In the final part of this tutorial, I will explain the Javascript-based codes for the Freelancer Decentralized App. Stay tuned!
- The Freelancer's Smart Contract: How It Works
- The Freelancer's Smart Contract: DApp Demo
- The Freelancer's Smart Contract: Solidity Codes Explained (this part)
- The Freelancer's Smart Contract: DApp Codes Explained
If you enjoyed this tutorial, perhaps you may also wish to read:
- NFT-Based Luxury Watch Certificate: An Implementation of an NFT-based luxury watch certification system
- Introducing the Ethereum Development Environment: A step-by-step guide to setting up a development environment for building Decentralized Apps in Ethereum.
- Freelancer Smart Contract: A payment system between a freelancer and his client to ensure both delivery and payment.
- Ropsten Ethereum Faucet: I built an Ethereum faucet to give out ETH on the Ropsten network.
- Voting on a Blockchain: An implementation of a Voting DApp on Ethereum.
- Smart Contract Explained by Demonstration: A demo of an Escrow Service Smart Contract DApp - in my opinion, the fastest way to explain to a layman what Blockchain is.