Contents

[ PizzaCoin the Series #4 ] Detailed Implementation of Team Contract

Figure 1. Pizza Hackathon 2018

Figure 1. Pizza Hackathon 2018


Welcome to the 4th part of PizzaCoin the Series. In the previous article, you have learned how PizzaCoinStaff and PizzaCoinPlayer contracts were implemented. In this article, you will learn the implementation of another child of PizzaCoin contract called PizzaCoinTeam. To better understand the contents of this article, you may be required to understand the contents of the previous article. So if you come across this article, we recommend you to read the previous article first.

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.

PizzaCoinTeam – one of the three PizzaCoin’s children contracts responsible for managing team-related tasks such as creating teams, registering a player to a specific team, revoking teams, revoking a specific player from a particular team, handling team voting, and providing team information as well as voting results.

Source files refered to in this article


The implementation of PizzaCoinTeam 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
62
63
64
65
66
67
68
69
70
71
72
73
74
75
pragma solidity ^0.4.23;

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


// ------------------------------------------------------------------------
// Interface for exporting external functions of PizzaCoinTeam contract
// ------------------------------------------------------------------------
interface ITeamContract {
    function lockRegistration() external;
    function startVoting() external;
    function stopVoting() external;
    function createTeam(string _teamName) external;
    function registerPlayerToTeam(address _player, string _teamName) external;
    function kickTeam(string _teamName) external;
    function kickPlayerOutOfTeam(address _player, string _teamName) external;
    function doesTeamExist(string _teamName) external view returns (bool bTeamExist);
    function getTotalPlayersInTeam(string _teamName) external view returns (uint256 _total);
    function getPlayerInTeamAtIndex(string _teamName, uint256 _playerIndex) 
        external view 
        returns (
            bool _endOfList, 
            address _player
        );
    function getTotalTeams() external view returns (uint256 _total);
    function getTeamInfoAtIndex(uint256 _teamIndex) 
        external view
        returns (
            bool _endOfList,
            string _teamName,
            uint256 _totalVoted
        );
    function getVotingPointsOfTeam(string _teamName) external view returns (uint256 _totalVoted);
    function getTotalVotersToTeam(string _teamName) external view returns (uint256 _total);
    function getVotingResultToTeamAtIndex(string _teamName, uint256 _voterIndex) 
        external view
        returns (
            bool _endOfList,
            address _voter,
            uint256 _voteWeight
        );
    function voteToTeam(address _voter, string _teamName, uint256 _votingWeight) external;
    function getMaxTeamVotingPoints() external view returns (uint256 _maxTeamVotingPoints);
    function getTotalWinningTeams() external view returns (uint256 _total);
    function getFirstFoundWinningTeam(uint256 _startSearchingIndex) 
        external view
        returns (
            bool _endOfList,
            uint256 _nextStartSearchingIndex,
            string _teamName, 
            uint256 _totalVoted
        );
}


// ----------------------------------------------------------------------------
// Pizza Coin Team Contract
// ----------------------------------------------------------------------------
contract PizzaCoinTeam is ITeamContract, Owned {
    /*
    * Owner of the contract is PizzaCoin contract, 
    * not a project deployer who is PizzaCoin owner
    */

    using SafeMath for uint256;
    using BasicStringUtils for string;



    ...
    ...
    ...
}
Code Snippet 1. Excerpt from PizzaCoinTeam contract’s source code


The code structure of PizzaCoinTeam contract is organized the same as PizzaCoinStaff and PizzaCoinPlayer. That is, PizzaCoinTeam consists of two integral parts as shown in the code snippet 1 including ITeamContract interface in the line no. 11 and PizzaCoinTeam contract in the line no. 61. ITeamContract contains function prototypes of PizzaCoinTeam that expose to PizzaCoin mother contract. PizzaCoinTeam contract implements ITeamContract interface and inherits from the contract named Owned similar to what we have discussed in the previous article.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
// Team with players
struct TeamInfo {
    // This is used to reduce potential gas cost consumption when kicking a team
    uint256 index;  // A pointing index to a particular team on the 'teams' array

    bool wasCreated;    // Check if a team is being created
    address[] players;  // A list of team members (the first member is the one who creates a team)

    // mapping(player => playerIndex)
    mapping(address => uint256) playerIndexMap;  // This is used to reduce potential gas cost consumption when kicking a player in a team

    address[] voters;  // A list of staffs and other teams' members who have ever voted to a team

    // mapping(voter => votingWeight)
    mapping(address => uint256) votesWeight;  // Voting weight from each voter
    
    uint256 totalVoted;  // Total voting weight from all voters
}

string[] private teams;
mapping(string => TeamInfo) private teamsInfo;  // mapping(team => TeamInfo)
Code Snippet 2. Data structures and state variables for storing teams and voting information


There are two state variables used for storing teams and voting information under PizzaCoinTeam contract namely teams and teamsInfo as respectively defined in the line no’s. 20 and 21 of the code snippet 2. teams is an array collecting names of all registered teams. Whereas teamsInfo is a mapping variable which gathers information of each particular team in terms of struct TeamInfo as defined in the line no. 2. Name of any specific team is used as a mapping key for accessing corresponding team information stored on teamsInfo mapping.

The struct TeamInfo gathers a group of state variables including index, wasCreated, players, playerIndexMap, voters, votesWeight and totalVoted. index points to an element on the array teams for a particular team. wasCreated is a boolean variable used for indicating a registration status of a particular team. The use of wasCreated variable is similar to what we have discussed about the use of wasRegistered under PizzaCoinStaff contract in the previous article. wasCreated plays an important role to significantly reduce gas consumption for verifying a registration status of a specific team. players is an array containing addresses of the members in a team. playerIndexMap is a mapping which maps a player address to an index pointing to a corresponding element on players array. voters is an array containing addresses of all voters who commit a vote to the team. votesWeight is a mapping variable, using an address of a specific voter as a mapping key, containing a voting weight that any particular voter has ever voted to the team. totalVoted represents a sum of voting weights from all voters.

 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
// ------------------------------------------------------------------------
// Player creates a new team
// ------------------------------------------------------------------------
function createTeam(string _teamName) external onlyRegistrationState onlyPizzaCoin {
    require(
        _teamName.isNotEmpty(),
        "'_teamName' might not be empty."
    );
    
    require(
        teamsInfo[_teamName].wasCreated == false,
        "The given team was created already."
    );

    // Create a new team
    teams.push(_teamName);
    teamsInfo[_teamName] = TeamInfo({
        wasCreated: true,
        players: new address[](0),
        voters: new address[](0),
        totalVoted: 0,
        /*
            Omit 'votesWeight'
        */
        index: teams.length - 1
        /*
            Omit 'playerIndexMap'
        */
    });
}
Code Snippet 3. The implementation of createTeam()


The implementation of createTeam function is shown in the code snippet 3. The function has a single input parameter named _teamName which indicates a name of a specific team needed to be registered. On receiving a transaction request, createTeam function first checks the existence of the specified team in the line no’s. 10 - 13. If the specified team is already registered, the function reverts a transaction in the line no. 12, unless the function registers the specified team to the system in the line no’s. 16 - 29. Technically, _teamName is pushed into teams array in the line no. 16. After that, the function initializes team information with initial values as shown in the line no’s. 18 - 28 and then records the initialized information into teamsInfo mapping in the line no. 17.

 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
// ------------------------------------------------------------------------
// Register a player to a specific team
// ------------------------------------------------------------------------
function registerPlayerToTeam(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(
        teamsInfo[_teamName].wasCreated,
        "The given team does not exist."
    );

    // Add a player to the specified team
    teamsInfo[_teamName].players.push(_player);
    teamsInfo[_teamName].playerIndexMap[_player] = teamsInfo[_teamName].players.length - 1;
}
Code Snippet 4. The implementation of registerPlayerToTeam()


The implementation of registerPlayerToTeam function is shown in the code snippet 4. This function is invoked when a user wants to join an existing team. The function has two input parameters _player and _teamName. First, the function verifies that _teamName indicates an existing team in the line no’s. 17 - 20. If the verification proccess succeeds then the function registers the specified address _player as a new member to the team _teamName in the line no’s. 23 and 24 else the function reverts a transaction in the line no. 19.

You may wonder why registerPlayerToTeam function does not authenticate the specified address _player. Let’s recap to understand this. According to the conceptual design of PizzaCoin voting system, we separate several functional subsystems into multiple contracts. Each contract manages distinct tasks. PizzaCoinPlayer manages functions related to tasks such as registering players, revoking players as well as authenticating players whereas PizzaCoinTeam manages functions related to tasks such as registering teams, revoking teams, authenticating teams as well as registering players to existing teams. However, PizzaCoinTeam contract is not responsible for authenticating an identity of any player because the contract does not hold any player information itself. To verify the authenticity of a player, therefore, we delegate PizzaCoin mother contract to do this task instead. More specifically, the mother contract would authenticate the specified address _player by consulting with PizzaCoinPlayer contract before invoking registerPlayerToTeam function. Certainly, you will better understand this point after reading the later articles. So keep staying with us.

 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
62
63
64
// ------------------------------------------------------------------------
// Remove a specific team (the team must be empty of players)
// ------------------------------------------------------------------------
function kickTeam(string _teamName) external onlyRegistrationState onlyPizzaCoin {
    require(
        _teamName.isNotEmpty(),
        "'_teamName' might not be empty."
    );

    require(
        teamsInfo[_teamName].wasCreated,
        "Cannot find the specified team."
    );

    uint256 totalPlayers = __getTotalPlayersInTeam(_teamName);

    // The team can be removed if and only if it has 0 player left
    if (totalPlayers != 0) {
        revert("The specified team is not empty.");
    }

    uint256 teamIndex = getTeamIndex(_teamName);

    // Remove the specified team from an array by moving 
    // the last array element to the element pointed by teamIndex
    teams[teamIndex] = teams[teams.length - 1];

    // Since we have just moved the last array element to 
    // the element pointed by teamIndex, we have to update 
    // the newly moved team's index to teamIndex too
    teamsInfo[teams[teamIndex]].index = teamIndex;

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

    // Remove the specified team from a mapping
    delete teamsInfo[_teamName];
}

// ------------------------------------------------------------------------
// Get a total number of players in the specified team (internal)
// ------------------------------------------------------------------------
function __getTotalPlayersInTeam(string _teamName) internal view returns (uint256 _total) {
    require(
        _teamName.isNotEmpty(),
        "'_teamName' might not be empty."
    );

    require(
        teamsInfo[_teamName].wasCreated,
        "Cannot find the specified team."
    );

    return teamsInfo[_teamName].players.length;
}

// ------------------------------------------------------------------------
// Get an index pointing to the specified team on the array 'teams'
// ------------------------------------------------------------------------
function getTeamIndex(string _teamName) internal view returns (uint256 _teamIndex) {
    assert(_teamName.isNotEmpty());
    assert(teamsInfo[_teamName].wasCreated);
    return teamsInfo[_teamName].index;
}
Code Snippet 5. The implementation of kickTeam(), __getTotalPlayersInTeam() and getTeamIndex()


Code snippet 5 describes the implementation of kickTeam function. With this function, the staff has right to revoke some team if necessary. The function requires one input parameter _teamName which indicates a name of the team to be revoked. As the function is defined with onlyRegistrationState modifier in the line no. 4, this function can be invoked only if PizzaCoinTeam contract is in Registration state. Moreover, as discussed above we delegate PizzaCoin contract to authenticate the staff who is invoking the function instead.

Once kickTeam function is invoked, the function first verifies the existence of the specified team in the line no’s. 10 - 13. If the specified team exists, the function proceeds to verify if the team is empty of players in the line no’s. 15 - 20. In more detail, kickTeam function invokes __getTotalPlayersInTeam( _teamName) function in the line no. 15 to get a number of current players joining in the specified team. If the team is not empty then the function reverts a transaction in line no’s. 18 - 20 else the function asks for an index of the specified team by calling to getTeamIndex(_teamName) function in the line no. 22. Finally, the function removes the specified team pointed by the obtained index from the array teams in the line no’s. 26, 31 and 34. The function then removes the revoked team information from the mapping teamsInfo 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
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
// ------------------------------------------------------------------------
// Remove a specific player from a particular team
// ------------------------------------------------------------------------
function kickPlayerOutOfTeam(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(
        teamsInfo[_teamName].wasCreated,
        "Cannot find the specified team."
    );

    bool found;
    uint256 playerIndex;

    (found, playerIndex) = getPlayerIndexInTeam(_player, _teamName);
    if (!found) {
        revert("Cannot find the specified player in a given team.");
    }

    // Remove the specified player from an array by moving 
    // the last array element to the element pointed by playerIndex
    teamsInfo[_teamName].players[playerIndex] = teamsInfo[_teamName].players[teamsInfo[_teamName].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
    teamsInfo[_teamName].playerIndexMap[teamsInfo[_teamName].players[playerIndex]] = playerIndex;

    // Remove the last element
    teamsInfo[_teamName].players.length--;

    // Remove the specified player from a mapping
    delete teamsInfo[_teamName].playerIndexMap[_player];
}

// ------------------------------------------------------------------------
// Get an index pointing to a specific player on the array 'players' of a given team
// ------------------------------------------------------------------------
function getPlayerIndexInTeam(address _player, string _teamName) 
    internal view 
    returns ( 
        bool _found, 
        uint256 _playerIndex
    ) 
{
    assert(_player != address(0));
    assert(_teamName.isNotEmpty());
    assert(teamsInfo[_teamName].wasCreated);

    _playerIndex = teamsInfo[_teamName].playerIndexMap[_player];
    _found = teamsInfo[_teamName].players[_playerIndex] == _player;
}
Code Snippet 6. The implementation of kickPlayerOutOfTeam() and getPlayerIndexInTeam()


Code snippet 6 shows the implementation of kickPlayerOutOfTeam function. Staff would invoke this function in order to revoke a specific player from a particular team. The function requires two input parameters _player and _teamName. _player denotes an address of the player to be revoked. _teamName indicates a name of the team in which the player associates with. Once the function is invoked, it verifies the existence of the team _teamName in line no’s. 17 - 20. Later, the function makes a call to getPlayerIndexInTeam( _player, _teamName) function in order to get a player index in the line no. 25. If everything went well, the function removes the player address pointed by the obtained index from the array players in the line no’s. 32, 37 and 40. Eventually, the function removes the player from the mapping playerIndexMap in the line no. 43.

 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
// ------------------------------------------------------------------------
// Allow the staff or player to give a vote to the specified team
// ------------------------------------------------------------------------
function voteToTeam(address _voter, string _teamName, uint256 _votingWeight) 
    external onlyVotingState onlyPizzaCoin 
{
    require(
        _voter != address(0),
        "'_voter' contains an invalid address."
    );

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

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

    require(
        teamsInfo[_teamName].wasCreated,
        "Cannot find the specified team."
    );

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

    teamsInfo[_teamName].votesWeight[_voter] = teamsInfo[_teamName].votesWeight[_voter].add(_votingWeight);
    teamsInfo[_teamName].totalVoted = teamsInfo[_teamName].totalVoted.add(_votingWeight);
}
Code Snippet 7. The implementation of voteToTeam()


The last function to be discussed in this article is voteToTeam function as described in the code snippet 7. This function allows both the staff and player commit a vote to a favourite team. The function requires three input parameters _voter, _teamName and _votingWeight. _voter denotes an address of the one who submits a voting transaction. _teamName indicates a name of the team to be voted. _votingWeight specifies a number of voting tokens given to the team. Note that again, we delegate PizzaCoin mother contract to authenticate a voter as well as verifying the voter’s token balance.

Upon receiving a voting transaction, voteToTeam function verifies the existence of the team to be voted in the line no’s. 22 - 25. Then, the function checks if the voter used to vote to the specified team or not in the line no. 29. That is, if the statement teamsInfo[_teamName].votesWeight[_voter] == 0 is true, this implies that the voter never used to vote to the specified team before. If so, the function registers the voter address into the array voters of the specified team which is stored on the mapping teamsInfo in the line no. 33. At last, the function securely transfers the given token _votingWeight to the specified team in the line no. 36 and then securely updates totalVoted variable of the specified team in the line no. 37. voteToTeam function leverages add function of SafeMath library in order to perform those secure update operations.

Summary


Let’s summarize. In this article, you have learned how PizzaCoinTeam contract was implemented. You have also learned that we delegated PizzaCoin mother contract as a coordinator for authenticating and verifying staffs and players instead, since PizzaCoinTeam contract does not hold any user information itself. In the next article, you will learn how to deploy PizzaCoin’s children contracts with contract factories.




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