Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add tool to validate the correctness of the Solidity code snippets #68

Merged
merged 5 commits into from
Mar 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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/
54 changes: 26 additions & 28 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,17 +183,14 @@ 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";

contract TestForge is Test, SuaveEnabled {
function testConfidentialInputs() public {
bytes memory input = hex"abcd";
setConfidentialInputs(input);
ctx.setConfidentialInputs(input);

bytes memory found2 = Suave.confidentialInputs();
assertEq0(input, found2);
Expand Down
2 changes: 1 addition & 1 deletion src/protocols/ChatGPT.sol
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// SPDX-License-Identifier: Unlicense
pragma solidity ^0.8.13;

import "src/suavelib/Suave.sol";
import "../suavelib/Suave.sol";
import "solady/src/utils/JSONParserLib.sol";

/// @notice ChatGPT is a library with utilities to interact with the OpenAI ChatGPT API.
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/