Contents

[ PizzaCoin the Series #3 ] Detailed Implementation of Staff and Player Contracts

Figure 1. Pizza Hackathon 2018

Figure 1. Pizza Hackathon 2018


Welcome to the 3rd part of PizzaCoin the Series. In the previous article, you have been walked through the workflow design of PizzaCoin voting system. You have seen that the system composes of nine components including PizzaCoin (mother) contract, PizzaCoinStaff contract, PizzaCoinPlayer contract, PizzaCoinTeam contract, PizzaCoinStaffDeployer library, PizzaCoinPlayerDeployer library, PizzaCoinTeamDeployer library, PizzaCoinCodeLib library and PizzaCoinCodeLib2 library.

Although each component is responsible for specific tasks and independent from each other, PizzaCoin mother contract would be a coordinator which collaborates with all other components. In this article we will be describing the more technical details on implementation of PizzaCoinStaff and PizzaCoinPlayer contracts.

Terms used in this article


PizzaCoin – the mother contract of PizzaCoinStaff, PizzaCoinPlayer and PizzaCoinTeam contracts.

PizzaCoinStaff – one of the three PizzaCoin’s children contracts responsible for managing staff-related tasks such as registering staffs, revoking staffs, providing staff information, and managing token balance as well as voting action for the staff.

PizzaCoinPlayer – one of the three PizzaCoin’s children contracts responsible for managing player-related tasks such as registering players, revoking players, providing player information, and managing token balance as well as voting action for a player.

Project Deployer – a user who deploys PizzaCoin contract which is considered as one of staffs.

Source files refered to in this article


The implementation of PizzaCoinStaff contract


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
pragma solidity ^0.4.23;

import "./SafeMath.sol";
import "./BasicStringUtils.sol";
import "./Owned.sol";


// ------------------------------------------------------------------------
// Interface for exporting external functions of PizzaCoinStaff contract
// ------------------------------------------------------------------------
interface IStaffContract {
    function lockRegistration() external;
    function startVoting() external;
    function stopVoting() external;
    function getTotalSupply() external view returns (uint256 _totalSupply);
    function isStaff(address _user) external view returns (bool _bStaff);
    function getStaffName(address _staff) external view returns (string _name);
    function registerStaff(address _staff, string _staffName) external;
    function kickStaff(address _staff) external;
    function getTotalStaffs() external view returns (uint256 _total);
    function getStaffInfoAtIndex(uint256 _staffIndex) 
        external view
        returns (
            bool _endOfList,
            address _staff,
            string _name,
            uint256 _tokenBalance
        );
    function getTotalTeamsVotedByStaff(address _staff) external view returns (uint256 _total);
    function getVotingResultByStaffAtIndex(address _staff, uint256 _votingIndex) 
        external view
        returns (
            bool _endOfList,
            string _team,
            uint256 _voteWeight
        );
    function getTokenBalance(address _staff) external view returns (uint256 _tokenBalance);
    function commitToVote(string _teamName, address _staff, uint256 _votingWeight) external;
}


// ----------------------------------------------------------------------------
// Pizza Coin Staff Contract
// ----------------------------------------------------------------------------
contract PizzaCoinStaff is IStaffContract, Owned {
    /*
    * Owner of the contract is PizzaCoin contract, 
    * not a project deployer who is PizzaCoin owner
    *
    * Let staffs[0] denote a project deployer (i.e., PizzaCoin owner)
    */

    using SafeMath for uint256;
    using BasicStringUtils for string;



    ...
    ...
    ...
}
Code Snippet 1. Excerpt from PizzaCoinStaff contract's source code


PizzaCoinStaff contract consists of two integral parts as shown in the code snippet 1 including IStaffContract interface in the line no. 11 and PizzaCoinStaff contract in the line no. 45. IStaffContract interface contains function prototypes of PizzaCoinStaff that expose to PizzaCoin contract to use (we will discuss about this in the later article). In the line no. 45, PizzaCoinStaff contract implements IStaffContract interface and also inherits from the contract named Owned, which will be explained in a while.

Under PizzaCoinStaff contract, we attached SafeMath library to type uint256 in the line no. 53 and attached BasicStringUtils library to type string in the line no. 54. Refer to the following link to understand more about using-for in Solidity.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
pragma solidity ^0.4.23;


// ----------------------------------------------------------------------------
// Owned Contract
// ----------------------------------------------------------------------------
contract Owned {
    address public owner;


    // ------------------------------------------------------------------------
    // Constructor
    // ------------------------------------------------------------------------
    constructor() public {
        owner = msg.sender;
    }

    // ------------------------------------------------------------------------
    // Guarantee that msg.sender must be a contract owner
    // ------------------------------------------------------------------------
    modifier onlyOwner {
        require(
            msg.sender == owner,
            "This address is not a contract owner."
        );
        _;
    }
}
Code Snippet 2. PizzaCoinStaff contract inherits from Owned contract


The functionality of Owned contract is just simple and straightforward as shown in the code snippet 2. The contract defines a contract creator as its owner once it is created in the line no. 15. Additionally, the contract has a single function modifier named onlyOwner. This modifier verifies whether a transaction is sent from a contract owner in the line no. 23; if a transaction is not sent from a contract owner, onlyOwner modifier will revert the transaction in the line no. 24. Since PizzaCoinStaff contract inherits from Owned contract, it derives onlyOwner modifier automatically.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
pragma solidity ^0.4.23;


/**
 * @title SafeMath
 * @dev Math operations with safety checks that throw on error
 */
library SafeMath {

    /**
    * @dev Multiplies two numbers, throws on overflow.
    */
    function mul(uint256 a, uint256 b) internal pure returns (uint256 c) {
        // Gas optimization: this is cheaper than asserting 'a' not being zero, but the
        // benefit is lost if 'b' is also tested.
        // See: https://github.com/OpenZeppelin/openzeppelin-solidity/pull/522
        if (a == 0) {
            return 0;
        }

        c = a * b;
        assert(c / a == b);
        return c;
    }

    /**
    * @dev Integer division of two numbers, truncating the quotient.
    */
    function div(uint256 a, uint256 b) internal pure returns (uint256) {
        // assert(b > 0); // Solidity automatically throws when dividing by 0
        // uint256 c = a / b;
        // assert(a == b * c + a % b); // There is no case in which this doesn't hold
        return a / b;
    }

    /**
    * @dev Subtracts two numbers, throws on overflow (i.e. if subtrahend is greater than minuend).
    */
    function sub(uint256 a, uint256 b) internal pure returns (uint256) {
        assert(b <= a);
        return a - b;
    }

    /**
    * @dev Adds two numbers, throws on overflow.
    */
    function add(uint256 a, uint256 b) internal pure returns (uint256 c) {
        c = a + b;
        assert(c >= a);
        return c;
    }
}
Code Snippet 3. SafeMath library used by PizzaCoinStaff contract


Code snippet 3 shows the implementation of SafeMath. SafeMath is a well-known library developed by OpenZeppelin. This library provides secure mathematical operations i.e. addition, substraction, division and multiplication operations on Solidity’s data type uint256. We adopted SafeMath in the line no. 53 of the snippet 1 to take care of secure mathematical operations on staff’s token balance. Here is the original source file of SafeMath library.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
pragma solidity ^0.4.23;


// ----------------------------------------------------------------------------
// Basic String Utils Library - just a lazy and expensive comparison
// ----------------------------------------------------------------------------
library BasicStringUtils {

    // ------------------------------------------------------------------------
    // Determine if two strings are equal or not
    // ------------------------------------------------------------------------
    function isEqual(string self, string other) internal pure returns (bool bEqual) {
        return keccak256(abi.encodePacked(self)) == keccak256(abi.encodePacked(other));
    }

    // ------------------------------------------------------------------------
    // Determine if the string is empty or not
    // ------------------------------------------------------------------------
    function isNotEmpty(string self) internal pure returns (bool bEmpty) {
        bytes memory selfInBytes = bytes(self);
        return selfInBytes.length != 0;
    }
}
Code Snippet 4. BasicStringUtils library used by PizzaCoinStaff contract


PizzaCoinStaff contract also employs BasicStringUtils library as shown in the code snippet 4 to attach to type string in the line no. 54 of the snippet 1. This library consists of two functions, that is, isEqual and isNotEmpty.

isEqual function is straightforward. We compare two strings using the Solidity’s built-in hash function named keccak256. More technically, we encode each string using abi.encodePacked function and then pipe the encoded result as an input to keccak256 function. We then compare two hashed results using the statement keccak256(abi.encodePacked(self)) == keccak256(abi.encodePacked(other)) in the line no. 13; if two strings match, the comparison result would be true. The following link explains how ABI encoding and hash functions in Solidity work.

The implementation of isNotEmpty function is just simpler. We directly convert the string to bytes using the statement bytes memory selfInBytes = bytes(self); in the line no. 20 and then compare the length of the contents stored in selfInBytes with 0 using the statement selfInBytes.length != 0 in the line no. 21. If the result is true, the string is not empty.

Alright, let’s come back to PizzaCoinStaff contract. PizzaCoinStaff contract is responsible for managing staff-related tasks such as registering staffs, revoking staffs, providing staff information and managing token balance as well as voting action for the staff. The following code snippet shows data structures and state variables used for storing personal information of each staff.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
struct StaffInfo {
    // This is used to reduce potential gas cost consumption when kicking the staff
    uint256 index;  // A pointing index to a particular staff on the 'staffs' array

    string name;
    bool wasRegistered;    // Check if a specific staff is being registered
    uint256 tokenBalance;  // Amount of tokens left for voting
    string[] teamsVoted;   // A collection of teams voted by this staff
    
    // mapping(team => votingWeight)
    mapping(string => uint256) votesWeight;  // Teams with voting weight approved by this staff
}

address[] private staffs;                          // staffs[0] denotes a project deployer (i.e., PizzaCoin owner)
mapping(address => StaffInfo) private staffsInfo;  // mapping(staff => StaffInfo)
Code Snippet 5. Data structures and state variables for storing staff's personal information


Under PizzaCoinStaff contract, there are two state variables used for collecting staff’s personal information viz. staffs and staffsInfo as defined in the line no’s. 14 and 15 respectively. staffs is an array holding ethereum addresses of all registered staffs. Whereas staffsInfo is a mapping variable which stores personal information of each particular staff in terms of struct StaffInfo as defined in the line no. 1. An address of the staff is used as a mapping key for accessing corresponding personal information stored on staffsInfo mapping.

The struct StaffInfo gathers a bunch of state variables including index, name, wasRegistered, tokenBalance, teamsVoted and votesWeight. index points to an element on the array staffs for a particular staff. name is self-explanatory. wasRegistered is a boolean variable used for indicating a registration status of a particular staff. tokenBalance represents an amount of voting tokens that a particular staff remains for voting. teamsVoted is an array containing names of the teams the staff used to vote to. votesWeight is a mapping variable, using a name of a specific team as a mapping key, containing a voting weight for any particular team that the staff has ever voted to.

1
2
3
4
5
uint256 private voterInitialTokens;
uint256 private totalSupply;

enum State { Registration, RegistrationLocked, Voting, VotingFinished }
State private state = State.Registration;
Code Snippet 6. Variables describing PizzaCoinStaff contract's state


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// ------------------------------------------------------------------------
// Constructor
// ------------------------------------------------------------------------
constructor(uint256 _voterInitialTokens) public {
    require(
        _voterInitialTokens > 0,
        "'_voterInitialTokens' must be larger than 0."
    );

    voterInitialTokens = _voterInitialTokens;
}
Code Snippet 7. PizzaCoinStaff contract's constructor()


Additionally, there is another bunch of state variables used by PizzaCoinStaff contract including voterInitialTokens, totalSupply and state variables as shown in the code snippet 6. voterInitialTokens represents a number of voting tokens (set by a project deployer as shown in the line no. 10 of the snippet 7) that PizzaCoinStaff contract would supply to each registered staff upon a registration process. Meanwhile, totalSupply logs a total number of voting tokens that have ever been supplied to all the registered staffs. state indicates a working context of PizzaCoinStaff contract. As described in the previous article, state comprises of four unidirectional steps inculding Registration, RegistrationLocked, Voting and VotingFinished as defined in the line no. 4 of the snippet 6.

Figure 2. Calling to a state-changing function vs calling to a non state-changing function on PizzaCoinStaff contract

Figure 2. Calling to a state-changing function vs calling to a non state-changing function on PizzaCoinStaff contract


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
// ------------------------------------------------------------------------
// Guarantee that msg.sender must be a contract deployer (i.e., PizzaCoin address)
// ------------------------------------------------------------------------
modifier onlyPizzaCoin {
    // owner == PizzaCoin address
    require(
        msg.sender == owner,
        "This address is not PizzaCoin contract"
    );
    _;
}

// ------------------------------------------------------------------------
// Allow the staff freeze Registration state and transfer the state to RegistrationLocked
// ------------------------------------------------------------------------
function lockRegistration() external onlyRegistrationState onlyPizzaCoin {
    state = State.RegistrationLocked;
}
Code Snippet 8. An example of state-changing function lockRegistration()


As mentioned in the previous article, PizzaCoin contract is considered as a contract coordinator for its children contracts including PizzaCoinStaff. Let’s say the staff wants to invoke any function that makes changes to any state variable of PizzaCoinStaff, the staff has to execute that function by way of calling to the mapped function which is on PizzaCoin contract like steps 1.1 - 1.3 in Figure 2. In other words, the staff cannot execute a state-changing function on PizzaCoinStaff contract directly. To enforce this rule, we defined a function modifier named onlyPizzaCoin as defined in the line no. 4 of the code snippet 8. In fact, onlyPizzaCoin modifier is applied to every state-changing function. For instance, applying onlyPizzaCoin modifier to the state-changing function lockRegistration in the line no. 16. Consequently, lockRegistration function has to be executed through PizzaCoin contract only.

1
2
3
4
5
6
// ------------------------------------------------------------------------
// Get a total number of staffs
// ------------------------------------------------------------------------
function getTotalStaffs() external view returns (uint256 _total) {
    return staffs.length;
}
Code Snippet 9. An example of non state-changing function getTotalStaffs()


Since we expected to reduce as much gas consumption as possible when deploying PizzaCoin contract, non state-changing functions of PizzaCoinStaff contract would not be mapped with any functions on PizzaCoin contract. For example, getTotalStaffs as shown in the code snippet 9 which is a non state-changing function (defined using view keyword in the line no. 4) of PizzaCoinStaff contract would not be applied with onlyPizzaCoin modifier. Hence, any user is allowed to make a direct call to this function right away like a step 2 in Figure 2.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// ------------------------------------------------------------------------
// Register the new staff
// ------------------------------------------------------------------------
function registerStaff(address _staff, string _staffName) external onlyRegistrationState onlyPizzaCoin {
    require(
        _staff != address(0),
        "'_staff' contains an invalid address."
    );

    require(
        _staffName.isNotEmpty(),
        "'_staffName' might not be empty."
    );

    require(
        staffsInfo[_staff].wasRegistered == false,
        "The specified staff was registered already."
    );

    // Register the new staff
    staffs.push(_staff);
    staffsInfo[_staff] = StaffInfo({
        wasRegistered: true,
        name: _staffName,
        tokenBalance: voterInitialTokens,
        teamsVoted: new string[](0),
        /*
            Omit 'votesWeight'
        */
        index: staffs.length - 1
    });

    totalSupply = totalSupply.add(voterInitialTokens);
}
Code Snippet 10. The implementation of registerStaff()


The implementation of registerStaff function is shown in the code snippet 10. registerStaff function is applied with two modifiers onlyRegistrationState and onlyPizzaCoin in the line no. 4. As a result, this function is executable if and only if the contract is in Registration state and the function has to be called via the mapped function on PizzaCoin contract only.

In the line no’s. 15 - 18, registerStaff function verifies whether an input address is already registered. If the specified address is already registered, a transaction would be reverted. Otherwise, the specified address would be registered as an address of the new staff. Specifically, the specified address is pushed into staffs array in the line no. 21. Personal record of the new staff in the line no’s. 23 - 30 would then be initialized and recorded into staffsInfo mapping. The specified address is used as a mapping key for accessing personal information of that particular staff in the line no. 22.

By nature, mapping data type can be seen as Hash table which is virtually initialized such that any possible key exists and each key is mapped to a value whose byte representations are all set to zeros of that value type. In case of wasRegistered which is a boolean variable, thus this variable in every possible mapping key would be automatically set to false (boolean’s default type) once staffsInfo mapping gets initialized during PizzaCoinStaff contract is being constructed. With such characteristic, wasRegistered can be employed to detect if any specified address is already registered or not as used in the line no. 16. If wasRegistered of a specific address indicates true, that means that the address is registered already. This solution enables PizzaCoinStaff contract a practical approach to verifying the registration status of any given address without the need to iterate over staffs array in order to search for being of that specific address, which consumes unreliable gas depending on a number of staffs being registered.

In the line no. 25, tokenBalance is initialized with the initial voting tokens predetermined by a project deployer. In other words, every registered staff would get equal voting tokens. In the line no. 30, index represents a pointer to a corresponding staff address stored on staffs array. The use of index will be discussed again on the implementation of kickStaff function below. In the line no. 33, totalSupply would be increased by a number of tokens supplied to the new staff (i.e., the value indicated by voterInitialTokens). SafeMath library is adopted to take care of a secure addition operation on totalSupply.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
// ------------------------------------------------------------------------
// Remove a specific staff
// ------------------------------------------------------------------------
function kickStaff(address _staff) external onlyRegistrationState onlyPizzaCoin {
    require(
        _staff != address(0),
        "'_staff' contains an invalid address."
    );

    require(
        staffsInfo[_staff].wasRegistered,
        "Cannot find the specified staff."
    );

    require(
        isProjectDeployer(_staff) == false,
        "Project deployer is not kickable."
    );

    uint256 staffIndex = getStaffIndex(_staff);

    // Remove the specified staff from an array by moving 
    // the last array element to the element pointed by staffIndex
    staffs[staffIndex] = staffs[staffs.length - 1];

    // Since we have just moved the last array element to 
    // the element pointed by staffIndex, we have to update 
    // the newly moved staff's index to staffIndex too
    staffsInfo[staffs[staffIndex]].index = staffIndex;

    // Remove the last element
    staffs.length--;

    // Remove the specified staff from a mapping
    delete staffsInfo[_staff];

    totalSupply = totalSupply.sub(voterInitialTokens);
}

// ------------------------------------------------------------------------
// Get an index pointing to the specified staff on the array 'staffs'
// ------------------------------------------------------------------------
function getStaffIndex(address _staff) internal view returns (uint256 _staffIndex) {
    assert(_staff != address(0));
    assert(staffsInfo[_staff].wasRegistered);
    return staffsInfo[_staff].index;
}
Code Snippet 11. The implementation of kickStaff() and getStaffIndex()


Code snippet 11 explains the implementation of kickStaff function. When kickStaff function is being called, it first checks if the specified address identifies a registered staff in the line no’s. 10 - 13. If not, a transaction would be reverted. Even though a project deployer is considered as one of staff members, the deployer is not allowed to be revoked in our system. In the line no’s. 15 - 18, kickStaff function would revert a transaction if there is any attempt to revoke a project deployer.

Since an array element in Solidity cannot be removed like other traditional programming languages, we will use the element substitution procedure to tackle this issue. That is, we will move the last element of an array to replace a target element of which we want to remove and then we will decrease the array length by 1.

To revoke the specified staff, hence, kickStaff function invokes getStaffIndex function in order to get the target staffIndex, which points to the specified address on staffs array, in the line no. 20. The implementation of getStaffIndex function is defined in the line no. 43. After obtaining the staffIndex, kickStaff function removes the specified staff’s address from staffs array by moving the last array element to replace the element pointed by the obtained staffIndex in the line no. 24. Since the last array element has just moved to the element pointed by staffIndex, the function has to update the last moved staff’s index to point to staffIndex in the line no. 29. Later, the function removes the last array element by decreasing the staffs array’s length by 1 according to the line no. 32. In the line no. 35, the function executes delete operator to remove the personal record of the revoked staff from staffsInfo mapping. Finally, SafeMath library undertakes a secure substraction operation by deducting totalSupply by a number of initial voting tokens (i.e., the value indicated by voterInitialTokens) in the line no. 37.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
// ------------------------------------------------------------------------
// Allow the staff give a vote to the specified team
// ------------------------------------------------------------------------
function commitToVote(address _staff, string _teamName, uint256 _votingWeight) 
    external onlyVotingState onlyPizzaCoin 
{
    require(
        _staff != address(0),
        "'_staff' contains an invalid address."
    );

    require(
        _teamName.isNotEmpty(),
        "'_teamName' might not be empty."
    );

    require(
        _votingWeight > 0,
        "'_votingWeight' must be larger than 0."
    );

    require(
        staffsInfo[_staff].wasRegistered,
        "Cannot find the specified staff."
    );

    require(
        _votingWeight <= staffsInfo[_staff].tokenBalance,
        "Insufficient voting balance."
    );

    staffsInfo[_staff].tokenBalance = staffsInfo[_staff].tokenBalance.sub(_votingWeight);

    // If staffsInfo[_staff].votesWeight[_teamName] > 0 is true, this implies that 
    // the staff used to give a vote to the specified team previously
    if (staffsInfo[_staff].votesWeight[_teamName] == 0) {
        // The staff has never given a vote to the specified team before
        // We, therefore, have to add a new team to the 'teamsVoted' array
        staffsInfo[_staff].teamsVoted.push(_teamName);
    }

    staffsInfo[_staff].votesWeight[_teamName] = staffsInfo[_staff].votesWeight[_teamName].add(_votingWeight);
}
Code Snippet 12. The implementation of commitToVote() of PizzaCoinStaff contract


The last function of PizzaCoinStaff contract we would like to discuss in this article is commitToVote function as shown in the code snippet 12. This function is called when the staff wants to give a vote to any favourite team. Like any other state-changing functions, commitToVote function must be executed by the staff through its mapped function on PizzaCoin contract only. Besides, the function is applied with onlyVotingState modifier in the line no. 5. Hence, this function can be executed successfully if and only if the PizzaCoinStaff contract is in Voting state.

commitToVote function requires three input parameters including _staff, _teamName and _votingWeight. _staff indicates an account address of the staff who submits a voting transaction. Meanwhile, _teamName represents a name of the team in which the staff would want to vote to. _votingWeight indicates a number of voting tokens in which the staff would like to give to the specified team.

Upon receiving a voting transaction, commitToVote function first checks whether or not _staff represents an address of the actual registered staff in the line no’s. 22 - 25. Then, the function verifies that the staff holds enough tokens to spend in the line no’s. 27 - 30. If everything went fine then the function deducts the token balance of that staff by _votingWeight in the line no. 32. Again, SafeMath library’s sub function undertakes a secure substraction operation on staff’s token balance at this point. In the line no’s. 36 - 40, the function pushes a name of the voted team _teamName into the array teamsVoted. This action is performed only when the staff gives a vote to the specified team for the first time. Eventually, the function employs add function of SafeMath library to securely transfer the given tokens _votingWeight to the specified team in the line no. 42.

The implementation of PizzaCoinPlayer contract


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
struct PlayerInfo {
    // This is used to reduce potential gas cost consumption when kicking a player
    uint256 index;  // A pointing index to a particular player on the 'players' array

    string name;
    bool wasRegistered;    // Check if a specific player is being registered
    string teamName;       // A team this player associates with
    uint256 tokenBalance;  // Amount of tokens left for voting
    string[] teamsVoted;   // A collection of teams voted by this player
    
    // mapping(team => votingWeight)
    mapping(string => uint256) votesWeight;  // Teams with voting weight approved by this player
}

address[] private players;
mapping(address => PlayerInfo) private playersInfo;  // mapping(player => PlayerInfo)
Code Snippet 13. Data structures and state variables for storing player’s personal information


Actually, the functionality between PizzaCoinStaff contract and PizzaCoinPlayer contract is quite similar. If you understand how PizzaCoinStaff contract works, you do understand how PizzaCoinPlayer contract works already. Both contracts provision almost all the same functions but they are targeting on different types of users i.e. staff and player. That is, PizzaCoinStaff contract manages staff-related tasks whereas PizzaCoinPlayer contract manages player-related tasks.

Once PizzaCoinStaff and PizzaCoinPlayer are integrated with PizzaCoin mother contract, the mother contract would redirect a transaction request to either PizzaCoinStaff or PizzaCoinPlayer in accordance with a type of a requesting user. More specifically, any transaction request sent from an address being registered as the staff would be routed to PizzaCoinStaff contract. The request would be transacted and recorded on PizzaCoinStaff contract. Meanwhile, any transaction sent from an address being registred as a player would be handed over to PizzaCoinPlayer contract to handle instead.

The code snippet 13 describes data structures and state variables used by PizzaCoinPlayer contract. As you can see, the only difference between PlayerInfo struct used by PizzaCoinPlayer and StaffInfo struct used by PizzaCoinStaff (code snippet 5) is that PlayerInfo has an additional field named teamName in the line no. 7, which indicates a team that associates with a specific player.

The code snippets 14 - 16 show the implementation of registerPlayer, kickPlayer and commitToVote functions of PizzaCoinPlayer contract. As mentioned earlier, all of them were implemented using the similar programming logic like what happens to PizzaCoinStaff contract. The following link points to the source file of PizzaCoinPlayer.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
// ------------------------------------------------------------------------
// Register a player
// ------------------------------------------------------------------------
function registerPlayer(address _player, string _playerName, string _teamName) 
    external onlyRegistrationState onlyPizzaCoin 
{
    require(
        _player != address(0),
        "'_player' contains an invalid address."
    );

    require(
        _playerName.isNotEmpty(),
        "'_playerName' might not be empty."
    );

    require(
        _teamName.isNotEmpty(),
        "'_teamName' might not be empty."
    );

    require(
        playersInfo[_player].wasRegistered == false,
        "The specified player was registered already."
    );

    // Register a new player
    players.push(_player);
    playersInfo[_player] = PlayerInfo({
        wasRegistered: true,
        name: _playerName,
        tokenBalance: voterInitialTokens,
        teamName: _teamName,
        teamsVoted: new string[](0),
        /*
            Omit 'votesWeight'
        */
        index: players.length - 1
    });

    totalSupply = totalSupply.add(voterInitialTokens);
}
Code Snippet 14. The implementation of registerPlayer()


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
// ------------------------------------------------------------------------
// Remove a specific player from a particular team
// ------------------------------------------------------------------------
function kickPlayer(address _player, string _teamName) external onlyRegistrationState onlyPizzaCoin {
    require(
        _player != address(0),
        "'_player' contains an invalid address."
    );

    require(
        _teamName.isNotEmpty(),
        "'_teamName' might not be empty."
    );
    
    require(
        __isPlayerInTeam(_player, _teamName),
        "Cannot find the specified player in a given team."
    );

    uint256 playerIndex = getPlayerIndex(_player);

    // Remove the specified player from an array by moving 
    // the last array element to the element pointed by playerIndex
    players[playerIndex] = players[players.length - 1];

    // Since we have just moved the last array element to 
    // the element pointed by playerIndex, we have to update 
    // the newly moved player's index to playerIndex too
    playersInfo[players[playerIndex]].index = playerIndex;

    // Remove the last element
    players.length--;

    // Remove the specified player from a mapping
    delete playersInfo[_player];

    totalSupply = totalSupply.sub(voterInitialTokens);
}

// ------------------------------------------------------------------------
// Get an index pointing to the specified player on the array 'players'
// ------------------------------------------------------------------------
function getPlayerIndex(address _player) internal view returns (uint256 _playerIndex) {
    assert(_player != address(0));
    assert(playersInfo[_player].wasRegistered);
    return playersInfo[_player].index;
}
Code Snippet 15. The implementation of kickPlayer() and getPlayerIndex()


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
// ------------------------------------------------------------------------
// Allow a player vote to other different teams
// ------------------------------------------------------------------------
function commitToVote(address _player, string _teamName, uint256 _votingWeight) 
    external onlyVotingState onlyPizzaCoin 
{
    require(
        _player != address(0),
        "'_player' contains an invalid address."
    );

    require(
        _teamName.isNotEmpty(),
        "'_teamName' might not be empty."
    );

    require(
        _votingWeight > 0,
        "'_votingWeight' must be larger than 0."
    );

    require(
        playersInfo[_player].wasRegistered,
        "Cannot find the specified player."
    );

    require(
        playersInfo[_player].teamName.isEqual(_teamName) == false,
        "A player is not permitted to vote to his/her own team."
    );

    require(
        _votingWeight <= playersInfo[_player].tokenBalance,
        "Insufficient voting balance."
    );

    playersInfo[_player].tokenBalance = playersInfo[_player].tokenBalance.sub(_votingWeight);

    // If playersInfo[_player].votesWeight[_teamName] > 0 is true, this implies that 
    // the player used to give a vote to the specified team previously
    if (playersInfo[_player].votesWeight[_teamName] == 0) {
        // The player has never given a vote to the specified team before
        // We, therefore, have to add a new team to the 'teamsVoted' array
        playersInfo[_player].teamsVoted.push(_teamName);
    }

    playersInfo[_player].votesWeight[_teamName] = playersInfo[_player].votesWeight[_teamName].add(_votingWeight);
}
Code Snippet 16. The implementation of commitToVote() of PizzaCoinPlayer contract


Summary


Let’s summarize. In this article, you have learned how PizzaCoinStaff and PizzaCoinPlayer contracts were implemented. In order to avoid ‘Out-of-Gas’ error when deploying the contracts as well as when invoking a contract function, several techniques were employed. In the next article, you will learn how PizzaCoinTeam, which is another child of PizzaCoin contract, was implemented in detail. See you in the next article.




PizzaCoin the series consists of 6 articles as follows.

Part 1: How Did We Develop Ethereum-based Voting System for Pizza Hackathon?

Part 2: Workflow Design for PizzaCoin Voting System

Part 3: Detailed Implementation of Staff and Player Contracts

Part 4: Detailed Implementation of Team Contract

Part 5: Deploying Children Contracts with Contract Factories

Part 6: Integrating PizzaCoin Contract with Dependencies