Skip to content

Commit

Permalink
Add tool to validate the correctness of the Solidity code snippets (#68)
Browse files Browse the repository at this point in the history
* Add std checker

* Try stuff

* Try again for real

* Try stuff

* Add more stuff
  • Loading branch information
ferranbt authored Mar 27, 2024
1 parent 9d65cba commit 783736c
Show file tree
Hide file tree
Showing 6 changed files with 204 additions and 28 deletions.
26 changes: 26 additions & 0 deletions .github/workflows/check-code-snippets.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
name: Check code snippets
on:
push:
branches:
- main
pull_request:

env:
FOUNDRY_PROFILE: ci

jobs:
check-code-snippets:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2

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

- name: Install deps
run: forge install

- name: Validate code snippets in README
run: go run tools/stdchecker/main.go --root ./tools/stdchecker ./README.md
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@ cache/
out/
src-forge-test/
docs/
suave-std-gen/
suave-std-gen/
repo-src/
52 changes: 25 additions & 27 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,36 +24,35 @@ Encode an `EIP155` transaction:
import "suave-std/Transactions.sol";
contract Example {
function example() {
Transactions.EIP155Request memory txn0 = Transactions.EIP155Request({
to: address(0x095E7BAea6a6c7c4c2DfeB977eFac326aF552d87),
gas: 50000,
gasPrice: 10,
value: 10,
...
});
function example() public {
Transactions.EIP155 memory txn0;
// fill the transaction fields
// legacyTxn0.to = ...
// legacyTxn0.gas = ...
// Encode to RLP
bytes memory rlp = Transactions.encodeRLP(txn0);
// Decode from RLP
Transactions.Legacy memory txn = Transactions.decodeRLP(rlp);
Transactions.EIP155 memory txn = Transactions.decodeRLP_EIP155(rlp);
}
}
```

Sign an `EIP-1559` transaction:

```
```solidity
import "suave-std/Transactions.sol";
contract Example {
function example() {
function example() public {
string memory signingKey = "b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291";
Transactions.EIP1559Request memory txnrequest = Transactions.EIP1559Request({
...
})
Transactions.EIP1559Request memory txnRequest;
txnRequest.to = address(0x095E7BAea6a6c7c4c2DfeB977eFac326aF552d87);
txnRequest.gas = 50000;
txnRequest.maxPriorityFeePerGas = 10;
// ...
Transactions.EIP1559 memory signedTxn = Transactions.signTxn(txnRequest, signingKey);
}
Expand All @@ -75,7 +74,7 @@ Available functions:
import "suave-std/Context.sol";
contract Example {
function example() {
function example() public {
bytes memory inputs = Context.confidentialInputs();
address kettle = Context.kettleAddress();
}
Expand All @@ -90,18 +89,23 @@ Helper library to send bundle requests with the Mev-Share protocol.

```solidity
import "suave-std/protocols/MevShare.sol";
import "suave-std/Transactions.sol";
contract Example {
function example() {
Transactions.Legacy memory legacyTxn0 = Transactions.Legacy({});
function example() public {
Transactions.EIP155 memory legacyTxn0;
// fill the transaction fields
// legacyTxn0.to = ...
// legacyTxn0.gas = ...
bytes memory rlp = Transactions.encodeRLP(legacyTxn0);
MevShare.Bundle memory bundle;
bundle.bodies = new bytes[](1);
bundle.bodies[0] = rlp;
// ...
MevShare.sendBundle(bundle);
MevShare.sendBundle("http://<relayer-url>", bundle);
}
}
```
Expand All @@ -116,7 +120,7 @@ Helper library to interact with the Ethereum JsonRPC protocol.
import "suave-std/protocols/EthJsonRPC.sol";
contract Example {
function example() {
function example() public {
EthJsonRPC jsonrpc = new EthJsonRPC("http://...");
jsonrpc.nonce(address(this));
}
Expand All @@ -131,7 +135,7 @@ Helper library to send completion requests to ChatGPT.
import "suave-std/protocols/ChatGPT.sol";
contract Example {
function example() {
function example() public {
ChatGPT chatgpt = new ChatGPT("apikey");
ChatGPT.Message[] memory messages = new ChatGPT.Message[](1);
Expand All @@ -155,12 +159,9 @@ $ suave --suave.dev
Then, your `forge` scripts/test must import the `SuaveEnabled` contract from the `suave-std/Test.sol` file.

```solidity
// SPDX-License-Identifier: Unlicense
pragma solidity ^0.8.13;
import "forge-std/Test.sol";
import "suave-std/Test.sol";
import "suave-std/Suave.sol";
import "suave-std/suavelib/Suave.sol";
contract TestForge is Test, SuaveEnabled {
address[] public addressList = [0xC8df3686b4Afb2BB53e60EAe97EF043FE03Fb829];
Expand All @@ -182,9 +183,6 @@ contract TestForge is Test, SuaveEnabled {
Use the `setConfidentialInputs` function to set the confidential inputs during tests.

```solidity
// SPDX-License-Identifier: Unlicense
pragma solidity ^0.8.13;
import "forge-std/Test.sol";
import "src/Test.sol";
import "src/suavelib/Suave.sol";
Expand Down
4 changes: 4 additions & 0 deletions tools/stdchecker/foundry.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
[profile.default]
src = "repo-src"
solc_version = "0.8.23"
include_paths = ["../../"]
142 changes: 142 additions & 0 deletions tools/stdchecker/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
package main

import (
"bytes"
"flag"
"fmt"
"io/fs"
"log"
"os"
"os/exec"
"path/filepath"
"regexp"
)

var (
inputFolder string
rootFolder string
)

func main() {
flag.StringVar(&rootFolder, "root", "./", "root folder of stdchecker")
flag.Parse()
args := flag.Args()

if len(args) != 1 {
log.Fatal("Usage: stdchecker <suave-std-folder>")
}

inputFolder = args[0]

snippets := readTargets(inputFolder)
writeSnippets(snippets)

// use forge to build the snippets
_, err := execForgeCommand([]string{
"build",
"--root", rootFolder,
})
if err != nil {
log.Fatal(err)
}
}

func writeSnippets(snippets [][]byte) {
// remove the destination folder first
if err := os.RemoveAll(filepath.Join(rootFolder, "repo-src")); err != nil {
log.Fatal(err)
}

for indx, snippet := range snippets {
dst := filepath.Join(rootFolder, fmt.Sprintf("repo-src/snippet_%d.sol", indx))

abs := filepath.Dir(dst)
if err := os.MkdirAll(abs, 0755); err != nil {
log.Fatal(err)
}

if err := os.WriteFile(dst, []byte(snippet), 0755); err != nil {
log.Fatal(err)
}
}
}

func readTargets(target string) [][]byte {
// if the target is a file, read the content and extract the Solidity code blocks
// if the target is a folder, read all the files in the folder and extract the Solidity code blocks
info, err := os.Stat(target)
if err != nil {
log.Fatal(err)
}

markdownFiles := []string{}
if info.IsDir() {
filepath.WalkDir(target, func(path string, d fs.DirEntry, err error) error {
if err != nil {
log.Fatal(err)
}
if d.IsDir() {
return nil
}

if filepath.Ext(path) != ".md" {
return nil
}
markdownFiles = append(markdownFiles, path)
return nil
})
} else {
markdownFiles = append(markdownFiles, target)
}

// Regular expression to match Solidity code blocks
re := regexp.MustCompile("```solidity\\s+(?s)(.*?)```")

snippets := [][]byte{}
for _, file := range markdownFiles {
content, err := os.ReadFile(file)
if err != nil {
log.Fatal(err)
}

// Find all matches
matches := re.FindAllSubmatch(content, -1)

for _, match := range matches {
snippet := match[1]
if bytes.Contains(snippet, []byte("[skip-check]")) {
// skip if the snippet contains the tag [skip-check]
continue
}
snippets = append(snippets, snippet)
}
}

if len(snippets) == 0 {
log.Fatal("No Solidity code blocks found in the target")
}
log.Printf("Found %d Solidity code blocks", len(snippets))
return snippets
}

func execForgeCommand(args []string) (string, error) {
_, err := exec.LookPath("forge")
if err != nil {
return "", fmt.Errorf("forge command not found in PATH: %v", err)
}

// Create a command to run the forge command
cmd := exec.Command("forge", args...)

// Set up output buffer
var outBuf, errBuf bytes.Buffer
cmd.Stdout = &outBuf
cmd.Stderr = &errBuf

// Run the command
if err := cmd.Run(); err != nil {
return "", fmt.Errorf("error running command: %v, %s", err, errBuf.String())
}

return outBuf.String(), nil
}
5 changes: 5 additions & 0 deletions tools/stdchecker/remappings.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
suave-std/=../../src/
Solidity-RLP/=../../lib/Solidity-RLP/contracts/
ds-test/=../../lib/forge-std/lib/ds-test/src/
forge-std/=../../lib/forge-std/src/
solady/=../../lib/solady/

0 comments on commit 783736c

Please sign in to comment.