diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4f12656e..2446b75a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -19,9 +19,6 @@ jobs: with: submodules: "recursive" - - name: "Install Foundry" - uses: "foundry-rs/foundry-toolchain@v1" - - name: "Install Node.js" uses: "actions/setup-node@v3" with: @@ -38,7 +35,7 @@ jobs: echo "## Lint result" >> $GITHUB_STEP_SUMMARY echo "✅ Passed" >> $GITHUB_STEP_SUMMARY - build: + proposal-simulator-test: runs-on: "ubuntu-latest" steps: - name: "Check out the repo" @@ -49,16 +46,29 @@ jobs: - name: "Install Foundry" uses: "foundry-rs/foundry-toolchain@v1" + - name: "Show the Foundry config" + run: "forge config" + + - name: + "Generate a fuzz seed that changes weekly to avoid burning through RPC + allowance" + run: > + echo "FOUNDRY_FUZZ_SEED=$( + echo $(($EPOCHSECONDS - $EPOCHSECONDS % 604800)) + )" >> $GITHUB_ENV + - name: "Build the contracts and print their size" run: "forge build --sizes" - - name: "Add build summary" + - name: "Run the tests" + run: "forge test --nmc TypeCheck" + + - name: "Add test summary" run: | - echo "## Build result" >> $GITHUB_STEP_SUMMARY + echo "## Proposal simulator tests result" >> $GITHUB_STEP_SUMMARY echo "✅ Passed" >> $GITHUB_STEP_SUMMARY - test: - needs: ["lint", "build"] + type-check-test: runs-on: "ubuntu-latest" steps: - name: "Check out the repo" @@ -71,19 +81,19 @@ jobs: - name: "Show the Foundry config" run: "forge config" + + - name: "Install Node.js" + uses: "actions/setup-node@v3" + with: + node-version: "lts/*" - - name: - "Generate a fuzz seed that changes weekly to avoid burning through RPC - allowance" - run: > - echo "FOUNDRY_FUZZ_SEED=$( - echo $(($EPOCHSECONDS - $EPOCHSECONDS % 604800)) - )" >> $GITHUB_ENV + - name: "Install the Node.js dependencies" + run: "cd typescript/ && npm install && cd ../" - - name: "Run the tests" - run: "forge test" + - name: "Run typecheck tests" + run: "forge test --mc TypeCheck --ffi --fork-url sepolia" - name: "Add test summary" run: | - echo "## Tests result" >> $GITHUB_STEP_SUMMARY + echo "## Type check tests result" >> $GITHUB_STEP_SUMMARY echo "✅ Passed" >> $GITHUB_STEP_SUMMARY diff --git a/.gitignore b/.gitignore index 6e0869f2..ac225a4b 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,8 @@ node_modules/ .env lcov.info + +# typescript +typescript/node_modules/ + +typescript/package-lock.json \ No newline at end of file diff --git a/README.md b/README.md index db1af188..4d673eda 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,9 @@ For guidance on tool usage, please read the [documentation](https://solidity-lab ## Usage -### Step 1: Install +### Proposal Simulation + +#### Step 1: Install Add `forge-proposal-simulator` to your project using Forge: @@ -14,7 +16,7 @@ Add `forge-proposal-simulator` to your project using Forge: forge install https://github.com/solidity-labs-io/forge-proposal-simulator.git ``` -### Step 2: Set Remappings +#### Step 2: Set Remappings Update your remappings.txt to include: @@ -22,7 +24,7 @@ Update your remappings.txt to include: @forge-proposal-simulator=lib/forge-proposal-simulator/ ``` -### Step 3: Create Addresses File +#### Step 3: Create Addresses File Create a JSON file following the instructions provided in [Addresses.md](docs/overview/architecture/addresses.md). We recommend keeping the @@ -35,7 +37,7 @@ Once the file is created, be sure to allow read access to `addresses.json` insid fs_permissions = [{ access = "read", path = "./addresses/addresses.json"}] ``` -### Step 4: Create a Proposal +#### Step 4: Create a Proposal Choose a model that fits your needs: @@ -43,10 +45,82 @@ Choose a model that fits your needs: - [Timelock Proposal](docs/guides/timelock-proposal.md) - [Governor Bravo Proposal](docs/guides/governor-bravo-proposal.md) -### Step 5: Implement Scripts and Tests +#### Step 5: Implement Scripts and Tests Create scripts and/or tests. Check [Guides](docs/guides/multisig-proposal.md) and [Integration Tests](docs/testing/integration-tests.md). +### Type Checking + +Type checking allows verification of deployed bytecode on any contracts with the bytecode present in local artifacts. With this feature, developer `A` can easily deploy some contracts, and developer `B` can verify `A`'s deployments by simply running the type checking script. Additionaly, `A` can also use this feature to verify their own deployments. `A` can take the following steps: + +- Follow the steps 1 to 3 on [Proposal Simulation](#proposal-simulation) section +- Add the deployed contracts to `Addresses.json`. +- Create a `TypeCheckAddresses.json` file following the instructions provided in [type-check.md](docs/guides/type-check.md). +- Enter `lib/forge-proposal-simulator/typescript` directory and install npm packages. + +```bash +cd lib/forge-proposal-simulator/typescript && npm i +``` + +- Change directory again to the root repo + +```bash +cd ../../../ +``` + +- Add below environment variables to `.env`. + +``` +ADDRESSES_PATH # Path to addresses.json file +TYPE_CHECK_ADDRESSES_PATH # Path to typeCheckAddresses.json file +ARTIFACT_PATH # Path of artifact folder +``` + +Example: + +``` +TYPE_CHECK_ADDRESSES_PATH=addresses/TypeCheckAddresses.json +ADDRESSES_PATH=addresses/Addresses.json +ARTIFACT_PATH=out/ +``` + +- Make sure to allow read access to `Addresses.json`, `TypeCheckAddresses.json` and `artifact` folder inside of `foundry.toml`. + +```toml +[profile.default] + +fs_permissions = [{ access = "read", path = "./"}] +``` + +- Run the following command on root repo to type check all contracts added in `TypeCheckAddresses.json`. + +```bash +forge script lib/forge-proposal-simulator/script/TypeCheck.s.sol:TypeCheck --ffi --fork-url +``` + +### Type checking on Example contracts FPS + +#### Step 1: Set enviornment variables + +``` +TYPE_CHECK_ADDRESSES_PATH="addresses/TypeCheckAddresses.json" +ADDRESSES_PATH="addresses/Addresses.json" +ARTIFACT_PATH="out/" +LIB_PATH="" +``` + +#### Step 2: Run script to test type checking + +```bash +forge script script/TypeCheck.s.sol:TypeCheck --ffi --fork-url sepolia +``` + +You can also run type checking through inline environment variables if not already set in .env + +```bash +TYPE_CHECK_ADDRESSES_PATH="addresses/TypeCheckAddresses.json" ADDRESSES_PATH="addresses/Addresses.json" ARTIFACT_PATH="out/" LIB_PATH="" forge script script/TypeCheck.s.sol:TypeCheck --ffi --fork-url sepolia +``` + ## Contribute There are many ways you can participate and help build the next version of FPS. Check out the [contribution guide](CONTRIBUTING.md)! diff --git a/addresses/Addresses.json b/addresses/Addresses.json index deec4c6a..66bae753 100644 --- a/addresses/Addresses.json +++ b/addresses/Addresses.json @@ -52,6 +52,17 @@ "name": "BRAVO_PROPOSER", "chainId": 31337, "isContract": false - + }, + { + "addr": "0x293cad26033577eb68137603a34d2dbfd05104d8", + "chainId": 11155111, + "name": "ExampleTypeCheck", + "isContract": true + }, + { + "addr": "0x9D7A602541230B2bA04D9aA542fad73B8aaBA4ED", + "chainId": 11155111, + "name": "ExampleTypeCheck_02", + "isContract": true } -] +] \ No newline at end of file diff --git a/addresses/AddressesIncorrect.json b/addresses/AddressesIncorrect.json new file mode 100644 index 00000000..071a7b73 --- /dev/null +++ b/addresses/AddressesIncorrect.json @@ -0,0 +1,14 @@ +[ + { + "addr": "0x8FB9B57A1Af027ca5f4EF262280fFF880c22D371", + "chainId": 11155111, + "name": "ExampleTypeCheck", + "isContract": true + }, + { + "addr": "0x8FB9B57A1Af027ca5f4EF262280fFF880c22D371", + "chainId": 11155111, + "name": "ExampleTypeCheck_02", + "isContract": true + } +] \ No newline at end of file diff --git a/addresses/TypeCheckAddresses.json b/addresses/TypeCheckAddresses.json new file mode 100644 index 00000000..121ca9f2 --- /dev/null +++ b/addresses/TypeCheckAddresses.json @@ -0,0 +1,12 @@ +[ + { + "name": "ExampleTypeCheck", + "constructorArgs": "[[\"0x95222290DD7278Aa3Ddd389Cc1E1d165CC4BAfe5\", \"0x95222290dd7278aa3ddd389cc1e1d165cc4bafe5000000000000000000000000\", [\"0x95222290DD7278Aa3Ddd389Cc1E1d165CC4BAfe5\", 2, [\"0x95222290DD7278Aa3Ddd389Cc1E1d165CC4BAfe5\", 2]]], [\"Arg1\", \"Arg2\"], [2, 3], [[\"0x95222290DD7278Aa3Ddd389Cc1E1d165CC4BAfe5\", 2, [\"0x95222290DD7278Aa3Ddd389Cc1E1d165CC4BAfe5\", 2]], [\"0x95222290DD7278Aa3Ddd389Cc1E1d165CC4BAfe5\", 2, [\"0x95222290DD7278Aa3Ddd389Cc1E1d165CC4BAfe5\", 2]]]]", + "artifactPath": "ExampleTypeCheck.sol:ExampleTypeCheck" + }, + { + "name": "ExampleTypeCheck_02", + "constructorArgs": "[[[1, 2], [3, 4]], [[[1, 2]], [[3, 4]]], [[[[[1, 2]], [[3, 4]]]], [[[[5, 6]], [[7, 8]]]]], [[\"0x95222290DD7278Aa3Ddd389Cc1E1d165CC4BAfe5\"], [\"0x95222290DD7278Aa3Ddd389Cc1E1d165CC4BAfe5\"]]]", + "artifactPath": "ExampleTypeCheck_02.sol:ExampleTypeCheck_02" + } +] \ No newline at end of file diff --git a/addresses/TypeCheckAddressesIncorrect.json b/addresses/TypeCheckAddressesIncorrect.json new file mode 100644 index 00000000..b33b08f2 --- /dev/null +++ b/addresses/TypeCheckAddressesIncorrect.json @@ -0,0 +1,12 @@ +[ + { + "name": "ExampleTypeCheck", + "constructorArgs": "[[\"0x95222290DD7278Aa3Ddd389Cc1E1d165CC4BAfe5\", \"0x95222290dd7278aa3ddd389cc1e1d165cc4bafe5000000000000000000000000\", [\"0x95222290DD7278Aa3Ddd389Cc1E1d165CC4BAfe5\", 2, [\"0x95222290DD7278Aa3Ddd389Cc1E1d165CC4BAfe5\", 2]]], [\"Arg1\", \"Arg2\"], [2, 3], [[\"0x95222290DD7278Aa3Ddd389Cc1E1d165CC4BAfe5\", 2, [\"0x95222290DD7278Aa3Ddd389Cc1E1d165CC4BAfe5\", 2]], [\"0x95222290DD7278Aa3Ddd389Cc1E1d165CC4BAfe5\", 2, [\"0x95222290DD7278Aa3Ddd389Cc1E1d165CC4BAfe5\", 2]]]]", + "artifactPath": "ExampleTypeCheck_02.sol:ExampleTypeCheck_02" + }, + { + "name": "ExampleTypeCheck_02", + "constructorArgs": "[[[1, 2], [3, 4]], [[[1, 2]], [[3, 4]]], [[[[[1, 2]], [[3, 4]]]], [[[[5, 6]], [[7, 8]]]]], [[\"0x95222290DD7278Aa3Ddd389Cc1E1d165CC4BAfe5\"], [\"0x95222290DD7278Aa3Ddd389Cc1E1d165CC4BAfe5\"]]]", + "artifactPath": "ExampleTypeCheck.sol:ExampleTypeCheck" + } +] \ No newline at end of file diff --git a/docs/SUMMARY.md b/docs/SUMMARY.md index 3bc965e1..bcd093c0 100644 --- a/docs/SUMMARY.md +++ b/docs/SUMMARY.md @@ -25,3 +25,9 @@ ## Testing - [Integration Tests](testing/integration-tests.md) + +## Type checking + +- [Introduction](type-check/introduction.md) +- [Type Check Example](type-check/example.md) +- [TypeCheckAddresses.json](type-check/type-check.md) diff --git a/docs/type-check/example.md b/docs/type-check/example.md new file mode 100644 index 00000000..1a8da54f --- /dev/null +++ b/docs/type-check/example.md @@ -0,0 +1,108 @@ +# Example Type check + +After adding FPS into project dependencies and setting up through [Setup guide](./introduction.md#setting-up), its time to go through an example contract to understand how type checking works. + +## Example Contracts + +The [folder](../../src/type-check) includes contracts used in the guide mentioned +below for demonstration purposes. Examples include [ExampleTypeCheck.sol](../../src/type-check/ExampleTypeCheck.sol) +and [ExampleTypeCheck_02.sol](../../src/type-check/ExampleTypeCheck_02.sol). + +Add file `ExampleTypeCheck.sol` in `src` of root repository: + +```solidity +// SPDX-License-Identifier: GPL-3.0 + +pragma solidity =0.8.19; + +contract ExampleTypeCheck { + struct StructC { + string varC1; + uint256 varC2; + } + + struct StructB { + bytes varB1; + uint256 varB2; + StructC structC; + } + struct StructA { + address varA1; + bytes32 varA2; + StructB structB; + } + + constructor( + StructA memory structA, + string[] memory arg1, + uint256[] memory arg2, + StructB[] memory structb + ) {} + + function encode( + StructA memory structA, + string[] memory arg1, + uint256[] memory arg2, + StructB[] memory structb + ) external pure returns (bytes memory) { + return abi.encode(structA, arg1, arg2, structb); + } +} +``` + +The contract constructor arguments looks complex for this example, luckily, we already have the constructor arguments ready for you. + +In the file `TypeCheckAddresses.json` that you have already made in setup, add another object in json file for the above contract: + +```json +{ + "name": "ExampleTypeCheck", + "constructorArgs": "[[\"0x95222290DD7278Aa3Ddd389Cc1E1d165CC4BAfe5\", \"0x95222290dd7278aa3ddd389cc1e1d165cc4bafe5000000000000000000000000\", [\"0x95222290DD7278Aa3Ddd389Cc1E1d165CC4BAfe5\", 2, [\"0x95222290DD7278Aa3Ddd389Cc1E1d165CC4BAfe5\", 2]]], [\"Arg1\", \"Arg2\"], [2, 3], [[\"0x95222290DD7278Aa3Ddd389Cc1E1d165CC4BAfe5\", 2, [\"0x95222290DD7278Aa3Ddd389Cc1E1d165CC4BAfe5\", 2]], [\"0x95222290DD7278Aa3Ddd389Cc1E1d165CC4BAfe5\", 2, [\"0x95222290DD7278Aa3Ddd389Cc1E1d165CC4BAfe5\", 2]]]]", + "artifactPath": "ExampleTypeCheck.sol:ExampleTypeCheck" +} +``` + +Now add the below object in `Addresses.json`: + +```json +{ + "addr": "0x293cad26033577eb68137603a34d2dbfd05104d8", + "chainId": 11155111, + "name": "ExampleTypeCheck", + "isContract": true + }, +``` + +The above contract is already deployed on sepolia testnet and we will ensure the correct contract is deployed by verifying bytecode of sepolia contract with the local bytecode of `ExampleTypeCheck` contract. + +Assuming you have already installed npm packages and added environment variables in [Setup guide](./introduction.md#setting-up). + +Make sure to allow read access to `Addresses.json`, `TypeCheckAddresses.json` and `artifact` folder inside of `foundry.toml`. + +```toml +[profile.default] + +fs_permissions = [{ access = "read", path = "./"}] +``` + +Run the following command on the root repo to type check all contracts added in `TypeCheckAddresses.json`. + +```bash +forge script lib/forge-proposal-simulator/script/TypeCheck.s.sol:TypeCheck --ffi --fork-url "https://ethereum-sepolia-rpc.publicnode.com" +``` + +The script should run successfully that the deployed contract is correct. The command should create the following output, as shown below: + +```txt +Script ran successfully. + +== Logs == + +Contract: ExampleTypeCheck + Encoded contructor args : + 0x000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000002200000000000000000000000000000000000000000000000000000000000000300000000000000000000000000000000000000000000000000000000000000036000000000000000000000000095222290dd7278aa3ddd389cc1e1d165cc4bafe595222290dd7278aa3ddd389cc1e1d165cc4bafe500000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000000000001495222290dd7278aa3ddd389cc1e1d165cc4bafe500000000000000000000000000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000002a3078393532323232393044443732373841613344646433383943633145316431363543433442416665350000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000044172673100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000441726732000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000030000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000001800000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000000000001495222290dd7278aa3ddd389cc1e1d165cc4bafe500000000000000000000000000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000002a307839353232323239304444373237384161334464643338394363314531643136354343344241666535000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000000000001495222290dd7278aa3ddd389cc1e1d165cc4bafe500000000000000000000000000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000002a30783935323232323930444437323738416133446464333839436331453164313635434334424166653500000000000000000000000000000000000000000000 + Local deployed bytecode: + 0x608060405234801561001057600080fd5b506004361061002b5760003560e01c806325b3caa214610030575b600080fd5b61004361003e3660046103f6565b610059565b6040516100509190610540565b60405180910390f35b606084848484604051602001610072949392919061063d565b6040516020818303038152906040529050949350505050565b634e487b7160e01b600052604160045260246000fd5b6040516060810167ffffffffffffffff811182821017156100c4576100c461008b565b60405290565b6040805190810167ffffffffffffffff811182821017156100c4576100c461008b565b604051601f8201601f1916810167ffffffffffffffff811182821017156101165761011661008b565b604052919050565b600067ffffffffffffffff8311156101385761013861008b565b61014b601f8401601f19166020016100ed565b905082815283838301111561015f57600080fd5b828260208301376000602084830101529392505050565b600082601f83011261018757600080fd5b6101968383356020850161011e565b9392505050565b6000606082840312156101af57600080fd5b6101b76100a1565b9050813567ffffffffffffffff808211156101d157600080fd5b818401915084601f8301126101e557600080fd5b6101f48583356020850161011e565b835260208401356020840152604084013591508082111561021457600080fd5b908301906040828603121561022857600080fd5b6102306100ca565b82358281111561023f57600080fd5b61024b87828601610176565b8252506020830135602082015280604085015250505092915050565b600067ffffffffffffffff8211156102815761028161008b565b5060051b60200190565b600082601f83011261029c57600080fd5b813560206102b16102ac83610267565b6100ed565b82815260059290921b840181019181810190868411156102d057600080fd5b8286015b8481101561031057803567ffffffffffffffff8111156102f45760008081fd5b6103028986838b0101610176565b8452509183019183016102d4565b509695505050505050565b600082601f83011261032c57600080fd5b8135602061033c6102ac83610267565b82815260059290921b8401810191818101908684111561035b57600080fd5b8286015b84811015610310578035835291830191830161035f565b600082601f83011261038757600080fd5b813560206103976102ac83610267565b82815260059290921b840181019181810190868411156103b657600080fd5b8286015b8481101561031057803567ffffffffffffffff8111156103da5760008081fd5b6103e88986838b010161019d565b8452509183019183016103ba565b6000806000806080858703121561040c57600080fd5b843567ffffffffffffffff8082111561042457600080fd5b908601906060828903121561043857600080fd5b6104406100a1565b82356001600160a01b038116811461045757600080fd5b81526020838101359082015260408301358281111561047557600080fd5b6104818a82860161019d565b6040830152509550602087013591508082111561049d57600080fd5b6104a98883890161028b565b945060408701359150808211156104bf57600080fd5b6104cb8883890161031b565b935060608701359150808211156104e157600080fd5b506104ee87828801610376565b91505092959194509250565b6000815180845260005b8181101561052057602081850181015186830182015201610504565b506000602082860101526020601f19601f83011685010191505092915050565b60208152600061019660208301846104fa565b600081516060845261056860608501826104fa565b9050602083015160208501526040830151848203604086015280516040835261059460408401826104fa565b6020928301519390920192909252949350505050565b600081518084526020808501945080840160005b838110156105da578151875295820195908201906001016105be565b509495945050505050565b600082825180855260208086019550808260051b84010181860160005b8481101561063057601f1986840301895261061e838351610553565b98840198925090830190600101610602565b5090979650505050505050565b608080825285516001600160a01b03169082015260208086015160a08301526040860151606060c08401526000919061067960e0850182610553565b9050838103828501528087518083528383019150838160051b840101848a0160005b838110156106c957601f198684030185526106b78383516104fa565b9487019492509086019060010161069b565b505086810360408801526106dd818a6105aa565b94505050505082810360608401526106f581856105e5565b97965050505050505056fe + Deployed bytecode: + 0x608060405234801561001057600080fd5b506004361061002b5760003560e01c806325b3caa214610030575b600080fd5b61004361003e3660046103f6565b610059565b6040516100509190610540565b60405180910390f35b606084848484604051602001610072949392919061063d565b6040516020818303038152906040529050949350505050565b634e487b7160e01b600052604160045260246000fd5b6040516060810167ffffffffffffffff811182821017156100c4576100c461008b565b60405290565b6040805190810167ffffffffffffffff811182821017156100c4576100c461008b565b604051601f8201601f1916810167ffffffffffffffff811182821017156101165761011661008b565b604052919050565b600067ffffffffffffffff8311156101385761013861008b565b61014b601f8401601f19166020016100ed565b905082815283838301111561015f57600080fd5b828260208301376000602084830101529392505050565b600082601f83011261018757600080fd5b6101968383356020850161011e565b9392505050565b6000606082840312156101af57600080fd5b6101b76100a1565b9050813567ffffffffffffffff808211156101d157600080fd5b818401915084601f8301126101e557600080fd5b6101f48583356020850161011e565b835260208401356020840152604084013591508082111561021457600080fd5b908301906040828603121561022857600080fd5b6102306100ca565b82358281111561023f57600080fd5b61024b87828601610176565b8252506020830135602082015280604085015250505092915050565b600067ffffffffffffffff8211156102815761028161008b565b5060051b60200190565b600082601f83011261029c57600080fd5b813560206102b16102ac83610267565b6100ed565b82815260059290921b840181019181810190868411156102d057600080fd5b8286015b8481101561031057803567ffffffffffffffff8111156102f45760008081fd5b6103028986838b0101610176565b8452509183019183016102d4565b509695505050505050565b600082601f83011261032c57600080fd5b8135602061033c6102ac83610267565b82815260059290921b8401810191818101908684111561035b57600080fd5b8286015b84811015610310578035835291830191830161035f565b600082601f83011261038757600080fd5b813560206103976102ac83610267565b82815260059290921b840181019181810190868411156103b657600080fd5b8286015b8481101561031057803567ffffffffffffffff8111156103da5760008081fd5b6103e88986838b010161019d565b8452509183019183016103ba565b6000806000806080858703121561040c57600080fd5b843567ffffffffffffffff8082111561042457600080fd5b908601906060828903121561043857600080fd5b6104406100a1565b82356001600160a01b038116811461045757600080fd5b81526020838101359082015260408301358281111561047557600080fd5b6104818a82860161019d565b6040830152509550602087013591508082111561049d57600080fd5b6104a98883890161028b565b945060408701359150808211156104bf57600080fd5b6104cb8883890161031b565b935060608701359150808211156104e157600080fd5b506104ee87828801610376565b91505092959194509250565b6000815180845260005b8181101561052057602081850181015186830182015201610504565b506000602082860101526020601f19601f83011685010191505092915050565b60208152600061019660208301846104fa565b600081516060845261056860608501826104fa565b9050602083015160208501526040830151848203604086015280516040835261059460408401826104fa565b6020928301519390920192909252949350505050565b600081518084526020808501945080840160005b838110156105da578151875295820195908201906001016105be565b509495945050505050565b600082825180855260208086019550808260051b84010181860160005b8481101561063057601f1986840301895261061e838351610553565b98840198925090830190600101610602565b5090979650505050505050565b608080825285516001600160a01b03169082015260208086015160a08301526040860151606060c08401526000919061067960e0850182610553565b9050838103828501528087518083528383019150838160051b840101848a0160005b838110156106c957601f198684030185526106b78383516104fa565b9487019492509086019060010161069b565b505086810360408801526106dd818a6105aa565b94505050505082810360608401526106f581856105e5565b97965050505050505056fe +``` diff --git a/docs/type-check/introduction.md b/docs/type-check/introduction.md new file mode 100644 index 00000000..c821be0f --- /dev/null +++ b/docs/type-check/introduction.md @@ -0,0 +1,78 @@ +# Type checking + +Type checking enables validation of deployed bytecode against local artifacts, ensuring accuracy. This facilitates +developer `A`'s contract deployment, with developer `B` verifying `A`'s deployments with the type checking script. +Additionally, the deploying developer can verify the deployed contract themselves, streamlining the process. Developer `A` can initiate verification by deploying the contract locally +through Foundry, then compare their generated bytecode with the already on-chain version. +By leveraging type checking, developers can efficiently validate contracts, enhancing transparency and security in their Solidity smart contract development workflows. + +## Setting up + +## Step 1: Add Dependency + +Add `forge-proposal-simulator` to your project using Forge: + +```sh +forge install https://github.com/solidity-labs-io/forge-proposal-simulator.git +``` + +## Step 2: Remapping + +Update your remappings.txt to include: + +```txt +@forge-proposal-simulator=lib/forge-proposal-simulator/ +``` + +## Step 3: Addresses File + +Create a JSON file following the structured defined in +[Addresses](../overview/architecture/addresses.md). We recommend keeping the +addresses file in a separate folder, for example `./addresses/addresses.json`. +Add all contracts to `Addresses.json`. + +## Step 4: TypeCheckAddresses File + +Create a `TypeCheckAddresses.json` file following the instructions provided in [type-check.md](./type-check.md). + +## Step 5: Install npm packages + +Enter `lib/forge-proposal-simulator/typescript` directory and install npm packages. + +```bash +cd lib/forge-proposal-simulator/typescript && npm i +``` + +Change directory again to the root repo + +```bash +cd ../../../ +``` + +## Step 6: Add environment variables + +Add below environment variables to `.env`. + +``` +ADDRESSES_PATH # Path to addresses.json file +TYPE_CHECK_ADDRESSES_PATH # Path to typeCheckAddresses.json file +ARTIFACT_PATH # Path of artifact folder +``` + +Example: + +``` +TYPE_CHECK_ADDRESSES_PATH=addresses/TypeCheckAddresses.json +ADDRESSES_PATH=addresses/Addresses.json +ARTIFACT_PATH=out/ +``` + +## Step 7: File read accesss + +Make sure to allow read access to `Addresses.json`, `TypeCheckAddresses.json` and `artifact` folder inside of `foundry.toml`. + +```toml +[profile.default] + +fs_permissions = [{ access = "read", path = "./"}] +``` diff --git a/docs/type-check/type-check.md b/docs/type-check/type-check.md new file mode 100644 index 00000000..03c90d66 --- /dev/null +++ b/docs/type-check/type-check.md @@ -0,0 +1,87 @@ +# Type check Addresses + +## Overview + +The `TypeCheckAddresses.json file` is a JSON file used for verifying the bytecode of already deployed contracts on any chain. This file contains a list of objects where each object stores `name`, `constructorArgs`, and `artifactPath` for all the contracts that we want to type check. `name` should be the same as the `name` in `Addresses.json` as it links both JSONs together. The `constructorArgs` should be in comma-separated array format. For `Address`, `bytes`, and `string`, double-quotes should be used. Tuples, on the other hand, are passed like arrays `[]`. Additionally, each `"` in JSON is escaped using `\` since it is a special character in the JSON file. + +## Structure + +ExampleTypeCheck.sol + +```solidity +pragma solidity =0.8.19; + +contract ExampleTypeCheck { + struct StructC { + string varC1; + uint256 varC2; + } + + struct StructB { + bytes varB1; + uint256 varB2; + StructC structC; + } + struct StructA { + address varA1; + bytes32 varA2; + StructB structB; + } + + constructor( + StructA memory structA, + string[] memory arg1, + uint256[] memory arg2, + StructB[] memory structb + ) {} + + function encode( + StructA memory structA, + string[] memory arg1, + uint256[] memory arg2, + StructB[] memory structb + ) external pure returns (bytes memory) { + return abi.encode(structA, arg1, arg2, structb); + } +} +``` + +ExampleTypeCheck_02.sol + +```solidity +pragma solidity =0.8.19; + +contract ExampleTypeCheck_02 { + struct StructA { + uint256[] varA1; + } + + struct StructB { + StructA[] varB1; + } + + constructor( + uint256[][] memory, + StructA[] memory, + StructB[] memory, + address[][] memory + ) {} +} +``` + +TypeCheckAddresses.json for above two contract. + +```json +[ + { + "name": "ExampleTypeCheck", + "constructorArgs": "[[\"0x95222290DD7278Aa3Ddd389Cc1E1d165CC4BAfe5\", \"0x95222290dd7278aa3ddd389cc1e1d165cc4bafe5000000000000000000000000\", [\"0x95222290DD7278Aa3Ddd389Cc1E1d165CC4BAfe5\", 2, [\"0x95222290DD7278Aa3Ddd389Cc1E1d165CC4BAfe5\", 2]]], [\"Arg1\", \"Arg2\"], [2, 3], [[\"0x95222290DD7278Aa3Ddd389Cc1E1d165CC4BAfe5\", 2, [\"0x95222290DD7278Aa3Ddd389Cc1E1d165CC4BAfe5\", 2]], [\"0x95222290DD7278Aa3Ddd389Cc1E1d165CC4BAfe5\", 2, [\"0x95222290DD7278Aa3Ddd389Cc1E1d165CC4BAfe5\", 2]]]]", + "artifactPath": "ExampleTypeCheck.sol:ExampleTypeCheck" + }, + { + "name": "ExampleTypeCheck_02", + "constructorArgs": "[[[1, 2], [3, 4]], [[[1, 2]], [[3, 4]]], [[[[[1, 2]], [[3, 4]]]], [[[[5, 6]], [[7, 8]]]]], [[\"0x95222290DD7278Aa3Ddd389Cc1E1d165CC4BAfe5\"], [\"0x95222290DD7278Aa3Ddd389Cc1E1d165CC4BAfe5\"]]]", + "artifactPath": "ExampleTypeCheck_02.sol:ExampleTypeCheck_02" + } +] +``` diff --git a/foundry.toml b/foundry.toml index edb50883..478741a9 100644 --- a/foundry.toml +++ b/foundry.toml @@ -1,6 +1,12 @@ [profile.default] libs = ['lib'] test = 'test' -src = 'proposals/' -fs_permissions = [{ access = "read", path = "./"}] + +# Require read access for 'addresses' and 'out' folder on root repo for bytecode verification +fs_permissions = [{ access = "read", path = "../../"}] evm_version = 'paris' +optimizer = true +optimizer_runs = 200 + +[rpc_endpoints] +sepolia = "https://ethereum-sepolia-rpc.publicnode.com" diff --git a/remappings.txt b/remappings.txt index 3c4112d4..c09fc1bc 100644 --- a/remappings.txt +++ b/remappings.txt @@ -1,6 +1,7 @@ @forge-std/=lib/forge-std/src/ @test=test/ -@proposals/=proposals/ +@proposals/=src/proposals/ +@type-check/=src/type-check/ @addresses/=addresses/ @utils/=utils/ @examples/=examples/ diff --git a/script/DeployExampleTypeCheck.s.sol b/script/DeployExampleTypeCheck.s.sol new file mode 100644 index 00000000..526b81e8 --- /dev/null +++ b/script/DeployExampleTypeCheck.s.sol @@ -0,0 +1,55 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity =0.8.19; + +import "forge-std/Script.sol"; +import "../src/type-check/ExampleTypeCheck.sol"; + +contract DeployExampleTypeCheck is Script { + function run() external { + vm.startBroadcast(); + + ExampleTypeCheck.StructC memory structC = ExampleTypeCheck.StructC({ + varC1: "0x95222290DD7278Aa3Ddd389Cc1E1d165CC4BAfe5", + varC2: 2 + }); + + ExampleTypeCheck.StructB memory structB = ExampleTypeCheck.StructB({ + varB1: hex"95222290DD7278Aa3Ddd389Cc1E1d165CC4BAfe5", + varB2: 2, + structC: structC + }); + + ExampleTypeCheck.StructA memory structA = ExampleTypeCheck.StructA({ + varA1: 0x95222290DD7278Aa3Ddd389Cc1E1d165CC4BAfe5, + varA2: hex"95222290DD7278Aa3Ddd389Cc1E1d165CC4BAfe5", + structB: structB + }); + + string[] memory stringArg = new string[](2); + stringArg[0] = "Arg1"; + stringArg[1] = "Arg2"; + + uint256[] memory uintArg = new uint256[](2); + uintArg[0] = 2; + uintArg[1] = 3; + + ExampleTypeCheck.StructB[] + memory structBArrayArg = new ExampleTypeCheck.StructB[](2); + + structBArrayArg[0] = ExampleTypeCheck.StructB({ + varB1: hex"95222290DD7278Aa3Ddd389Cc1E1d165CC4BAfe5", + varB2: 2, + structC: structC + }); + + structBArrayArg[1] = ExampleTypeCheck.StructB({ + varB1: hex"95222290DD7278Aa3Ddd389Cc1E1d165CC4BAfe5", + varB2: 2, + structC: structC + }); + + new ExampleTypeCheck(structA, stringArg, uintArg, structBArrayArg); + + vm.stopBroadcast(); + } +} diff --git a/script/TypeCheck.s.sol b/script/TypeCheck.s.sol new file mode 100644 index 00000000..acd1c746 --- /dev/null +++ b/script/TypeCheck.s.sol @@ -0,0 +1,22 @@ +pragma solidity ^0.8.0; + +import "forge-std/Script.sol"; +import {TypeChecker} from "@type-check/TypeChecker.sol"; + +contract TypeCheck is Script { + string public ADDRESSES_PATH = vm.envString("ADDRESSES_PATH"); + string public TYPE_CHECK_ADDRESSES_PATH = + vm.envString("TYPE_CHECK_ADDRESSES_PATH"); + string public ARTIFACT_PATH = vm.envString("ARTIFACT_PATH"); + string public LIB_PATH = + vm.envOr("LIB_PATH", string("lib/forge-proposal-simulator/")); + + function run() public virtual { + new TypeChecker( + ADDRESSES_PATH, + TYPE_CHECK_ADDRESSES_PATH, + ARTIFACT_PATH, + LIB_PATH + ); + } +} diff --git a/proposals/CrossChainProposal.sol b/src/proposals/CrossChainProposal.sol similarity index 100% rename from proposals/CrossChainProposal.sol rename to src/proposals/CrossChainProposal.sol diff --git a/proposals/GovernorBravoProposal.sol b/src/proposals/GovernorBravoProposal.sol similarity index 100% rename from proposals/GovernorBravoProposal.sol rename to src/proposals/GovernorBravoProposal.sol diff --git a/proposals/IProposal.sol b/src/proposals/IProposal.sol similarity index 100% rename from proposals/IProposal.sol rename to src/proposals/IProposal.sol diff --git a/proposals/MultisigProposal.sol b/src/proposals/MultisigProposal.sol similarity index 100% rename from proposals/MultisigProposal.sol rename to src/proposals/MultisigProposal.sol diff --git a/proposals/Proposal.sol b/src/proposals/Proposal.sol similarity index 100% rename from proposals/Proposal.sol rename to src/proposals/Proposal.sol diff --git a/proposals/TimelockProposal.sol b/src/proposals/TimelockProposal.sol similarity index 100% rename from proposals/TimelockProposal.sol rename to src/proposals/TimelockProposal.sol diff --git a/src/type-check/ExampleTypeCheck.sol b/src/type-check/ExampleTypeCheck.sol new file mode 100644 index 00000000..74bd1787 --- /dev/null +++ b/src/type-check/ExampleTypeCheck.sol @@ -0,0 +1,37 @@ +// SPDX-License-Identifier: GPL-3.0 + +pragma solidity =0.8.19; + +contract ExampleTypeCheck { + struct StructC { + string varC1; + uint256 varC2; + } + + struct StructB { + bytes varB1; + uint256 varB2; + StructC structC; + } + struct StructA { + address varA1; + bytes32 varA2; + StructB structB; + } + + constructor( + StructA memory structA, + string[] memory arg1, + uint256[] memory arg2, + StructB[] memory structb + ) {} + + function encode( + StructA memory structA, + string[] memory arg1, + uint256[] memory arg2, + StructB[] memory structb + ) external pure returns (bytes memory) { + return abi.encode(structA, arg1, arg2, structb); + } +} diff --git a/src/type-check/ExampleTypeCheck_02.sol b/src/type-check/ExampleTypeCheck_02.sol new file mode 100644 index 00000000..6b31cf37 --- /dev/null +++ b/src/type-check/ExampleTypeCheck_02.sol @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: GPL-3.0 + +pragma solidity =0.8.19; + +contract ExampleTypeCheck_02 { + struct StructA { + uint256[] varA1; + } + + struct StructB { + StructA[] varB1; + } + constructor( + uint256[][] memory, + StructA[] memory, + StructB[] memory, + address[][] memory + ) {} +} diff --git a/src/type-check/TypeChecker.sol b/src/type-check/TypeChecker.sol new file mode 100644 index 00000000..65a29802 --- /dev/null +++ b/src/type-check/TypeChecker.sol @@ -0,0 +1,148 @@ +pragma solidity ^0.8.0; + +import {Addresses} from "@addresses/Addresses.sol"; +import {Strings} from "@openzeppelin/utils/Strings.sol"; +import {Test} from "@forge-std/Test.sol"; +import "@forge-std/console.sol"; + +contract TypeChecker is Test { + using Strings for string; + + Addresses public addresses; + + bytes constant IPFS = hex"69706673"; + + bytes constant SOLC = hex"736f6c63"; + + struct SavedTypeCheckAddresses { + /// Artifact path + string artifactPath; + /// Constructor argument for the contract to be checked + string constructorArgs; + /// name of contract to store + string name; + } + + constructor( + string memory addressesPath, + string memory typeCheckAddressesPath, + string memory artifactDirectory, + string memory libPath + ) { + addresses = new Addresses(addressesPath); + + string memory addressesData = string( + abi.encodePacked(vm.readFile(typeCheckAddressesPath)) + ); + + bytes memory parsedJson = vm.parseJson(addressesData); + + SavedTypeCheckAddresses[] memory savedTypeCheckAddresses = abi.decode( + parsedJson, + (SavedTypeCheckAddresses[]) + ); + + for (uint256 i = 0; i < savedTypeCheckAddresses.length; i++) { + string[] memory commands = new string[](6); + + /// note to future self, ffi absolutely flips out if you try to set env vars + commands[0] = "npx"; + commands[1] = "ts-node"; + commands[2] = string( + abi.encodePacked(libPath, "typescript/encode.ts") + ); + commands[3] = savedTypeCheckAddresses[i].constructorArgs; + commands[4] = savedTypeCheckAddresses[i].artifactPath; + commands[5] = artifactDirectory; + + console.log("\nContract:", savedTypeCheckAddresses[i].name); + + bytes memory encodedConstructorArgs = vm.ffi(commands); + console.log("Encoded contructor args :"); + emit log_bytes(encodedConstructorArgs); + + address contractAddress = deployCode( + savedTypeCheckAddresses[i].artifactPath, + encodedConstructorArgs + ); + + bytes memory deployedBytecode = addresses + .getAddress(savedTypeCheckAddresses[i].name) + .code; + + bytes memory localDeployedBytecode = contractAddress.code; + + if ( + matchPattern(localDeployedBytecode, IPFS) && + matchPattern(localDeployedBytecode, SOLC) + ) { + deployedBytecode = _removeMetadata(deployedBytecode); + + localDeployedBytecode = _removeMetadata(localDeployedBytecode); + } + + console.log("Local deployed bytecode: "); + emit log_bytes(localDeployedBytecode); + console.log("Deployed bytecode: "); + emit log_bytes(deployedBytecode); + + require( + keccak256(deployedBytecode) == keccak256(localDeployedBytecode), + "Deployed bytecode not matched" + ); + } + } + + function _removeMetadata( + bytes memory data + ) internal pure returns (bytes memory) { + require(data.length >= 2, "Byte array must have at least 2 bytes"); + + // Convert last two bytes to uint16 + uint16 metadataSize = uint16(uint8(data[data.length - 2])) * + 256 + + uint16(uint8(data[data.length - 1])); + + require( + data.length >= metadataSize, + "Number of bytes to remove exceeds length of byte array" + ); + + // Subtracting 2 to remove last 2 bytes that stores the metadata size + uint256 trimmedSize = data.length - metadataSize - 2; + + // Create a new byte array with the updated length + bytes memory trimmedData = new bytes(trimmedSize); + + // Copy data except the last metadata bytes + for (uint256 i = 0; i < trimmedSize; i++) { + trimmedData[i] = data[i]; + } + + return trimmedData; + } + + function matchPattern( + bytes memory data, + bytes memory pattern + ) public pure returns (bool) { + require( + data.length >= pattern.length, + "Data length is less than pattern length" + ); + + for (uint256 i = 0; i <= data.length - pattern.length; i++) { + bool isMatch = true; + for (uint256 j = 0; j < pattern.length; j++) { + if (data[i + j] != pattern[j]) { + isMatch = false; + break; + } + } + if (isMatch) { + return true; + } + } + return false; + } +} diff --git a/test/TypeCheck.t.sol b/test/TypeCheck.t.sol new file mode 100644 index 00000000..3b43d9b7 --- /dev/null +++ b/test/TypeCheck.t.sol @@ -0,0 +1,50 @@ +pragma solidity ^0.8.0; + +import {TypeChecker} from "@type-check/TypeChecker.sol"; +import {Test} from "forge-std/Test.sol"; + +contract TypeCheck is Test { + string public constant ADDRESSES_PATH = "addresses/Addresses.json"; + string public constant TYPE_CHECK_ADDRESSES_PATH = + "addresses/TypeCheckAddresses.json"; + string public constant ARTIFACT_DIRECTORY = "out/"; + string public constant TYPE_CHECK_ADDRESSES_PATH_INCORRECT = + "addresses/TypeCheckAddressesIncorrect.json"; + string public constant ADDRESSES_PATH_INCORRECT = + "addresses/AddressesIncorrect.json"; + string public constant LIB_PATH = ""; + TypeChecker public typechecker; + + function test_typeCheck() public { + typechecker = new TypeChecker( + ADDRESSES_PATH, + TYPE_CHECK_ADDRESSES_PATH, + ARTIFACT_DIRECTORY, + LIB_PATH + ); + } + + // Artifact path is incorrect in TypeCheckAddresses.json + function test_typeCheckIncorrectArtifact() public { + vm.expectRevert( + "StdCheats deployCode(string,bytes): Deployment failed." + ); + typechecker = new TypeChecker( + ADDRESSES_PATH, + TYPE_CHECK_ADDRESSES_PATH_INCORRECT, + ARTIFACT_DIRECTORY, + LIB_PATH + ); + } + + // Deployed bytecode is incorrect + function test_typeCheckIncorrectByteCode() public { + vm.expectRevert("Deployed bytecode not matched"); + typechecker = new TypeChecker( + ADDRESSES_PATH_INCORRECT, + TYPE_CHECK_ADDRESSES_PATH, + ARTIFACT_DIRECTORY, + LIB_PATH + ); + } +} diff --git a/typescript/encode.ts b/typescript/encode.ts new file mode 100644 index 00000000..6d9cd619 --- /dev/null +++ b/typescript/encode.ts @@ -0,0 +1,82 @@ +import { ethers } from 'ethers'; +import * as fs from 'fs'; + +function extractABI(artifactPath: string, artifactDirectory: string): any { + artifactPath = artifactPath.replace(":", "/"); + + const filePath = `${artifactDirectory}${artifactPath}.json`; + + try { + const jsonData = fs.readFileSync(filePath, 'utf-8'); + const contractJSON = JSON.parse(jsonData); + + return contractJSON.abi; + } catch (error) { + throw new Error(`ABI not found on path.`); + } +} + +// Recursive function to traverse and generate type array +function traverseInputs(inputs: any, depth: number): any { + if (depth == 0) { + return inputs.map((input: any) => { + if (input.type === 'tuple[]') { + return `tuple(${traverseInputs(input.components, depth + 1)})[]` + } + if (input.type === 'tuple') { + return `tuple(${traverseInputs(input.components, depth + 1)})`; + } else { + return input.type; + } + }); + } + return inputs.map((input: any) => { + if (input.type === 'tuple[]') { + return `tuple(${traverseInputs(input.components, depth + 1)})[]` + } + else if (input.type === 'tuple') { + return `tuple(${traverseInputs(input.components, depth + 1)})`; + } else { + return input.type; + } + }).join(', '); +} + +// Function to generate type array based on the ABI format +function generateTypeArray(abi: any[]): string[] { + const constructorAbi = abi.find(item => item.type === 'constructor'); + if (!constructorAbi) { + throw new Error(`Constructor not found in ABI.`); + } + + const inputs = constructorAbi.inputs; + + const typeArray: string[] = traverseInputs(inputs, 0); + return typeArray; +} + +// Function to encode data based on the ABI format +function encodeData(abi: any[], constructorInputs: any[]): string { + + const types = generateTypeArray(abi); + + return ethers.utils.defaultAbiCoder.encode(types, constructorInputs); +} + +// Gets command line arguments +const args = process.argv; + +// Removes '\' from command line args +const cleanedArg = args[2].replace(/\\/g, ""); + +const constructorInputs = JSON.parse(cleanedArg); + +const artifactPath = args[3]; + +const artifactDirectory = args[4]; + +const abi = extractABI(artifactPath, artifactDirectory); + +// Encode the constructor inputs +const encodedData = encodeData(abi, constructorInputs); +process.stdout.write(encodedData); diff --git a/typescript/package.json b/typescript/package.json new file mode 100644 index 00000000..a008ec6a --- /dev/null +++ b/typescript/package.json @@ -0,0 +1,9 @@ +{ + "name": "encode-args-ts", + "version": "1.0.0", + "main": "index.js", + "license": "MIT", + "dependencies": { + "ethers": "5.7.2" + } +} diff --git a/typescript/tsconfig.json b/typescript/tsconfig.json new file mode 100644 index 00000000..1d30ba8e --- /dev/null +++ b/typescript/tsconfig.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "target": "ES2015", + "moduleResolution": "node", + "module": "commonjs", + "resolveJsonModule": true, + "esModuleInterop": true + }, + "include": ["./**/*.ts"] +}