• Time to read 5 minutes
The Freelancer's Smart Contract: Solidity Codes Explained

The Freelancer's Smart Contract: Solidity Codes Explained

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 Phase
  • description: e.g. "Design Phase"
  • value: 1e.g.  ETH
  • scheduleState: one of these states that this schedule is currently in, i.e. planned, funded, started, approved, and released

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 called
  • inProjectState: accepted - The project must be accepted by the client.
  • inScheduleState: planned - The schedule to be funded is in a planned state
  • ampleFunding - 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 project
  • inScheduleState: funded - the client must have funded this schedule
  • onlyFreelancer: 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 project
  • inScheduleState: started - the freelancer must have started work on this task
  • onlyClient - 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 project
  • inScheduleState: 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!

  1. The Freelancer's Smart Contract: How It Works
  2. The Freelancer's Smart Contract: DApp Demo
  3. The Freelancer's Smart Contract: Solidity Codes Explained (this part)
  4. The Freelancer's Smart Contract: DApp Codes Explained

If you enjoyed this tutorial, perhaps you may also wish to read:

Photo by Woody Yan on Unsplash