Skip to content

Commit

Permalink
Add contracts (#103)
Browse files Browse the repository at this point in the history
## tl;dr

- Adds smart contracts to repo
- Sets up Foundry in the repo 
- Adds tests for smart contracts
- Sets up `abigen` for generating code out of smart contracts

## Warning

You will need to run `./dev/up` to generate some code and start the blockchain node

You may also have to run `git submodule update --init --recursive` to pull down the submodules.
  • Loading branch information
neekolas authored Aug 12, 2024
2 parents 75bcd84 + 57b294a commit dfcdc77
Show file tree
Hide file tree
Showing 28 changed files with 1,380 additions and 9 deletions.
26 changes: 25 additions & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ on:
pull_request:
jobs:
test:
name: Test
name: Test (Node)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
Expand All @@ -31,3 +31,27 @@ jobs:
service: xmtp-node-go
files: report.xml
env: ci
contracts:
name: Test (Contracts)
strategy:
fail-fast: true
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
submodules: recursive

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

- name: Run Forge build
working-directory: contracts
run: |
forge --version
forge build --sizes
- name: Run Forge tests
working-directory: contracts
run: |
forge test -vvv
id: test
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,4 @@ bin/

# Dependency directories (remove the comment below to include it)
# vendor/
build/*
6 changes: 6 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
[submodule "lib/forge-std"]
path = contracts/lib/forge-std
url = https://github.com/foundry-rs/forge-std
[submodule "lib/openzeppelin-contracts"]
path = contracts/lib/openzeppelin-contracts
url = https://github.com/openzeppelin/openzeppelin-contracts
11 changes: 6 additions & 5 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,11 @@
// Instructions from https://github.com/segmentio/golines
"emeraldwalk.runonsave": {
"commands": [
{
"match": "\\.go$",
"cmd": "golines ${file} -w"
}
{
"match": "\\.go$",
"cmd": "golines ${file} -w"
}
]
}
},
"solidity.defaultCompiler": "localNodeModule"
}
14 changes: 14 additions & 0 deletions contracts/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# Compiler files
cache/
out/

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

# Docs
docs/

# Dotenv file
.env
66 changes: 66 additions & 0 deletions contracts/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
## Foundry

**Foundry is a blazing fast, portable and modular toolkit for Ethereum application development written in Rust.**

Foundry consists of:

- **Forge**: Ethereum testing framework (like Truffle, Hardhat and DappTools).
- **Cast**: Swiss army knife for interacting with EVM smart contracts, sending transactions and getting chain data.
- **Anvil**: Local Ethereum node, akin to Ganache, Hardhat Network.
- **Chisel**: Fast, utilitarian, and verbose solidity REPL.

## Documentation

https://book.getfoundry.sh/

## Usage

### Build

```shell
$ forge build
```

### Test

```shell
$ forge test
```

### Format

```shell
$ forge fmt
```

### Gas Snapshots

```shell
$ forge snapshot
```

### Anvil

```shell
$ anvil
```

### Deploy

```shell
$ forge script script/Counter.s.sol:CounterScript --rpc-url <your_rpc_url> --private-key <your_private_key>
```

### Cast

```shell
$ cast <subcommand>
```

### Help

```shell
$ forge --help
$ anvil --help
$ cast --help
```
6 changes: 6 additions & 0 deletions contracts/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 contracts/lib/forge-std
Submodule forge-std added at bf6606
1 change: 1 addition & 0 deletions contracts/lib/openzeppelin-contracts
Submodule openzeppelin-contracts added at c304b6
1 change: 1 addition & 0 deletions contracts/remappings.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
@openzeppelin/contracts/=lib/openzeppelin-contracts/contracts/
16 changes: 16 additions & 0 deletions contracts/script/Deployer.s.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;

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

contract Deployer is Script {
function setUp() public {}

function run() public {
vm.startBroadcast();
new Nodes();

vm.broadcast();
}
}
14 changes: 14 additions & 0 deletions contracts/src/GroupMessages.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;

contract GroupMessages {
event MessageSent(bytes32 groupId, bytes message, uint64 sequenceId);

uint64 sequenceId;

function addMessage(bytes32 groupId, bytes memory message) public {
sequenceId++;

emit MessageSent(groupId, message, sequenceId);
}
}
91 changes: 91 additions & 0 deletions contracts/src/Nodes.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;

import "@openzeppelin/contracts/access/Ownable.sol";

contract Nodes is Ownable {
constructor() Ownable(msg.sender) {}

struct Node {
string httpAddress;
uint256 originatorId;
bool isHealthy;
// Maybe we want a TLS cert separate from the public key for MTLS authenticated connections?
}

event NodeUpdate(
bytes publicKey,
string httpAddress,
uint256 originatorId,
bool isHealthy
);

// List of public keys
bytes[] publicKeys;

// Mapping of publicKey to node
mapping(bytes => Node) public nodes;

/**
Add a node to the network
*/
function addNode(
bytes calldata publicKey,
string calldata httpAddress
) public onlyOwner {
require(
bytes(nodes[publicKey].httpAddress).length == 0,
"Node already exists"
);

require(bytes(httpAddress).length != 0, "HTTP address is required");

nodes[publicKey] = Node({
httpAddress: httpAddress,
originatorId: publicKeys.length + 1,
isHealthy: true
});

publicKeys.push(publicKey);

emit NodeUpdate(publicKey, httpAddress, publicKeys.length, true);
}

/**
The contract owner can use this function to mark a node as unhealthy
triggering all other nodes to stop replicating to/from this node
*/
function markNodeUnhealthy(bytes calldata publicKey) public onlyOwner {
require(
bytes(nodes[publicKey].httpAddress).length != 0,
"Node does not exist"
);
nodes[publicKey].isHealthy = false;

emit NodeUpdate(
publicKey,
nodes[publicKey].httpAddress,
nodes[publicKey].originatorId,
false
);
}

/**
The contract owner can use this function to mark a node as healthy
triggering all other nodes to
*/
function markNodeHealthy(bytes calldata publicKey) public onlyOwner {
require(
bytes(nodes[publicKey].httpAddress).length != 0,
"Node does not exist"
);
nodes[publicKey].isHealthy = true;

emit NodeUpdate(
publicKey,
nodes[publicKey].httpAddress,
nodes[publicKey].originatorId,
true
);
}
}
25 changes: 25 additions & 0 deletions contracts/test/GroupMessage.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;

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

