Skip to content

Commit

Permalink
First commit
Browse files Browse the repository at this point in the history
  • Loading branch information
jbell-circle committed Sep 15, 2024
0 parents commit d81e88b
Show file tree
Hide file tree
Showing 9 changed files with 330 additions and 0 deletions.
45 changes: 45 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
name: CI

on:
push:
pull_request:
workflow_dispatch:

env:
FOUNDRY_PROFILE: ci

jobs:
check:
strategy:
fail-fast: true

name: Foundry project
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
submodules: recursive

- name: Install Foundry
uses: foundry-rs/foundry-toolchain@v1
with:
version: nightly

- name: Show Forge version
run: |
forge --version
- name: Run Forge fmt
run: |
forge fmt --check
id: fmt

- name: Run Forge build
run: |
forge build --sizes
id: build

- name: Run Forge tests
run: |
forge test -vvv
id: test
17 changes: 17 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Compiler files
cache/
out/

# Ignores development broadcast logs
!/broadcast
/broadcast/*/31337/
/broadcast/**/dry-run/

# Docs
docs/

# Dotenv file
.env

# Ignores snapshots
.gas-snapshot
3 changes: 3 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[submodule "lib/forge-std"]
path = lib/forge-std
url = https://github.com/foundry-rs/forge-std
87 changes: 87 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
# Ballot

## The Contract

The Ballot contract showcases many Solidity features.
It implements a voting contract.
It creates one contract per ballot, providing a short name for each option.
The creator of the contract - the chairperson - gives the right to vote to each address individually.
The persons behind the addresses can then choose to either vote themselves or to delegate their vote to someone else.
winningProposal() returns the proposal with the largest number of votes.

The source code for the contract is found at ./src/Ballot.sol

## Requirements

### Foundry

Install foundry by running the following command and following the instructions.

```sh
curl -L https://foundry.paradigm.xyz | bash
```

Foundry is a toolkit for Ethereum application development written in Rust.
It consists of several tools but we'll focus on **forge**: an ethereum build and testing framework (like Truffle or Hardhat).

Read https://book.getfoundry.sh/ to learn more.

## Intro

The following command will build and run the tests for your smart contracts.

```
forge test
```

### Challenge

1. Write tests for the delegate function.
2. Write a Ballot function that allows the chairperson to give multiple addresses the right to vote.

## Experimenting

Foundry also comes with a local testnet node, **anvil**.
The following command will start the node, expose an rpc endpoint on `127.0.0.1:8545`, and initialize 10 accounts with some ETH balance.

```sh
anvil --acccounts 10
```

You can then use the provided ./script/Ballot.s.sol to deploy a Ballot with two proposals.
The `BALLOT_DEPLOYER_PRIVATEKEY` envvar is used referenced in the script.
By default, anvil creates the private key with the value presented below.

```sh
export BALLOT_RPCURL=127.0.0.1:8545
export BALLOT_DEPLOYER_PRIVATEKEY=0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80
forge script script/Ballot.s.sol:BallotScript --rpc-url $BALLOT_RPCURL --broadcast
```

The output above will include the contract address.
Copy this to an environment variable for further use.

```
export BALLOT_CONTRACTADDRESS=<CONTRACT_ADDRESS>
```

**cast** is Foundry’s command-line tool for Ethereum RPC calls.
The following sends a vote from the deployer address.

```sh
cast send --private-key $BALLOT_DEPLOYER_PRIVATEKEY \
$BALLOT_CONTRACTADDRESS "vote(uint256)" 1 \
--rpc-url $BALLOT_RPCURL
```

Read-only commands don't require a private key.

```sh
cast call $BALLOT_CONTRACTADDRESS "getVoteCount(uint256)" 1 \
--rpc-url $BALLOT_RPCURL
```

### Challenge

