From 783736ce02eb7d2e51f19457fd33450711c7af65 Mon Sep 17 00:00:00 2001 From: Ferran Borreguero Date: Wed, 27 Mar 2024 08:32:13 +0000 Subject: [PATCH] Add tool to validate the correctness of the Solidity code snippets (#68) * Add std checker * Try stuff * Try again for real * Try stuff * Add more stuff --- .github/workflows/check-code-snippets.yml | 26 ++++ .gitignore | 3 +- README.md | 52 ++++---- tools/stdchecker/foundry.toml | 4 + tools/stdchecker/main.go | 142 ++++++++++++++++++++++ tools/stdchecker/remappings.txt | 5 + 6 files changed, 204 insertions(+), 28 deletions(-) create mode 100644 .github/workflows/check-code-snippets.yml create mode 100644 tools/stdchecker/foundry.toml create mode 100644 tools/stdchecker/main.go create mode 100644 tools/stdchecker/remappings.txt diff --git a/.github/workflows/check-code-snippets.yml b/.github/workflows/check-code-snippets.yml new file mode 100644 index 0000000..5f173d4 --- /dev/null +++ b/.github/workflows/check-code-snippets.yml @@ -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 diff --git a/.gitignore b/.gitignore index e797ee5..e21b7bf 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,5 @@ cache/ out/ src-forge-test/ docs/ -suave-std-gen/ \ No newline at end of file +suave-std-gen/ +repo-src/ \ No newline at end of file diff --git a/README.md b/README.md index 4a66973..280172c 100644 --- a/README.md +++ b/README.md @@ -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); } @@ -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(); } @@ -90,10 +89,15 @@ 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; @@ -101,7 +105,7 @@ contract Example { bundle.bodies[0] = rlp; // ... - MevShare.sendBundle(bundle); + MevShare.sendBundle("http://", bundle); } } ``` @@ -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)); } @@ -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); @@ -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]; @@ -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"; diff --git a/tools/stdchecker/foundry.toml b/tools/stdchecker/foundry.toml new file mode 100644 index 0000000..6ff4100 --- /dev/null +++ b/tools/stdchecker/foundry.toml @@ -0,0 +1,4 @@ +[profile.default] +src = "repo-src" +solc_version = "0.8.23" +include_paths = ["../../"] diff --git a/tools/stdchecker/main.go b/tools/stdchecker/main.go new file mode 100644 index 0000000..74038b6 --- /dev/null +++ b/tools/stdchecker/main.go @@ -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 ") + } + + 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 +} diff --git a/tools/stdchecker/remappings.txt b/tools/stdchecker/remappings.txt new file mode 100644 index 0000000..94c25ce --- /dev/null +++ b/tools/stdchecker/remappings.txt @@ -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/