diff --git a/.github/workflows/run-sut.yml b/.github/workflows/run-sut.yml new file mode 100644 index 0000000000..e6b8d98221 --- /dev/null +++ b/.github/workflows/run-sut.yml @@ -0,0 +1,20 @@ + +name: Running Solidity Unit Tests +on: [push] + +jobs: + run_sol_contracts_job: + runs-on: ubuntu-latest + name: A job to run solidity unit tests on github actions CI + steps: + - name: Checkout + uses: actions/checkout@v2 + - name: Environment Setup + uses: actions/setup-node@v3 + with: + node-version: 14.17.6 + - name: Run SUT Action + uses: EthereumRemix/sol-test@v1 + with: + test-path: 'apps/remix-ide/contracts/tests' + compiler-version: '0.8.15' \ No newline at end of file diff --git a/apps/remix-ide/contracts/ballot.sol b/apps/remix-ide/contracts/ballot.sol index d55734ed9a..ffcc6c3609 100644 --- a/apps/remix-ide/contracts/ballot.sol +++ b/apps/remix-ide/contracts/ballot.sol @@ -1,10 +1,13 @@ -pragma solidity ^0.4.0; +// SPDX-License-Identifier: GPL-3.0 -/// @title Voting with delegation. +pragma solidity >=0.7.0 <0.9.0; + +/** + * @title Ballot + * @dev Implements voting process along with vote delegation + */ contract Ballot { - // This declares a new complex type which will - // be used for variables later. - // It will represent a single voter. + struct Voter { uint weight; // weight is accumulated by delegation bool voted; // if true, that person already voted @@ -12,33 +15,31 @@ contract Ballot { uint vote; // index of the voted proposal } - // This is a type for a single proposal. struct Proposal { + // If you can limit the length to a certain number of bytes, + // always use one of bytes1 to bytes32 because they are much cheaper bytes32 name; // short name (up to 32 bytes) uint voteCount; // number of accumulated votes } address public chairperson; - // This declares a state variable that - // stores a \`Voter\` struct for each possible address. mapping(address => Voter) public voters; - // A dynamically-sized array of \`Proposal\` structs. Proposal[] public proposals; - /// Create a new ballot to choose one of \`proposalNames\`. - function Ballot(bytes32[] proposalNames) { + /** + * @dev Create a new ballot to choose one of 'proposalNames'. + * @param proposalNames names of proposals + */ + constructor(bytes32[] memory proposalNames) { chairperson = msg.sender; voters[chairperson].weight = 1; - // For each of the provided proposal names, - // create a new proposal object and add it - // to the end of the array. for (uint i = 0; i < proposalNames.length; i++) { - // \`Proposal({...})\` creates a temporary - // Proposal object and \`proposals.push(...)\` - // appends it to the end of \`proposals\`. + // 'Proposal({...})' creates a temporary + // Proposal object and 'proposals.push(...)' + // appends it to the end of 'proposals'. proposals.push(Proposal({ name: proposalNames[i], voteCount: 0 @@ -46,98 +47,92 @@ contract Ballot { } } - // Give \`voter\` the right to vote on this ballot. - // May only be called by \`chairperson\`. - function giveRightToVote(address voter) { - if (msg.sender != chairperson || voters[voter].voted) { - // \`throw\` terminates and reverts all changes to - // the state and to Ether balances. It is often - // a good idea to use this if functions are - // called incorrectly. But watch out, this - // will also consume all provided gas. - throw; - } + /** + * @dev Give 'voter' the right to vote on this ballot. May only be called by 'chairperson'. + * @param voter address of voter + */ + function giveRightToVote(address voter) public { + require( + msg.sender == chairperson, + "Only chairperson can give right to vote." + ); + require( + !voters[voter].voted, + "The voter already voted." + ); + require(voters[voter].weight == 0); voters[voter].weight = 1; } - /// Delegate your vote to the voter \`to\`. - function delegate(address to) { - // assigns reference - Voter sender = voters[msg.sender]; - if (sender.voted) - throw; - - // Forward the delegation as long as - // \`to\` also delegated. - // In general, such loops are very dangerous, - // because if they run too long, they might - // need more gas than is available in a block. - // In this case, the delegation will not be executed, - // but in other situations, such loops might - // cause a contract to get "stuck" completely. - while ( - voters[to].delegate != address(0) && - voters[to].delegate != msg.sender - ) { + /** + * @dev Delegate your vote to the voter 'to'. + * @param to address to which vote is delegated + */ + function delegate(address to) public { + Voter storage sender = voters[msg.sender]; + require(!sender.voted, "You already voted."); + require(to != msg.sender, "Self-delegation is disallowed."); + + while (voters[to].delegate != address(0)) { to = voters[to].delegate; - } - // We found a loop in the delegation, not allowed. - if (to == msg.sender) { - throw; + // We found a loop in the delegation, not allowed. + require(to != msg.sender, "Found loop in delegation."); } - - // Since \`sender\` is a reference, this - // modifies \`voters[msg.sender].voted\` sender.voted = true; sender.delegate = to; - Voter delegate = voters[to]; - if (delegate.voted) { + Voter storage delegate_ = voters[to]; + if (delegate_.voted) { // If the delegate already voted, // directly add to the number of votes - proposals[delegate.vote].voteCount += sender.weight; + proposals[delegate_.vote].voteCount += sender.weight; } else { // If the delegate did not vote yet, // add to her weight. - delegate.weight += sender.weight; + delegate_.weight += sender.weight; } } - /// Give your vote (including votes delegated to you) - /// to proposal \`proposals[proposal].name\`. - function vote(uint proposal) { - Voter sender = voters[msg.sender]; - if (sender.voted) - throw; + /** + * @dev Give your vote (including votes delegated to you) to proposal 'proposals[proposal].name'. + * @param proposal index of proposal in the proposals array + */ + function vote(uint proposal) public { + Voter storage sender = voters[msg.sender]; + require(sender.weight != 0, "Has no right to vote"); + require(!sender.voted, "Already voted."); sender.voted = true; sender.vote = proposal; - // If \`proposal\` is out of the range of the array, + // If 'proposal' is out of the range of the array, // this will throw automatically and revert all // changes. proposals[proposal].voteCount += sender.weight; } - /// @dev Computes the winning proposal taking all - /// previous votes into account. - function winningProposal() constant - returns (uint winningProposal) + /** + * @dev Computes the winning proposal taking all previous votes into account. + * @return winningProposal_ index of winning proposal in the proposals array + */ + function winningProposal() public view + returns (uint winningProposal_) { uint winningVoteCount = 0; for (uint p = 0; p < proposals.length; p++) { if (proposals[p].voteCount > winningVoteCount) { winningVoteCount = proposals[p].voteCount; - winningProposal = p; + winningProposal_ = p; } } } - - // Calls winningProposal() function to get the index - // of the winner contained in the proposals array and then - // returns the name of the winner - function winnerName() constant - returns (bytes32 winnerName) + + /** + * @dev Calls winningProposal() function to get the index of the winner contained in the proposals array and then + * @return winnerName_ the name of the winner + */ + function winnerName() public view + returns (bytes32 winnerName_) { - winnerName = proposals[winningProposal()].name; + winnerName_ = proposals[winningProposal()].name; } -} +} \ No newline at end of file diff --git a/apps/remix-ide/contracts/tests/Ballot_test.sol b/apps/remix-ide/contracts/tests/Ballot_test.sol new file mode 100644 index 0000000000..452a5433b4 --- /dev/null +++ b/apps/remix-ide/contracts/tests/Ballot_test.sol @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: GPL-3.0 + +pragma solidity >=0.7.0 <0.9.0; +import "remix_tests.sol"; // this import is automatically injected by Remix. +import "hardhat/console.sol"; +import "../ballot.sol"; + +contract BallotTest { + + bytes32[] proposalNames; + + Ballot ballotToTest; + function beforeAll () public { + proposalNames.push(bytes32("candidate1")); + ballotToTest = new Ballot(proposalNames); + } + + function checkWinningProposal () public { + console.log("Running checkWinningProposal"); + ballotToTest.vote(0); + Assert.equal(ballotToTest.winningProposal(), uint(0), "proposal at index 0 should be the winning proposal"); + Assert.equal(ballotToTest.winnerName(), bytes32("candidate1"), "candidate1 should be the winner name"); + } + + function checkWinninProposalWithReturnValue () public view returns (bool) { + return ballotToTest.winningProposal() == 0; + } +} \ No newline at end of file