diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..e83d3f2 --- /dev/null +++ b/.env.example @@ -0,0 +1,13 @@ +# Deployer key (REQUIRED) +PUBLIC_KEY= + +### Base URLs ### +# Mainnet +BASE_MAIN_RPC_URL= + +# Testnet +BASE_TEST_RPC_URL=https://sepolia.base.org + +# Blockscout API Key +# Foundry expects the API key to be defined as ETHERSCAN_API_KEY +ETHERSCAN_API_KEY= \ No newline at end of file diff --git a/.gas-snapshot b/.gas-snapshot new file mode 100644 index 0000000..a7c21de --- /dev/null +++ b/.gas-snapshot @@ -0,0 +1,13 @@ +LLMOracleCoordinatorTest:test_Deployment() (gas: 86928751) +LLMOracleCoordinatorTest:test_RegisterOracles() (gas: 87213539) +LLMOracleCoordinatorTest:test_ValidatorIsGenerator() (gas: 87630540) +LLMOracleCoordinatorTest:test_WithValidation() (gas: 88289236) +LLMOracleCoordinatorTest:test_WithoutValidation() (gas: 87870279) +LLMOracleRegistryTest:test_Deployment() (gas: 18833944) +LLMOracleRegistryTest:test_RegisterGeneratorOracle() (gas: 19172865) +LLMOracleRegistryTest:test_RegisterValidatorOracle() (gas: 19172934) +LLMOracleRegistryTest:test_RevertWhen_RegisterSameGeneratorTwice() (gas: 19176257) +LLMOracleRegistryTest:test_RevertWhen_RegistryHasNotApprovedByOracle() (gas: 18973195) +LLMOracleRegistryTest:test_RevertWhen_UnregisterSameGeneratorTwice() (gas: 19192397) +LLMOracleRegistryTest:test_UnregisterOracle() (gas: 19189291) +LLMOracleRegistryTest:test_WithdrawStakesAfterUnregistering() (gas: 19214255) \ No newline at end of file diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..156e8af --- /dev/null +++ b/Makefile @@ -0,0 +1,51 @@ +-include .env + +.PHONY: build test local-key base-sepolia-key deploy anvil install update doc + +# Capture the network name +network := $(word 2, $(MAKECMDGOALS)) + +# Default to forked base-sepolia network +KEY_NAME := local-key +NETWORK_ARGS := --account local-key --sender 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 --broadcast + +ifeq ($(network), base-sepolia) +KEY_NAME := base-sepolia-key +NETWORK_ARGS:= --rpc-url $(BASE_TEST_RPC_URL) --account base-sepolia-key --sender $(PUBLIC_KEY) --broadcast --verify --verifier blockscout --verifier-url https://base-sepolia.blockscout.com/api/ +endif + +# Install Dependencies +install: + forge install foundry-rs/forge-std --no-commit && forge install firstbatchxyz/dria-oracle-contracts --no-commit && forge install OpenZeppelin/openzeppelin-contracts --no-commit && forge install OpenZeppelin/openzeppelin-foundry-upgrades --no-commit && forge install OpenZeppelin/openzeppelin-contracts-upgradeable --no-commit + +# Build the contracts +build: + forge clean && forge build + +# Generate gas snapshot under snapshots directory +snapshot: + forge snapshot + +# Test the contracts on forked base-sepolia network +test: + forge clean && forge test --fork-url $(BASE_TEST_RPC_URL) + +anvil: + anvil --fork-url $(BASE_TEST_RPC_URL) + +# Create keystores for encrypted private keys by using bls12-381 curve (https://eips.ethereum.org/EIPS/eip-2335) +key: + cast wallet import $(KEY_NAME) --interactive + +# Default to local network if no network is specified +deploy: + forge script ./script/Deploy.s.sol:Deploy $(NETWORK_ARGS) + +# Generate contract documentation under docs dir. You can also see the docs on http://localhost:4000 +doc: + forge doc + +# TODO: forge-verify + +# Prevent make from interpreting the network name as a target +$(eval $(network):;@:) \ No newline at end of file diff --git a/README.md b/README.md index 9265b45..482cf3c 100644 --- a/README.md +++ b/README.md @@ -1,66 +1,116 @@ -## Foundry +# LLM Oracle -**Foundry is a blazing fast, portable and modular toolkit for Ethereum application development written in Rust.** +This document provides instructions for LLM contracts using Foundry. -Foundry consists of: +## Test -- **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. +Compile the contracts: -## Documentation +```sh +make build +``` + +> [!NOTE] +> +> Please prepare a valid `.env` according to `.env.example` before running tests. + +Run tests on forked base-sepolia: -https://book.getfoundry.sh/ +```sh +make test +``` -## Usage +## Coverage -### Build +Check coverages with: -```shell -$ forge build +```sh +bash coverage.sh ``` -### Test +You can see coverages under the coverage directory. + +## Storage Layout + +Get storage layout with: -```shell -$ forge test +```sh +bash storage.sh ``` -### Format +You can see storage layouts under the storage directory. -```shell -$ forge fmt +## Deployment + +**Step 1.** +Import your `PUBLIC_KEY` and `ETHERSCAN_API_KEY` to env file. + +> [!NOTE] +> +> Foundry expects the API key to be defined as `ETHERSCAN_API_KEY` even though you're using another explorer. + +**Step 2.** +Create keystores for deployment. [See more for keystores](https://eips.ethereum.org/EIPS/eip-2335) + +```sh +make local-key ``` -### Gas Snapshots +or for Base Sepolia -```shell -$ forge snapshot +```sh +make base-sepolia-key ``` -### Anvil +> [!NOTE] +> +> Recommended to create keystores on directly on your shell. +> You HAVE to type your password on the terminal to be able to use your keys. (e.g when deploying a contract) + +**Step 3.** +Enter your private key (associated with the public key you added to env file) and password on terminal. You'll see your public key on terminal. -```shell -$ anvil +> [!NOTE] +> +> If you want to deploy contracts on localhost please provide localhost public key for the command above. + +**Step 4.** Required only for local deployment. + +Start a local node with: + +```sh +make anvil +``` + +**Step 5.** +Deploy the contracts on localhost (forked Base Sepolia by default) using Deploy script: + +```sh +make deploy ``` -### Deploy +or Base Sepolia with the command below: -```shell -$ forge script script/Counter.s.sol:CounterScript --rpc-url --private-key +```sh +make deploy base-sepolia ``` -### Cast +You can see deployed contract addresses under the `deployment/.json` + +## Gas Snapshot + +Take the gas snapshot with: -```shell -$ cast +```sh +make snapshot ``` -### Help +You can see the snapshot `.gas-snapshot` file in the current directory. -```shell -$ forge --help -$ anvil --help -$ cast --help +## Generate documentation + +```sh +make doc ``` + +You can see the documentation under the `docs/` directory. diff --git a/deployment/31337.json b/deployment/31337.json new file mode 100644 index 0000000..35c4b01 --- /dev/null +++ b/deployment/31337.json @@ -0,0 +1 @@ +"{ \"LLMOracleRegistry\": { \"proxyAddr\": \"0x9fe46736679d2d9a65f0992f2272de9f3c7fa6e0\", \"implAddr\": \"0xe7f1725e7734ce288f8367e1bb143e90bb3f0512\" }, \"LLMOracleCoordinator\": { \"proxyAddr\": \"0xdc64a140aa3e981100a9beca4e685f962f0cf6c9\", \"implAddr\": \"0xcf7ed3acca5a467e9e704c703e8d87f634fb0fc9\" }" \ No newline at end of file diff --git a/foundry.toml b/foundry.toml index 25b918f..9f0cb44 100644 --- a/foundry.toml +++ b/foundry.toml @@ -2,5 +2,19 @@ src = "src" out = "out" libs = ["lib"] +test = 'test' +script = 'script' +cache_path = 'cache' +ffi = true +ast = true +build_info = true +optimizer = true +extra_output = ["storageLayout"] +fs_permissions = [{ access = "read", path = "out" }, { access = "write", path = "deployment" }] +remappings = [ +"@openzeppelin/contracts-upgradeable/=lib/openzeppelin-contracts-upgradeable/contracts/", +"@openzeppelin/contracts/=lib/openzeppelin-contracts/contracts/", +"@openzeppelin/foundry-upgrades=lib/openzeppelin-foundry-upgrades/src", +] # See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options diff --git a/remappings.txt b/remappings.txt deleted file mode 100644 index f1792f7..0000000 --- a/remappings.txt +++ /dev/null @@ -1,4 +0,0 @@ -forge-std/=lib/forge-std/src/ -@openzeppelin/contracts/=lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/contracts/ -@openzeppelin/contracts-upgradeable/=lib/openzeppelin-contracts-upgradeable/contracts/ -@openzeppelin/contracts/=lib/openzeppelin-contracts/contracts/ diff --git a/script/Counter.s.sol b/script/Counter.s.sol deleted file mode 100644 index cdc1fe9..0000000 --- a/script/Counter.s.sol +++ /dev/null @@ -1,19 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.13; - -import {Script, console} from "forge-std/Script.sol"; -import {Counter} from "../src/Counter.sol"; - -contract CounterScript is Script { - Counter public counter; - - function setUp() public {} - - function run() public { - vm.startBroadcast(); - - counter = new Counter(); - - vm.stopBroadcast(); - } -} diff --git a/script/Deploy.s.sol b/script/Deploy.s.sol new file mode 100644 index 0000000..68c0a1b --- /dev/null +++ b/script/Deploy.s.sol @@ -0,0 +1,96 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.20; + +import {Script} from "forge-std/Script.sol"; +import {HelperConfig} from "./HelperConfig.s.sol"; +import {Upgrades} from "@openzeppelin/foundry-upgrades/Upgrades.sol"; +import {LLMOracleRegistry} from "../src/LLMOracleRegistry.sol"; +import {LLMOracleCoordinator, LLMOracleTaskParameters} from "../src/LLMOracleCoordinator.sol"; +import {Strings} from "@openzeppelin/contracts/utils/Strings.sol"; +import {Vm} from "forge-std/Vm.sol"; + +contract Deploy is Script { + // contracts + LLMOracleCoordinator public oracleCoordinator; + LLMOracleRegistry public oracleRegistry; + + // implementation addresses + address registryImplementation; + address coordinatorImplementation; + + HelperConfig public config; + uint256 chainId; + + function run() external { + chainId = block.chainid; + config = new HelperConfig(); + + vm.startBroadcast(); + deployLLM(); + vm.stopBroadcast(); + + writeContractAddresses(); + } + + function deployLLM() internal { + // get stakes + (uint256 genStake, uint256 valStake) = config.stakes(); + + // get fees + (uint256 platformFee, uint256 genFee, uint256 valFee) = config.fees(); + + // deploy llm contracts + address registryProxy = Upgrades.deployUUPSProxy( + "LLMOracleRegistry.sol", + abi.encodeCall(LLMOracleRegistry.initialize, (genStake, valStake, address(config.token()))) + ); + + // wrap proxy with the LLMOracleRegistry + oracleRegistry = LLMOracleRegistry(registryProxy); + registryImplementation = Upgrades.getImplementationAddress(registryProxy); + + // deploy coordinator contract + address coordinatorProxy = Upgrades.deployUUPSProxy( + "LLMOracleCoordinator.sol", + abi.encodeCall( + LLMOracleCoordinator.initialize, + (address(oracleRegistry), address(config.token()), platformFee, genFee, valFee) + ) + ); + + oracleCoordinator = LLMOracleCoordinator(coordinatorProxy); + coordinatorImplementation = Upgrades.getImplementationAddress(coordinatorProxy); + } + + function writeContractAddresses() internal { + // create a deployment file if not exist + string memory dir = "deployment/"; + string memory fileName = Strings.toString(chainId); + string memory path = string.concat(dir, fileName, ".json"); + + // create dir if not exist + vm.createDir(dir, true); + + string memory contracts = string.concat( + "{", + ' "LLMOracleRegistry": {', + ' "proxyAddr": "', + Strings.toHexString(uint256(uint160(address(oracleRegistry))), 20), + '",', + ' "implAddr": "', + Strings.toHexString(uint256(uint160(address(registryImplementation))), 20), + '"', + " },", + ' "LLMOracleCoordinator": {', + ' "proxyAddr": "', + Strings.toHexString(uint256(uint160(address(oracleCoordinator))), 20), + '",', + ' "implAddr": "', + Strings.toHexString(uint256(uint160(address(coordinatorImplementation))), 20), + '"', + " }" + ); + + vm.writeJson(contracts, path); + } +} diff --git a/script/HelperConfig.s.sol b/script/HelperConfig.s.sol new file mode 100644 index 0000000..8696f26 --- /dev/null +++ b/script/HelperConfig.s.sol @@ -0,0 +1,40 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.20; + +import {Script} from "forge-std/Script.sol"; +import {WETH9} from "../test/WETH9.sol"; +import {LLMOracleTaskParameters} from "../src/LLMOracleTask.sol"; + +struct Stakes { + uint256 generatorStakeAmount; + uint256 validatorStakeAmount; +} + +struct Fees { + uint256 platformFee; + uint256 generatorFee; + uint256 validatorFee; +} + +contract HelperConfig is Script { + LLMOracleTaskParameters public taskParams; + + Stakes public stakes; + Fees public fees; + WETH9 public token; + + constructor() { + // set deployment parameters + stakes = Stakes({generatorStakeAmount: 0.0001 ether, validatorStakeAmount: 0.000001 ether}); + fees = Fees({platformFee: 0.0001 ether, generatorFee: 0.0001 ether, validatorFee: 0.0001 ether}); + taskParams = LLMOracleTaskParameters({difficulty: 2, numGenerations: 1, numValidations: 1}); + + // for base sepolia + if (block.chainid == 84532) { + // use deployed weth + token = WETH9(payable(0x4200000000000000000000000000000000000006)); + } + // for local create a new token + token = new WETH9(); + } +} diff --git a/src/Counter.sol b/src/Counter.sol deleted file mode 100644 index 47deaec..0000000 --- a/src/Counter.sol +++ /dev/null @@ -1,14 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.22; - -contract Counter { - uint256 public number; - - function setNumber(uint256 newNumber) public { - number = newNumber; - } - - function increment() public { - number++; - } -} diff --git a/test/Counter.t.sol b/test/Counter.t.sol deleted file mode 100644 index f9e3057..0000000 --- a/test/Counter.t.sol +++ /dev/null @@ -1,25 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.22; - -import {Test, console} from "forge-std/Test.sol"; - -import {Counter} from "../src/Counter.sol"; - -contract CounterTest is Test { - Counter public counter; - - function setUp() public { - counter = new Counter(); - counter.setNumber(0); - } - - function test_Increment() public { - counter.increment(); - assertEq(counter.number(), 1); - } - - function testFuzz_SetNumber(uint256 x) public { - counter.setNumber(x); - assertEq(counter.number(), x); - } -} diff --git a/test/HelperTest.sol b/test/Helper.t.sol similarity index 96% rename from test/HelperTest.sol rename to test/Helper.t.sol index efafa8f..414a38f 100644 --- a/test/HelperTest.sol +++ b/test/Helper.t.sol @@ -1,9 +1,9 @@ // SPDX-License-Identifier: Apache-2.0 pragma solidity ^0.8.20; -import {Vm} from "../lib/forge-std/src/Vm.sol"; -import {Upgrades} from "../lib/openzeppelin-foundry-upgrades/src/Upgrades.sol"; -import {Test, console} from "../lib/forge-std/src/Test.sol"; +import {Vm} from "forge-std/Vm.sol"; +import {Upgrades} from "@openzeppelin/foundry-upgrades/Upgrades.sol"; +import {Test} from "forge-std/Test.sol"; import {LLMOracleRegistry, LLMOracleKind} from "../src/LLMOracleRegistry.sol"; import {LLMOracleCoordinator} from "../src/LLMOracleCoordinator.sol"; @@ -11,7 +11,7 @@ import {LLMOracleTaskParameters} from "../src/LLMOracleTask.sol"; import {WETH9} from "./WETH9.sol"; -abstract contract HelperTest is Test { +abstract contract Helper is Test { struct Stakes { uint256 generatorStakeAmount; uint256 validatorStakeAmount; @@ -156,7 +156,7 @@ abstract contract HelperTest is Test { (address requester,,,,,,,,) = oracleCoordinator.requests(taskId); uint256 target = type(uint256).max >> oracleParameters.difficulty; - uint nonce = 0; + uint256 nonce = 0; for (; nonce < type(uint256).max; nonce++) { bytes memory message = abi.encodePacked(taskId, input, requester, responder, nonce); uint256 digest = uint256(keccak256(message)); @@ -209,5 +209,4 @@ abstract contract HelperTest is Test { vm.prank(validator); oracleCoordinator.validate(taskId, nonce, scores, metadata); } - } diff --git a/test/LLMOracleCoordinator.t.sol b/test/LLMOracleCoordinator.t.sol index abe83dd..ecbb1f6 100644 --- a/test/LLMOracleCoordinator.t.sol +++ b/test/LLMOracleCoordinator.t.sol @@ -1,9 +1,9 @@ // SPDX-License-Identifier: Apache-2.0 pragma solidity ^0.8.20; -import {Vm} from "../lib/forge-std/src/Vm.sol"; -import {HelperTest} from "./HelperTest.sol"; -import {Upgrades} from "../lib/openzeppelin-foundry-upgrades/src/Upgrades.sol"; +import {Vm} from "forge-std/Vm.sol"; +import {Helper} from "./Helper.t.sol"; +import {Upgrades} from "@openzeppelin/foundry-upgrades/Upgrades.sol"; import {LLMOracleTask, LLMOracleTaskParameters} from "../src/LLMOracleTask.sol"; import {LLMOracleRegistry, LLMOracleKind} from "../src/LLMOracleRegistry.sol"; @@ -11,7 +11,7 @@ import {LLMOracleCoordinator} from "../src/LLMOracleCoordinator.sol"; import {WETH9} from "./WETH9.sol"; -contract LLMOracleCoordinatorTest is HelperTest { +contract LLMOracleCoordinatorTest is Helper { address dummy = vm.addr(20); address requester = vm.addr(21); diff --git a/test/LLMOracleRegistry.t.sol b/test/LLMOracleRegistry.t.sol index 10c19fe..18d2b9d 100644 --- a/test/LLMOracleRegistry.t.sol +++ b/test/LLMOracleRegistry.t.sol @@ -1,15 +1,15 @@ // SPDX-License-Identifier: Apache-2.0 pragma solidity ^0.8.20; -import {Vm} from "../lib/forge-std/src/Vm.sol"; -import {Upgrades} from "../lib/openzeppelin-foundry-upgrades/src/Upgrades.sol"; +import {Vm} from "forge-std/Vm.sol"; +import {Upgrades} from "@openzeppelin/foundry-upgrades/Upgrades.sol"; import {LLMOracleRegistry, LLMOracleKind} from "../src/LLMOracleRegistry.sol"; import {WETH9} from "./WETH9.sol"; -import {HelperTest} from "./HelperTest.sol"; +import {Helper} from "./Helper.t.sol"; -contract LLMOracleRegistryTest is HelperTest { +contract LLMOracleRegistryTest is Helper { uint256 totalStakeAmount; address oracle;