diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 762a296..c050793 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -36,7 +36,7 @@ jobs: - name: Run Forge build run: | - forge build --sizes + forge clean && forge build --sizes id: build - name: Run Forge tests diff --git a/.gitignore b/.gitignore index 36a800a..a5cc170 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,7 @@ out/ !/broadcast /broadcast/*/31337/ /broadcast/**/dry-run/ +broadcast # Docs docs/ @@ -13,4 +14,6 @@ docs/ # Dotenv file .env -node_modules/ \ No newline at end of file +node_modules/ + +deployments \ No newline at end of file diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..f473229 --- /dev/null +++ b/Makefile @@ -0,0 +1,2 @@ +# Lints +lint :; forge fmt \ No newline at end of file diff --git a/README.md b/README.md index 7fb8469..0ee4df2 100644 --- a/README.md +++ b/README.md @@ -2,12 +2,8 @@ This is a demo repo using OVM contracts lib to calculate Pi onchain. -`./prgrams` contains the python code for computing task. `./src` is the main entry for the contract code. -## Documentation - -https://book.getfoundry.sh/ ## Usage @@ -29,34 +25,27 @@ $ forge test $ forge fmt ``` -### Gas Snapshots - -```shell -$ forge snapshot -``` - -### Anvil - -```shell -$ anvil -``` - ### Deploy ```shell -$ forge script script/Counter.s.sol:CounterScript --rpc-url --private-key -``` +# With verification +forge script script/Deploy.s.sol:Deploy \ +--chain-id $CHAIN_ID \ +--rpc-url $RPC_URL \ +--private-key $PRIVATE_KEY \ +--verifier-url $VERIFIER_URL \ +--verifier $VERIFIER \ +--verify \ +--broadcast --ffi -vvvv -### Cast +# Without verification +forge script script/Deploy.s.sol:Deploy \ +--chain-id $CHAIN_ID \ +--rpc-url $RPC_URL \ +--private-key $PRIVATE_KEY \ +--broadcast --ffi -vvvv -```shell -$ cast -``` -### Help - -```shell -$ forge --help -$ anvil --help -$ cast --help -``` +# generate easily readable abi to /deployments +forge script script/Deploy.s.sol:Deploy --sig 'sync()' --rpc-url $RPC_URL --broadcast --ffi +``` \ No newline at end of file diff --git a/deploy-config/30732.json b/deploy-config/30732.json new file mode 100644 index 0000000..f2409a4 --- /dev/null +++ b/deploy-config/30732.json @@ -0,0 +1,5 @@ +{ + "proxyAdminOwner": "0xEA02bb4e91e36c8F853D9b37B74A832FFeDbF157", + "templateAdmin": "0xE0b98a1062B7f2f77087Cc1Dc9889B79FF46cAb8", + "ovmTaskAddress": "0xca0338b1862bce76b4e4b39f1f0c3117429fbdfa" +} \ No newline at end of file diff --git a/foundry.toml b/foundry.toml index 96be5b2..f5e244c 100644 --- a/foundry.toml +++ b/foundry.toml @@ -1,6 +1,87 @@ [profile.default] -src = "src" -out = "out" libs = ["node_modules","lib"] -# See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options +auto_detect_solc = false +bytecode_hash = "none" +evm_version = "paris" +fs_permissions = [ + { access = "read", path = "./out-optimized" }, + { access = "read", path = "package.json" }, + { access = "read-write", path = "./benchmark/results" }, + { access='read-write', path='./deployments/' }, + { access='read', path='./deploy-config/' }, + { access='read', path='./broadcast/' }, + { access='read', path = './out/' }, + { access='read', path='./script/' }, +] +gas_limit = 9223372036854775807 + +optimizer = true +optimizer_runs = 999999 +out = "out" +script = "script" +sender = "0x1804c8AB1F12E6bbf3894d4083f33e07309d1f38" +solc = "0.8.24" +src = "src" +test = "test" + +[profile.default.fuzz] +max_test_rejects = 1_000_000 # Number of times `vm.assume` can fail +runs = 50 + +[profile.default.invariant] +call_override = false # Override unsafe external calls to perform reentrancy checks +depth = 20 # Number of calls executed in one run +fail_on_revert = true +runs = 20 + +# Run only the code inside benchmark directory +[profile.benchmark] +test = "benchmark" + +# Speed up compilation and tests during development +[profile.lite] +optimizer = false + +# Compile only the production code and the test mocks with via IR +[profile.optimized] +out = "out-optimized" +test = "test/mocks" +via_ir = true + +# See "SMTChecker and Formal Verification" in the Solidity docs +[profile.smt] +ignored_error_codes = [ + 7737, # Disable inline assembly warnings +] +out = "out-optimized" +script = "src" +test = "src" +via_ir = true + + + +# Test the optimized contracts without re-compiling them +[profile.test-optimized] +src = "test" + +[doc] +ignore = ["**/*.t.sol", "script/**"] +out = "docs" + + +[fmt] +line_length = 100 +tab_width = 4 +bracket_spacing = false +int_types = "long" +multiline_func_header = "attributes_first" +quote_style = "preserve" +number_underscore = "preserve" +hex_underscore = "remove" +single_line_statement_blocks = "preserve" +override_spacing = false +wrap_comments = true +ignore = [] +contract_new_lines = false +sort_imports = true diff --git a/programs/main.py b/programs/main.py deleted file mode 100644 index a396739..0000000 --- a/programs/main.py +++ /dev/null @@ -1,27 +0,0 @@ -import math -import sys -from typing import Union -import eth_abi -import ovm - -bridge = ovm.Bridge() - - -def cal_pi(n: int) -> Union[bool, str]: - if n > 15: - raise ValueError("n must be less than 15") - - return True, format(math.pi, f".{n}f") - - -def main(): - if len(sys.argv) != 2: - raise ValueError("missing messages argument") - - (input,) = eth_abi.decode(["int"], bytes.fromhex(sys.argv[1])) - - bridge.submit(["bool", "string"], [True, cal_pi(input)]) - - -if __name__ == "__main__": - main() diff --git a/programs/ovm/__init__.py b/programs/ovm/__init__.py deleted file mode 100644 index 78e2173..0000000 --- a/programs/ovm/__init__.py +++ /dev/null @@ -1,11 +0,0 @@ -import os -import sys - - -sys.path.append(os.path.dirname(os.path.abspath(__file__))) - -from . import bridge_pb2 -from . import bridge_pb2_grpc -from .bridge import Bridge - -__all__ = ["Bridge", "bridge_pb2", "bridge_pb2_grpc"] diff --git a/programs/ovm/bridge.py b/programs/ovm/bridge.py deleted file mode 100644 index b7f1fe5..0000000 --- a/programs/ovm/bridge.py +++ /dev/null @@ -1,16 +0,0 @@ -from typing import Any, Iterable -import eth_abi -from eth_typing import TypeStr -import grpc -from . import bridge_pb2 -from . import bridge_pb2_grpc - -class Bridge: - def __init__(self): - self.bridge_client = bridge_pb2_grpc.BridgeStub(grpc.insecure_channel("unix:///var/run/ovm.sock")) - - def load(self): - return self.bridge_client.Load(bridge_pb2.LoadRequest()) - - def submit(self, abi: Iterable[TypeStr], values: Iterable[Any]): - return self.bridge_client.Submit(bridge_pb2.SubmitRequest(output=eth_abi.encode(abi, values).hex())) diff --git a/programs/ovm/bridge_pb2.py b/programs/ovm/bridge_pb2.py deleted file mode 100644 index 3894414..0000000 --- a/programs/ovm/bridge_pb2.py +++ /dev/null @@ -1,45 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by the protocol buffer compiler. DO NOT EDIT! -# NO CHECKED-IN PROTOBUF GENCODE -# source: api/v1/ovm/bridge.proto -# Protobuf Python Version: 5.27.3 -"""Generated protocol buffer code.""" -from google.protobuf import descriptor as _descriptor -from google.protobuf import descriptor_pool as _descriptor_pool -from google.protobuf import runtime_version as _runtime_version -from google.protobuf import symbol_database as _symbol_database -from google.protobuf.internal import builder as _builder -_runtime_version.ValidateProtobufRuntimeVersion( - _runtime_version.Domain.PUBLIC, - 5, - 27, - 3, - '', - 'api/v1/ovm/bridge.proto' -) -# @@protoc_insertion_point(imports) - -_sym_db = _symbol_database.Default() - - - - -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x17\x61pi/v1/ovm/bridge.proto\x12\x03ovm\"\r\n\x0bLoadRequest\"$\n\x0cLoadResponse\x12\x14\n\x05input\x18\x02 \x01(\tR\x05input\"\'\n\rSubmitRequest\x12\x16\n\x06output\x18\x01 \x01(\tR\x06output\"\x10\n\x0eSubmitResponse2l\n\x06\x42ridge\x12-\n\x04Load\x12\x10.ovm.LoadRequest\x1a\x11.ovm.LoadResponse\"\x00\x12\x33\n\x06Submit\x12\x12.ovm.SubmitRequest\x1a\x13.ovm.SubmitResponse\"\x00\x42Q\n\x07\x63om.ovmB\x0b\x42ridgeProtoP\x01Z\rgo/api/v1/ovm\xa2\x02\x03OXX\xaa\x02\x03Ovm\xca\x02\x03Ovm\xe2\x02\x0fOvm\\GPBMetadata\xea\x02\x03Ovmb\x06proto3') - -_globals = globals() -_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) -_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'api.v1.ovm.bridge_pb2', _globals) -if not _descriptor._USE_C_DESCRIPTORS: - _globals['DESCRIPTOR']._loaded_options = None - _globals['DESCRIPTOR']._serialized_options = b'\n\007com.ovmB\013BridgeProtoP\001Z\rgo/api/v1/ovm\242\002\003OXX\252\002\003Ovm\312\002\003Ovm\342\002\017Ovm\\GPBMetadata\352\002\003Ovm' - _globals['_LOADREQUEST']._serialized_start=32 - _globals['_LOADREQUEST']._serialized_end=45 - _globals['_LOADRESPONSE']._serialized_start=47 - _globals['_LOADRESPONSE']._serialized_end=83 - _globals['_SUBMITREQUEST']._serialized_start=85 - _globals['_SUBMITREQUEST']._serialized_end=124 - _globals['_SUBMITRESPONSE']._serialized_start=126 - _globals['_SUBMITRESPONSE']._serialized_end=142 - _globals['_BRIDGE']._serialized_start=144 - _globals['_BRIDGE']._serialized_end=252 -# @@protoc_insertion_point(module_scope) diff --git a/programs/ovm/bridge_pb2_grpc.py b/programs/ovm/bridge_pb2_grpc.py deleted file mode 100644 index 90f213e..0000000 --- a/programs/ovm/bridge_pb2_grpc.py +++ /dev/null @@ -1,120 +0,0 @@ -# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT! -"""Client and server classes corresponding to protobuf-defined services.""" -import grpc - -import bridge_pb2 as api_dot_v1_dot_ovm_dot_bridge__pb2 - - -class BridgeStub(object): - """Missing associated documentation comment in .proto file.""" - - def __init__(self, channel): - """Constructor. - - Args: - channel: A grpc.Channel. - """ - self.Load = channel.unary_unary( - '/ovm.Bridge/Load', - request_serializer=api_dot_v1_dot_ovm_dot_bridge__pb2.LoadRequest.SerializeToString, - response_deserializer=api_dot_v1_dot_ovm_dot_bridge__pb2.LoadResponse.FromString, - _registered_method=True) - self.Submit = channel.unary_unary( - '/ovm.Bridge/Submit', - request_serializer=api_dot_v1_dot_ovm_dot_bridge__pb2.SubmitRequest.SerializeToString, - response_deserializer=api_dot_v1_dot_ovm_dot_bridge__pb2.SubmitResponse.FromString, - _registered_method=True) - - -class BridgeServicer(object): - """Missing associated documentation comment in .proto file.""" - - def Load(self, request, context): - """Missing associated documentation comment in .proto file.""" - context.set_code(grpc.StatusCode.UNIMPLEMENTED) - context.set_details('Method not implemented!') - raise NotImplementedError('Method not implemented!') - - def Submit(self, request, context): - """Missing associated documentation comment in .proto file.""" - context.set_code(grpc.StatusCode.UNIMPLEMENTED) - context.set_details('Method not implemented!') - raise NotImplementedError('Method not implemented!') - - -def add_BridgeServicer_to_server(servicer, server): - rpc_method_handlers = { - 'Load': grpc.unary_unary_rpc_method_handler( - servicer.Load, - request_deserializer=api_dot_v1_dot_ovm_dot_bridge__pb2.LoadRequest.FromString, - response_serializer=api_dot_v1_dot_ovm_dot_bridge__pb2.LoadResponse.SerializeToString, - ), - 'Submit': grpc.unary_unary_rpc_method_handler( - servicer.Submit, - request_deserializer=api_dot_v1_dot_ovm_dot_bridge__pb2.SubmitRequest.FromString, - response_serializer=api_dot_v1_dot_ovm_dot_bridge__pb2.SubmitResponse.SerializeToString, - ), - } - generic_handler = grpc.method_handlers_generic_handler( - 'ovm.Bridge', rpc_method_handlers) - server.add_generic_rpc_handlers((generic_handler,)) - server.add_registered_method_handlers('ovm.Bridge', rpc_method_handlers) - - - # This class is part of an EXPERIMENTAL API. -class Bridge(object): - """Missing associated documentation comment in .proto file.""" - - @staticmethod - def Load(request, - target, - options=(), - channel_credentials=None, - call_credentials=None, - insecure=False, - compression=None, - wait_for_ready=None, - timeout=None, - metadata=None): - return grpc.experimental.unary_unary( - request, - target, - '/ovm.Bridge/Load', - api_dot_v1_dot_ovm_dot_bridge__pb2.LoadRequest.SerializeToString, - api_dot_v1_dot_ovm_dot_bridge__pb2.LoadResponse.FromString, - options, - channel_credentials, - insecure, - call_credentials, - compression, - wait_for_ready, - timeout, - metadata, - _registered_method=True) - - @staticmethod - def Submit(request, - target, - options=(), - channel_credentials=None, - call_credentials=None, - insecure=False, - compression=None, - wait_for_ready=None, - timeout=None, - metadata=None): - return grpc.experimental.unary_unary( - request, - target, - '/ovm.Bridge/Submit', - api_dot_v1_dot_ovm_dot_bridge__pb2.SubmitRequest.SerializeToString, - api_dot_v1_dot_ovm_dot_bridge__pb2.SubmitResponse.FromString, - options, - channel_credentials, - insecure, - call_credentials, - compression, - wait_for_ready, - timeout, - metadata, - _registered_method=True) diff --git a/programs/requirements.txt b/programs/requirements.txt deleted file mode 100644 index 56cbe21..0000000 --- a/programs/requirements.txt +++ /dev/null @@ -1,12 +0,0 @@ -cytoolz==1.0.0 -eth-hash==0.7.0 -eth-typing==5.0.0 -eth-utils==5.0.0 -eth_abi==5.1.0 -grpcio==1.66.2 -hexbytes==1.2.1 -parsimonious==0.10.0 -protobuf==5.28.2 -regex==2024.9.11 -toolz==1.0.0 -typing_extensions==4.12.2 diff --git a/script/Deploy.s.sol b/script/Deploy.s.sol new file mode 100644 index 0000000..bdcd70c --- /dev/null +++ b/script/Deploy.s.sol @@ -0,0 +1,78 @@ +// SPDX-License-Identifier: MIT +// solhint-disable no-console,ordering,custom-errors +pragma solidity 0.8.24; + +import {Pi} from "../src/Pi.sol"; +import {DeployConfig} from "./DeployConfig.s.sol"; +import {Deployer} from "./Deployer.sol"; +import {TransparentUpgradeableProxy} from + "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; +import {console} from "forge-std/console.sol"; + +contract Deploy is Deployer { + DeployConfig internal _cfg; + + /// @notice Modifier that wraps a function in broadcasting. + modifier broadcast() { + vm.startBroadcast(); + _; + vm.stopBroadcast(); + } + + /// @notice The name of the script, used to ensure the right deploy artifacts + /// are used. + function name() public pure override returns (string memory name_) { + name_ = "Deploy"; + } + + function setUp() public override { + super.setUp(); + string memory path = + string.concat(vm.projectRoot(), "/deploy-config/", deploymentContext, ".json"); + _cfg = new DeployConfig(path); + + console.log("Deploying from %s", deployScript); + console.log("Deployment context: %s", deploymentContext); + } + + /* solhint-disable comprehensive-interface */ + function run() external { + deployImplementations(); + + deployProxies(); + } + + /// @notice Deploy all of the proxies + function deployProxies() public { + deployProxy("Pi"); + } + + function deployProxy(string memory name_) public broadcast returns (address addr_) { + address logic = mustGetAddress(_stripSemver(name_)); + TransparentUpgradeableProxy proxy = new TransparentUpgradeableProxy({ + _logic: logic, + initialOwner: _cfg.proxyAdminOwner(), + _data: "" + }); + + string memory proxyName = string.concat(name_, "Proxy"); + save(proxyName, address(proxy)); + console.log("%s deployed at %s", proxyName, address(proxy)); + + addr_ = address(proxy); + } + + /// @notice Deploy all of the logic contracts + function deployImplementations() public broadcast { + deployPi(); + } + + function deployPi() public returns (address addr) { + console.log("Deploying Pi.sol"); + Pi pi = new Pi(_cfg.ovmTaskAddress(), _cfg.templateAdmin()); + + save("Pi", address(pi)); + console.log("Pi deployed at %s", address(pi)); + addr = address(pi); + } +} diff --git a/script/DeployConfig.s.sol b/script/DeployConfig.s.sol new file mode 100644 index 0000000..13379c5 --- /dev/null +++ b/script/DeployConfig.s.sol @@ -0,0 +1,34 @@ +// SPDX-License-Identifier: MIT +// solhint-disable private-vars-leading-underscore,no-console +pragma solidity 0.8.24; + +import {Script, console} from "forge-std/Script.sol"; +import {stdJson} from "forge-std/StdJson.sol"; + +/// @title DeployConfig +/// @notice Represents the configuration required to deploy the system. It is expected +/// to read the file from JSON. A future improvement would be to have fallback +/// values if they are not defined in the JSON themselves. +contract DeployConfig is Script { + string internal _json; + + address public proxyAdminOwner; + address public templateAdmin; + address public ovmTaskAddress; + + constructor(string memory _path) { + console.log("DeployConfig: reading file %s", _path); + try vm.readFile(_path) returns (string memory data) { + _json = data; + } catch { + console.log( + "Warning: unable to read config. Do not deploy unless you are not using config." + ); + return; + } + + proxyAdminOwner = stdJson.readAddress(_json, "$.proxyAdminOwner"); + templateAdmin = stdJson.readAddress(_json, "$.templateAdmin"); + ovmTaskAddress = stdJson.readAddress(_json, "$.ovmTaskAddress"); + } +} diff --git a/script/Deployer.sol b/script/Deployer.sol new file mode 100644 index 0000000..3675d76 --- /dev/null +++ b/script/Deployer.sol @@ -0,0 +1,528 @@ +// SPDX-License-Identifier: MIT +// solhint-disable private-vars-leading-underscore,no-console,no-empty-blocks,ordering,quotes +pragma solidity 0.8.24; + +import {Chains} from "./lib/Chains.sol"; +import {Executables} from "./lib/Executables.sol"; +import {Script} from "forge-std/Script.sol"; +import {stdJson} from "forge-std/StdJson.sol"; +import {console2 as console} from "forge-std/console2.sol"; + +/// @notice store the new deployment to be saved +struct Deployment { + string name; + address payable addr; +} + +/// @notice A `hardhat-deploy` style artifact +struct Artifact { + string abi; + address addr; + string[] args; + bytes bytecode; + bytes deployedBytecode; + string devdoc; + string metadata; + uint256 numDeployments; + string receipt; + bytes32 solcInputHash; + string storageLayout; + bytes32 transactionHash; + string userdoc; +} + +/// @title Deployer +/// @author tynes +/// @notice A contract that can make deploying and interacting with deployments easy. +/// When a contract is deployed, call the `save` function to write its name and +/// contract address to disk. Then the `sync` function can be called to generate +/// hardhat deploy style artifacts. Forked from `forge-deploy`. +abstract contract Deployer is Script { + /// @notice The set of deployments that have been done during execution. + mapping(string name => Deployment deployment) internal _namedDeployments; + /// @notice The same as `_namedDeployments` but as an array. + Deployment[] internal _newDeployments; + /// @notice The namespace for the deployment. Can be set with the env var DEPLOYMENT_CONTEXT. + string internal deploymentContext; + /// @notice Path to the deploy artifact generated by foundry + string internal deployPath; + /// @notice Path to the directory containing the hh deploy style artifacts + string internal deploymentsDir; + /// @notice The name of the deploy script that sends the transactions. + /// Can be modified with the env var DEPLOY_SCRIPT + string internal deployScript; + /// @notice The path to the temp deployments file + string internal tempDeploymentsPath; + /// @notice Error for when attempting to fetch a deployment and it does not exist + + error DeploymentDoesNotExist(string); + /// @notice Error for when trying to save an invalid deployment + error InvalidDeployment(string); + /// @notice The storage slot that holds the address of the implementation. + /// bytes32(uint256(keccak256('eip1967.proxy.implementation')) - 1) + + bytes32 internal constant IMPLEMENTATION_KEY = + 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc; + /// @notice The storage slot that holds the address of the owner. + /// bytes32(uint256(keccak256('eip1967.proxy.admin')) - 1) + bytes32 internal constant OWNER_KEY = + 0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103; + + /// @notice Create the global variables and set up the filesystem. + /// Forge script will create a file where the prefix is the + /// name of the function that runs with the suffix `-latest.json`. + /// By default, `run()` is called. Allow the user to use the SIG + /// env var to specify what function signature was called so that + /// the `sync()` method can be used to create hardhat deploy style + /// artifacts. + function setUp() public virtual { + string memory root = vm.projectRoot(); + deployScript = vm.envOr("DEPLOY_SCRIPT", name()); + + deploymentContext = _getDeploymentContext(); + string memory sig = vm.envOr("SIG", string("run")); + string memory deployFile = vm.envOr("DEPLOY_FILE", string.concat(sig, "-latest.json")); + uint256 chainId = vm.envOr("CHAIN_ID", block.chainid); + deployPath = string.concat( + root, "/broadcast/", deployScript, ".s.sol/", vm.toString(chainId), "/", deployFile + ); + + deploymentsDir = string.concat(root, "/deployments/", deploymentContext); + try vm.createDir(deploymentsDir, true) {} catch (bytes memory) {} + + string memory chainIdPath = string.concat(deploymentsDir, "/.chainId"); + + try vm.readFile(chainIdPath) returns (string memory localChainId) { + if (vm.envOr("STRICT_DEPLOYMENT", true)) { + require(vm.parseUint(localChainId) == chainId, "Misconfigured networks"); + } + } catch { + vm.writeFile(chainIdPath, vm.toString(chainId)); + } + console.log("Connected to network with chainid %s", chainId); + + tempDeploymentsPath = string.concat(deploymentsDir, "/.deploy"); + try vm.readFile(tempDeploymentsPath) returns (string memory) {} + catch { + vm.writeJson("{}", tempDeploymentsPath); + } + console.log("Storing temp deployment data in %s", tempDeploymentsPath); + } + + /// @notice Call this function to sync the deployment artifacts such that + /// hardhat deploy style artifacts are created. + // solhint-disable-next-line function-max-lines + function sync() public { + Deployment[] memory deployments = _getTempDeployments(); + console.log("Syncing %s deployments", deployments.length); + console.log("Using deployment artifact %s", deployPath); + + for (uint256 i; i < deployments.length; i++) { + address addr = deployments[i].addr; + string memory deploymentName = deployments[i].name; + + string memory deployTx = _getDeployTransactionByContractAddress(addr); + string memory contractName = _getContractNameFromDeployTransaction(deployTx); + console.log("Syncing deployment %s: contract %s", deploymentName, contractName); + + string[] memory args = getDeployTransactionConstructorArguments(deployTx); + bytes memory code = ""; + bytes memory deployedCode = ""; + string memory receipt = _getDeployReceiptByContractAddress(addr); + + string memory artifactPath = string.concat(deploymentsDir, "/", deploymentName, ".json"); + + uint256 numDeployments = 0; + try vm.readFile(artifactPath) returns (string memory res) { + numDeployments = stdJson.readUint(string(res), "$.numDeployments"); + vm.removeFile(artifactPath); + } catch {} + numDeployments++; + + Artifact memory artifact = Artifact({ + abi: getAbi(contractName), + addr: addr, + args: args, + bytecode: code, + deployedBytecode: deployedCode, + devdoc: getDevDoc(contractName), + metadata: getMetadata(contractName), + numDeployments: numDeployments, + receipt: receipt, + solcInputHash: bytes32(0), + storageLayout: getStorageLayout(contractName), + transactionHash: stdJson.readBytes32(deployTx, "$.hash"), + userdoc: getUserDoc(contractName) + }); + + string memory json = _serializeArtifact(artifact); + + vm.writeJson({json: json, path: artifactPath}); + } + + console.log("Synced temp deploy files, deleting %s", tempDeploymentsPath); + vm.removeFile(tempDeploymentsPath); + } + + /// @notice Returns the name of the deployment script. Children contracts + /// must implement this to ensure that the deploy artifacts can be found. + function name() public pure virtual returns (string memory); + + /// @notice Returns all of the deployments done in the current context. + function newDeployments() external view returns (Deployment[] memory) { + return _newDeployments; + } + + /// @notice Returns whether or not a particular deployment exists. + /// @param _name The name of the deployment. + /// @return Whether the deployment exists or not. + function has(string memory _name) public view returns (bool) { + Deployment memory existing = _namedDeployments[_name]; + if (existing.addr != address(0)) { + return bytes(existing.name).length > 0; + } + return _getExistingDeploymentAdress(_name) != address(0); + } + + /// @notice Returns the address of a deployment. + /// @param _name The name of the deployment. + /// @return The address of the deployment. May be `address(0)` if the deployment does not + /// exist. + function getAddress(string memory _name) public view returns (address payable) { + Deployment memory existing = _namedDeployments[_name]; + if (existing.addr != address(0)) { + if (bytes(existing.name).length == 0) { + return payable(address(0)); + } + return existing.addr; + } + return _getExistingDeploymentAdress(_name); + } + + /// @notice Returns the address of a deployment and reverts if the deployment + /// does not exist. + /// @return The address of the deployment. + function mustGetAddress(string memory _name) public view returns (address payable) { + address addr = getAddress(_name); + if (addr == address(0)) { + revert DeploymentDoesNotExist(_name); + } + return payable(addr); + } + + /// @notice Returns a deployment that is suitable to be used to interact with contracts. + /// @param _name The name of the deployment. + /// @return The deployment. + function get(string memory _name) public view returns (Deployment memory) { + Deployment memory deployment = _namedDeployments[_name]; + if (deployment.addr != address(0)) { + return deployment; + } else { + return _getExistingDeployment(_name); + } + } + + /// @notice Writes a deployment to disk as a temp deployment so that the + /// hardhat deploy artifact can be generated afterwards. + /// @param _name The name of the deployment. + /// @param _deployed The address of the deployment. + function save(string memory _name, address _deployed) public { + if (bytes(_name).length == 0) { + revert InvalidDeployment("EmptyName"); + } + if (bytes(_namedDeployments[_name].name).length > 0) { + revert InvalidDeployment("AlreadyExists"); + } + + Deployment memory deployment = Deployment({name: _name, addr: payable(_deployed)}); + _namedDeployments[_name] = deployment; + _newDeployments.push(deployment); + _writeTemp(_name, _deployed); + } + + /// @notice Reads the temp deployments from disk that were generated + /// by the deploy script. + /// @return An array of deployments. + function _getTempDeployments() internal returns (Deployment[] memory) { + string memory json = vm.readFile(tempDeploymentsPath); + string[] memory cmd = new string[](3); + cmd[0] = Executables.bash; + cmd[1] = "-c"; + cmd[2] = string.concat(Executables.jq, " 'keys' <<< '", json, "'"); + bytes memory res = vm.ffi(cmd); + string[] memory names = stdJson.readStringArray(string(res), ""); + + Deployment[] memory deployments = new Deployment[](names.length); + for (uint256 i; i < names.length; i++) { + string memory contractName = names[i]; + address addr = stdJson.readAddress(json, string.concat("$.", contractName)); + deployments[i] = Deployment({name: contractName, addr: payable(addr)}); + } + return deployments; + } + + /// @notice Returns the json of the deployment transaction given a contract address. + function _getDeployTransactionByContractAddress(address _addr) + internal + returns (string memory) + { + string[] memory cmd = new string[](3); + cmd[0] = Executables.bash; + cmd[1] = "-c"; + cmd[2] = string.concat( + Executables.jq, + " -r '.transactions[] | select(.contractAddress == ", + '"', + vm.toString(_addr), + '"', + ") | select(.transactionType == ", + '"CREATE"', + ")' < ", + deployPath + ); + bytes memory res = vm.ffi(cmd); + return string(res); + } + + /// @notice Returns the contract name from a deploy transaction. + function _getContractNameFromDeployTransaction(string memory _deployTx) + internal + pure + returns (string memory) + { + return stdJson.readString(_deployTx, ".contractName"); + } + + /// @notice Wrapper for vm.getCode that handles semver in the name. + function _getCode(string memory _name) internal returns (bytes memory) { + string memory fqn = _getFullyQualifiedName(_name); + console.log("fqn:%s", fqn); + bytes memory code = vm.getCode(fqn); + return code; + } + + /// @notice Wrapper for vm.getDeployedCode that handles semver in the name. + function _getDeployedCode(string memory _name) internal returns (bytes memory) { + string memory fqn = _getFullyQualifiedName(_name); + bytes memory code = vm.getDeployedCode(fqn); + return code; + } + + /// @notice Removes the semantic versioning from a contract name. The semver will exist if the + /// contract is compiled + /// more than once with different versions of the compiler. + function _stripSemver(string memory _name) internal returns (string memory) { + string[] memory cmd = new string[](3); + cmd[0] = Executables.bash; + cmd[1] = "-c"; + cmd[2] = string.concat( + Executables.echo, + " ", + _name, + " | ", + Executables.sed, + " -E 's/[.][0-9]+\\.[0-9]+\\.[0-9]+//g'" + ); + bytes memory res = vm.ffi(cmd); + return string(res); + } + + /// @notice Returns the constructor argument of a deployment transaction given a transaction + /// json. + function getDeployTransactionConstructorArguments(string memory _transaction) + internal + returns (string[] memory) + { + string[] memory cmd = new string[](3); + cmd[0] = Executables.bash; + cmd[1] = "-c"; + cmd[2] = string.concat(Executables.jq, " -r '.arguments' <<< '", _transaction, "'"); + bytes memory res = vm.ffi(cmd); + + string[] memory args = new string[](0); + if (keccak256(bytes("null")) != keccak256(res)) { + args = stdJson.readStringArray(string(res), ""); + } + return args; + } + + /// @notice Builds the fully qualified name of a contract. Assumes that the + /// file name is the same as the contract name but strips semver for the file name. + function _getFullyQualifiedName(string memory _name) internal returns (string memory) { + string memory sanitized = _stripSemver(_name); + return string.concat(sanitized, ".sol:", _name); + } + + /// @notice Returns the filesystem path to the artifact path. Assumes that the name of the + /// file matches the name of the contract. + function _getForgeArtifactPath(string memory _name) internal returns (string memory) { + string[] memory cmd = new string[](3); + cmd[0] = Executables.bash; + cmd[1] = "-c"; + cmd[2] = string.concat(Executables.forge, " config --json | ", Executables.jq, " -r .out"); + bytes memory res = vm.ffi(cmd); + string memory contractName = _stripSemver(_name); + string memory forgeArtifactPath = string.concat( + vm.projectRoot(), "/", string(res), "/", contractName, ".sol/", _name, ".json" + ); + return forgeArtifactPath; + } + + /// @notice Returns the forge artifact given a contract name. + function _getForgeArtifact(string memory _name) internal returns (string memory) { + string memory forgeArtifactPath = _getForgeArtifactPath(_name); + string memory forgeArtifact = vm.readFile(forgeArtifactPath); + return forgeArtifact; + } + + /// @notice Returns the receipt of a deployment transaction. + function _getDeployReceiptByContractAddress(address addr) internal returns (string memory) { + string[] memory cmd = new string[](3); + cmd[0] = Executables.bash; + cmd[1] = "-c"; + cmd[2] = string.concat( + Executables.jq, + " -r '.receipts[] | select(.contractAddress == ", + '"', + vm.toString(addr), + '"', + ")' < ", + deployPath + ); + bytes memory res = vm.ffi(cmd); + string memory receipt = string(res); + return receipt; + } + + /// @notice Returns the devdoc for a deployed contract. + function getDevDoc(string memory _name) internal returns (string memory) { + string[] memory cmd = new string[](3); + cmd[0] = Executables.bash; + cmd[1] = "-c"; + cmd[2] = string.concat(Executables.jq, " -r '.devdoc' < ", _getForgeArtifactPath(_name)); + bytes memory res = vm.ffi(cmd); + return string(res); + } + + /// @notice Returns the storage layout for a deployed contract. + function getStorageLayout(string memory _name) internal returns (string memory) { + string[] memory cmd = new string[](3); + cmd[0] = Executables.bash; + cmd[1] = "-c"; + cmd[2] = + string.concat(Executables.jq, " -r '.storageLayout' < ", _getForgeArtifactPath(_name)); + bytes memory res = vm.ffi(cmd); + return string(res); + } + + /// @notice Returns the abi for a deployed contract. + function getAbi(string memory _name) internal returns (string memory) { + string[] memory cmd = new string[](3); + cmd[0] = Executables.bash; + cmd[1] = "-c"; + cmd[2] = string.concat(Executables.jq, " -r '.abi' < ", _getForgeArtifactPath(_name)); + bytes memory res = vm.ffi(cmd); + return string(res); + } + + /// @notice Returns the userdoc for a deployed contract. + function getUserDoc(string memory _name) internal returns (string memory) { + string[] memory cmd = new string[](3); + cmd[0] = Executables.bash; + cmd[1] = "-c"; + cmd[2] = string.concat(Executables.jq, " -r '.userdoc' < ", _getForgeArtifactPath(_name)); + bytes memory res = vm.ffi(cmd); + return string(res); + } + + /// @notice + function getMetadata(string memory _name) internal returns (string memory) { + string[] memory cmd = new string[](3); + cmd[0] = Executables.bash; + cmd[1] = "-c"; + cmd[2] = string.concat( + Executables.jq, " '.metadata | tostring' < ", _getForgeArtifactPath(_name) + ); + bytes memory res = vm.ffi(cmd); + return string(res); + } + + /// @notice Adds a deployment to the temp deployments file + function _writeTemp(string memory _name, address _deployed) internal { + vm.writeJson({json: stdJson.serialize("", _name, _deployed), path: tempDeploymentsPath}); + } + + /// @notice Turns an Artifact into a json serialized string + /// @param _artifact The artifact to serialize + /// @return The json serialized string + function _serializeArtifact(Artifact memory _artifact) internal returns (string memory) { + string memory json = ""; + json = stdJson.serialize("", "address", _artifact.addr); + json = stdJson.serialize("", "abi", _artifact.abi); + json = stdJson.serialize("", "args", _artifact.args); + json = stdJson.serialize("", "bytecode", _artifact.bytecode); + json = stdJson.serialize("", "deployedBytecode", _artifact.deployedBytecode); + json = stdJson.serialize("", "devdoc", _artifact.devdoc); + json = stdJson.serialize("", "metadata", _artifact.metadata); + json = stdJson.serialize("", "numDeployments", _artifact.numDeployments); + json = stdJson.serialize("", "receipt", _artifact.receipt); + json = stdJson.serialize("", "solcInputHash", _artifact.solcInputHash); + json = stdJson.serialize("", "storageLayout", _artifact.storageLayout); + json = stdJson.serialize("", "transactionHash", _artifact.transactionHash); + json = stdJson.serialize("", "userdoc", _artifact.userdoc); + return json; + } + + /// @notice The context of the deployment is used to namespace the artifacts. + /// An unknown context will use the chainid as the context name. + // solhint-disable-next-line code-complexity + function _getDeploymentContext() private view returns (string memory) { + string memory context = vm.envOr("DEPLOYMENT_CONTEXT", string("")); + if (bytes(context).length > 0) { + return context; + } + + uint256 chainid = vm.envOr("CHAIN_ID", block.chainid); + if (chainid == Chains.Mainnet) { + return "mainnet"; + } else if (chainid == Chains.Goerli) { + return "goerli"; + } else if (chainid == Chains.OPMainnet) { + return "optimism-mainnet"; + } else if (chainid == Chains.Sepolia) { + return "sepolia"; + } else if (chainid == Chains.LocalDevNet) { + return "devnet"; + } else { + return vm.toString(chainid); + } + } + + /// @notice Reads the artifact from the filesystem by name and returns the address. + /// @param _name The name of the artifact to read. + /// @return The address of the artifact. + function _getExistingDeploymentAdress(string memory _name) + internal + view + returns (address payable) + { + return _getExistingDeployment(_name).addr; + } + + /// @notice Reads the artifact from the filesystem by name and returns the Deployment. + /// @param _name The name of the artifact to read. + /// @return The deployment corresponding to the name. + function _getExistingDeployment(string memory _name) + internal + view + returns (Deployment memory) + { + string memory path = string.concat(deploymentsDir, "/", _name, ".json"); + try vm.readFile(path) returns (string memory json) { + bytes memory addr = stdJson.parseRaw(json, "$.address"); + return Deployment({addr: abi.decode(addr, (address)), name: _name}); + } catch { + return Deployment({addr: payable(address(0)), name: ""}); + } + } +} diff --git a/script/lib/Chains.sol b/script/lib/Chains.sol new file mode 100644 index 0000000..8ade02a --- /dev/null +++ b/script/lib/Chains.sol @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: MIT +// solhint-disable private-vars-leading-underscore +pragma solidity 0.8.24; + +/// @notice Chain IDs for the various networks. +library Chains { + uint256 internal constant Mainnet = 1; + uint256 internal constant OPMainnet = 10; + uint256 internal constant Goerli = 5; + uint256 internal constant Sepolia = 11155111; + uint256 internal constant LocalDevNet = 31337; +} diff --git a/script/lib/Executables.sol b/script/lib/Executables.sol new file mode 100644 index 0000000..49f4227 --- /dev/null +++ b/script/lib/Executables.sol @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: MIT +// solhint-disable private-vars-leading-underscore +pragma solidity 0.8.24; + +/// @notice The executables used in ffi commands. These are set here +/// to have a single source of truth in case absolute paths +/// need to be used. +library Executables { + string internal constant bash = "bash"; + string internal constant jq = "jq"; + string internal constant forge = "forge"; + string internal constant echo = "echo"; + string internal constant sed = "sed"; +} diff --git a/src/Pi.sol b/src/Pi.sol index f66605b..90a7a23 100644 --- a/src/Pi.sol +++ b/src/Pi.sol @@ -2,7 +2,12 @@ pragma solidity ^0.8.13; import {OVMClient} from "@webisopen/ovm-contracts/src/OVMClient.sol"; -import {ExecMode, Requirement, Specification, Arch} from "@webisopen/ovm-contracts/src/libraries/DataTypes.sol"; +import { + Arch, + ExecMode, + Requirement, + Specification +} from "@webisopen/ovm-contracts/src/libraries/DataTypes.sol"; event ResponseParsed(bytes32 requestId, bool success, string strPI); @@ -27,7 +32,8 @@ contract Pi is OVMClient { spec.repoTag = "0xb6a6502fa480fd1fb5bf95c1fb1366bcbc335a08356c2a97daf6bc44e9cc0253"; spec.license = "WTFPL"; spec.entrypoint = "src/main.py"; - spec.requirement = Requirement({ram: "256mb", disk: "5mb", timeout: 600, cpu: 1, gpu: false}); + spec.requirement = + Requirement({ram: "256mb", disk: "5mb", timeout: 600, cpu: 1, gpu: false}); spec.apiABIs = '[{"request":{"type":"function","name":"getResponse","inputs":[{"name":"requestId","type":"bytes32","internalType":"bytes32"}],"outputs":[{"name":"","type":"string","internalType":"string"}],"stateMutability":"view"},"getResponse":{"type":"function","name":"getResponse","inputs":[{"name":"requestId","type":"bytes32","internalType":"bytes32"}],"outputs":[{"name":"","type":"string","internalType":"string"}],"stateMutability":"view"}}]'; spec.royalty = 5; diff --git a/test/Pi.t.sol b/test/Pi.t.sol index da176fd..8c81edf 100644 --- a/test/Pi.t.sol +++ b/test/Pi.t.sol @@ -1,7 +1,7 @@ pragma solidity 0.8.24; -import {Test} from "forge-std/Test.sol"; import {Pi, ResponseParsed} from "../src/Pi.sol"; +import {Test} from "forge-std/Test.sol"; contract PiTest is Test { address public constant alice = address(0x1111);