1. Give another address the right to vote and use that address.
2. Get the winning proposal name. This will be in bytes32 so use `cast parse-bytes32-string` to convert it to a string [link](https://book.getfoundry.sh/reference/cast/cast-parse-bytes32-string)
6 changes: 6 additions & 0 deletions foundry.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
[profile.default]
src = "src"
out = "out"
libs = ["lib"]

# See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options
1 change: 1 addition & 0 deletions lib/forge-std
Submodule forge-std added at 1714be
24 changes: 24 additions & 0 deletions script/Ballot.s.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;

import {Script, console} from "forge-std/Script.sol";
import {Ballot} from "../src/Ballot.sol";

contract BallotScript is Script {
Ballot public ballot;

function setUp() public {}

function run() public {
uint256 deployerPrivateKey = vm.envUint("BALLOT_DEPLOYER_PRIVATEKEY");
vm.startBroadcast(deployerPrivateKey);

bytes32[] memory proposals = new bytes32[](3);
proposals[0] = bytes32("prop0");
proposals[1] = bytes32("prop1");

ballot = new Ballot(proposals);

vm.stopBroadcast();
}
}
89 changes: 89 additions & 0 deletions src/Ballot.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.0 <0.9.0;

/// @title Voting with delegation.
contract Ballot {
// This declares a new complex type which will
// be used for variables later.
// It will represent a single voter.

struct Voter {
uint256 weight; // weight is accumulated by delegation
bool voted; // if true, that person already voted
address delegate; // person delegated to
uint256 vote; // index of the voted proposal
}

// This is a type for a single proposal.
struct Proposal {
bytes32 name; // short name (up to 32 bytes)
uint256 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`.
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 (uint256 i = 0; i < proposalNames.length; i++) {
// `Proposal({...})` creates a temporary Proposal object and `proposals.push(...)`
// appends it to the end of `proposals`.
proposals.push(Proposal({name: proposalNames[i], voteCount: 0}));
}
}

// Give `voter` the right to vote on this ballot.
// May only be called by the `chairperson`
function giveRightToVote(address voter) external {
require(msg.sender == chairperson, "Only chairperson can give right to vote.");
require(!voters[voter].voted, "voter has already voted.");
require(voters[voter].weight == 0);

voters[voter].weight = 1;
}

// Check if the voter is still allowed to vote.
function hasRightToVote(address voter) public view returns (bool) {
return voters[voter].weight > 0;
}

function vote(uint256 proposal) external {
Voter storage sender = voters[msg.sender];
require(!sender.voted, "sender has already voted");
require(sender.weight > 0, "sender does not have right to vote");
sender.voted = true;
sender.vote = proposal;

proposals[proposal].voteCount += sender.weight;
}

function getVoteCount(uint256 proposal) public view returns (uint256) {
return proposals[proposal].voteCount;
}

function winningProposal() public view returns (uint256 winningProposal_) {
uint256 winningVoteCount = 0;
for (uint256 p = 0; p < proposals.length; p++) {
if (proposals[p].voteCount > winningVoteCount) {
winningVoteCount = proposals[p].voteCount;
winningProposal_ = p;
}
}
}

function winningProposalName() public view returns (bytes32 winningProposalName_) {
return proposals[winningProposal()].name;
}
}
58 changes: 58 additions & 0 deletions test/Ballot.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;

import {Test, console} from "forge-std/Test.sol";
import {Ballot} from "../src/Ballot.sol";

contract BallotTest is Test {
Ballot public ballot;

function setUp() public {
bytes32[] memory proposals = new bytes32[](3);
proposals[0] = bytes32("prop0");
proposals[1] = bytes32("prop1");

ballot = new Ballot(proposals);
}

function test_Vote() public {
ballot.vote(0);
assertEq(ballot.getVoteCount(0), 1);
}

function test_NoRightToVote_RevertVote() public {
// Given...
// the voter hasn't been given the right to vote
address voter = address(2);
vm.startPrank(voter);

// Expect...
vm.expectRevert("sender does not have right to vote");

// When...
// the voter tries to vote
ballot.vote(1);

vm.stopPrank();
}

function test_AlreadyVoted_RevertVote() public {
// Given...
// the voter has the right to vote
address voter = address(1);
ballot.giveRightToVote(voter);

// the voter has already voted
vm.startPrank(voter);
ballot.vote(0);

// Expect...
vm.expectRevert("sender has already voted");

// When...
// the voter tries to vote again
ballot.vote(1);

vm.stopPrank();
}
}

0 comments on commit d81e88b

Please sign in to comment.