contract CounterTest is Test {
GroupMessages public groupMessages;

function setUp() public {
groupMessages = new GroupMessages();
}

function test_AddMessage2kb() public {
bytes32 groupId = bytes32(
0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef
);
bytes memory message = new bytes(1024);
for (uint256 i = 0; i < message.length; i++) {
message[i] = bytes1(uint8(i % 256)); // Set each byte to its index modulo 256
}

groupMessages.addMessage(groupId, message);
}
}
17 changes: 17 additions & 0 deletions dev/abigen
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
#!/bin/bash

set -e

rm -f ./build/*.abi.json
rm -f ./pkg/abis/*.go

cd contracts

# Generate the abi files out of the solidity code
forge inspect ./src/Nodes.sol:Nodes abi > ../build/Nodes.abi.json
forge inspect ./src/GroupMessages.sol:GroupMessages abi > ../build/GroupMessages.abi.json

cd ..
# Generate Go code out of the ABI files
abigen --abi ./build/Nodes.abi.json --pkg abis --type Nodes --out ./pkg/abis/nodes.go
abigen --abi ./build/GroupMessages.abi.json --pkg abis --type GroupMessages --out ./pkg/abis/groupMessages.go
3 changes: 3 additions & 0 deletions dev/contracts/.env
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# This is the first default private key for anvil. Nothing sensitive here.
export PRIVATE_KEY=0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80
export DOCKER_RPC_URL=http://localhost:7545
14 changes: 14 additions & 0 deletions dev/contracts/deploy-local
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
#!/bin/bash
# Deploy the smart contracts to the local anvil node

source dev/contracts/.env

cd ./contracts

# Deploy a contract and save the output (which includes the contract address) to a JSON file to be used in tests
function deploy_contract() {
forge create --legacy --json --rpc-url $DOCKER_RPC_URL --private-key $PRIVATE_KEY "$1:$2" > ../build/$2.json
}

deploy_contract src/GroupMessages.sol GroupMessages
deploy_contract src/Nodes.sol Nodes
5 changes: 5 additions & 0 deletions dev/docker/anvil.Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
FROM ghcr.io/foundry-rs/foundry

WORKDIR /anvil

ENTRYPOINT anvil --host 0.0.0.0
3 changes: 2 additions & 1 deletion dev/docker/compose
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#!/bin/bash

set -e
. dev/docker/env

docker_compose "$@"
docker_compose "$@"
8 changes: 8 additions & 0 deletions dev/docker/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,14 @@ services:
ports:
- 8765:5432

chain:
platform: linux/amd64
build:
context: .
dockerfile: ./anvil.Dockerfile
ports:
- 7545:8545

prometheus:
image: prom/prometheus
ports:
Expand Down
3 changes: 2 additions & 1 deletion dev/docker/down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#!/usr/bin/env sh
#!/usr/bin/env bash
set -e

. dev/docker/env

docker_compose down
1 change: 1 addition & 0 deletions dev/generate
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ set -e

go generate ./...
mockery
./dev/abigen

rm -rf pkg/proto/**/*.pb.go pkg/proto/**/*.pb.gw.go pkg/proto/**/*.swagger.json
if ! buf generate https://github.com/xmtp/proto.git#subdir=proto; then
Expand Down
Loading

0 comments on commit dfcdc77

Please sign in to comment.