diff --git a/.solhint.json b/.solhint.json index 898adb5..4439c72 100644 --- a/.solhint.json +++ b/.solhint.json @@ -3,6 +3,9 @@ "rules": { "compiler-version": ["error", "^0.8"], "import-path-check": "off", - "use-natspec": "warn" + "use-natspec": "warn", + "func-visibility": ["error", { "ignoreConstructors": true }], + "no-complex-fallback": "off", + "no-inline-assembly": "off" } } diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..6d347b4 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "solidity.compileUsingRemoteVersion": "v0.8.34+commit.80d5c536" +} diff --git a/README.md b/README.md index 3dfdb98..460b4de 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,8 @@ -# Contract Template +# Solver7702Delegate -Template for creating new smart contract projects. +`Solver7702Delegate` is a minimal ERC-7702 delegation target for CoW Protocol solvers. It lets a solver keep using its existing solver EOA while allowing a fixed set of auxiliary EOAs to submit transactions through that solver EOA. The main benefit is parallel settlement submission: auxiliary EOAs provide independent nonce lanes, while downstream contracts still see the solver EOA as `msg.sender`, which keeps the authorization clean. -This project is meant to be used as a templated during the creation of new Github repositories (will show in the `Create a new repository > Configuration > Start with a template` selector). - -It will contain some useful configuration files and scripts, that can be used also with existing projects (manually copied). +Read more about the initiative [here](https://www.notion.so/cownation/Solver7702Delegate-Design-Doc-3588da5f04ca80a1b521c436abf17724). ## Usage @@ -88,37 +86,45 @@ just snapshot ### Deploy +The deploy script reads the five approved caller addresses from environment variables: + +```shell +export APPROVED_CALLER_0= +export APPROVED_CALLER_1= +export APPROVED_CALLER_2= +export APPROVED_CALLER_3= +export APPROVED_CALLER_4= + +just forge script script/DeploySolver7702Delegate.s.sol:DeploySolver7702Delegate \ + --rpc-url \ + --private-key \ + --broadcast +``` + +This deploys without `CREATE2`. + +To deploy with `CREATE2`, set `SALT`. Exact `0x`-prefixed 32-byte hex values are used directly as the `CREATE2` salt. Any other value is treated as a string and hashed as `keccak256(bytes(SALT))`: + +```shell +export SALT= + +just forge script script/DeploySolver7702Delegate.s.sol:DeploySolver7702Delegate \ + --rpc-url \ + --private-key \ + --broadcast +``` + +When `SALT` is set, Foundry uses the canonical `CREATE2` deployer `0x4e59b44847b379578588920cA78FbF26c0B4956C`, unless a different address is passed with `--create2-deployer`. +The `CREATE2` address is deterministic. To get the same address across networks, use the same `CREATE2` deployer address, salt, bytecode, and approved caller addresses. + +To compute the `CREATE2` address before deployment with the approved caller addresses from the same environment variables: + ```shell -just forge script script/Counter.s.sol:CounterScript --rpc-url --private-key +just forge script script/DeploySolver7702Delegate.s.sol:DeploySolver7702Delegate --sig predictAddress ``` -## New project creation checklist - -The following operations need to be performed after this repository has been created. - -- [ ] Discuss and confirm the project license with the team lead before starting implementation work. You must set this up before writing project code. - - [ ] The license is very likely going to be one of the following: - - [ ] `MIT OR Apache-2.0` for projects with low strategic relevance (included by default in the template). - - [ ] `LGPL-3.0-or-later` for projects with high strategic relevance. - - [ ] In some cases, a different license may be needed. - - [ ] If it's `MIT OR Apache-2.0`, the license is already included. Otherwise, remove the existing license files and add the selected license as a file in the repository root. - - [ ] Update `dev/package.json` with the selected license. - - [ ] Update each Solidity smart contract's `SPDX-License-Identifier` with the selected license. -- [ ] In GitHub repo settings: - - [ ] Add a new ruleset called "Protected branches" and include the following changes: - - Enforcement status: active - - Target branches: Include default branch - - Require linear history - - Require a pull request before merging - - Required approvals: 1 - - Allowed merge methods: Squash - - Block force pushes - - [ ] In General → Features → Pull requests: - - Select "Pull request title and description" in "Default commit message" option - - Unckeck "Allow merge commits" option - - Check "Allow auto-merge" option -- [ ] Run `just forge install` to install the dependencies. This will create a new `foundry.lock` file which you should commit to the project -- [ ] Set up [Local tooling](#local-tooling) so Solhint and Slither use the pinned project versions -- [ ] Update the project details in `dev/package.json`, including `name` and `description` -- [ ] Make sure you use the [latest version of Solidity](https://github.com/argotorg/solidity/releases) by updating the `solc` version in `foundry.toml` -- [ ] Once all entries in this list are checked, delete this section from the readme +Set `CREATE2_DEPLOYER` to the address passed with `--create2-deployer` if you override Foundry's default: + +```shell +export CREATE2_DEPLOYER= +``` diff --git a/dev/package.json b/dev/package.json index 96069ce..9d3ceee 100644 --- a/dev/package.json +++ b/dev/package.json @@ -1,8 +1,8 @@ { - "name": "contracts-template-dev", + "name": "solver-7702-delegate", "version": "0.0.0", "private": true, - "description": "Local development dependencies for the contracts template.", + "description": "Local development dependencies for the Solver7702Delegate contract.", "license": "(MIT OR Apache-2.0)", "packageManager": "pnpm@10.33.2", "devDependencies": { diff --git a/foundry.toml b/foundry.toml index 1a8cbfc..c012882 100644 --- a/foundry.toml +++ b/foundry.toml @@ -7,6 +7,7 @@ solc = "0.8.34" # See latest release at: https://github.com/argotorg/solidity/re [fmt] sort_imports = true number_underscore = "thousands" +wrap_comments = true [profile.ci] deny = "warnings" # Why not always: sometimes you just want to code and see what comes out diff --git a/script/Counter.s.sol b/script/Counter.s.sol deleted file mode 100644 index b882199..0000000 --- a/script/Counter.s.sol +++ /dev/null @@ -1,20 +0,0 @@ -// SPDX-License-Identifier: MIT OR Apache-2.0 -pragma solidity ^0.8; - -import {Counter} from "../src/Counter.sol"; -import {Script} from "forge-std/Script.sol"; - -/// @title CounterScript -/// @author CoW DAO Developers -/// @notice Script to deploy the Counter contract -contract CounterScript is Script { - /// @notice The deployed Counter contract - Counter public counter; - - /// @notice Run the script - function run() public { - vm.startBroadcast(); - counter = new Counter(); - vm.stopBroadcast(); - } -} diff --git a/script/DeploySolver7702Delegate.s.sol b/script/DeploySolver7702Delegate.s.sol new file mode 100644 index 0000000..c579e00 --- /dev/null +++ b/script/DeploySolver7702Delegate.s.sol @@ -0,0 +1,67 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 +pragma solidity ^0.8.34; + +import {Script} from "forge-std/Script.sol"; + +import {Solver7702Delegate} from "../src/Solver7702Delegate.sol"; + +/// @title DeploySolver7702Delegate +/// @author CoW Foundation +/// @notice Deploys Solver7702Delegate with five approved caller addresses. +contract DeploySolver7702Delegate is Script { + /// @notice Foundry's default CREATE2 deployer. + address internal constant DEFAULT_CREATE2_DEPLOYER = 0x4e59b44847b379578588920cA78FbF26c0B4956C; + + /// @notice Deploys Solver7702Delegate using approved caller environment variables. + /// @return solver7702Delegate The deployed Solver7702Delegate contract. + function run() external returns (Solver7702Delegate solver7702Delegate) { + address[5] memory approvedCallers = getApprovedCallers(); + + vm.startBroadcast(); + if (vm.envExists("SALT")) { + solver7702Delegate = new Solver7702Delegate{salt: getSalt()}(approvedCallers); + } else { + solver7702Delegate = new Solver7702Delegate(approvedCallers); + } + vm.stopBroadcast(); + } + + /// @notice Predicts the CREATE2 address using environment variables. + /// @dev Reads `SALT`, approved callers, and optional `CREATE2_DEPLOYER`. + /// @return The predicted Solver7702Delegate address. + function predictAddress() external view returns (address) { + address deployer = + vm.envExists("CREATE2_DEPLOYER") ? vm.envAddress("CREATE2_DEPLOYER") : DEFAULT_CREATE2_DEPLOYER; + address[5] memory approvedCallers = getApprovedCallers(); + bytes32 salt = getSalt(); + bytes memory initCode = abi.encodePacked(type(Solver7702Delegate).creationCode, abi.encode(approvedCallers)); + bytes32 addressHash = keccak256(abi.encodePacked(bytes1(0xff), deployer, salt, keccak256(initCode))); + + return address(uint160(uint256(addressHash))); + } + + /// @notice Reads the approved caller addresses from environment variables. + /// @return approvedCallers The approved caller addresses. + function getApprovedCallers() internal view returns (address[5] memory approvedCallers) { + approvedCallers = [ + vm.envAddress("APPROVED_CALLER_0"), + vm.envAddress("APPROVED_CALLER_1"), + vm.envAddress("APPROVED_CALLER_2"), + vm.envAddress("APPROVED_CALLER_3"), + vm.envAddress("APPROVED_CALLER_4") + ]; + } + + /// @notice Reads the CREATE2 salt from the `SALT` environment variable. + /// @dev Exact `0x`-prefixed 32-byte hex strings are used directly; all other strings are hashed. + /// @return The CREATE2 salt. + function getSalt() internal view returns (bytes32) { + string memory salt = vm.envString("SALT"); + bytes memory saltBytes = bytes(salt); + if (saltBytes.length == 66 && saltBytes[0] == "0" && (saltBytes[1] == "x" || saltBytes[1] == "X")) { + return vm.parseBytes32(salt); + } + + return keccak256(bytes(salt)); + } +} diff --git a/slither.config.json b/slither.config.json index dff06dd..2f38302 100644 --- a/slither.config.json +++ b/slither.config.json @@ -1,4 +1,4 @@ { - "detectors_to_exclude": "solc-version", + "detectors_to_exclude": "solc-version,naming-convention,assembly", "filter_paths": "/(lib|test|script)/" } diff --git a/src/Counter.sol b/src/Counter.sol deleted file mode 100644 index 0f01178..0000000 --- a/src/Counter.sol +++ /dev/null @@ -1,21 +0,0 @@ -// SPDX-License-Identifier: MIT OR Apache-2.0 -pragma solidity ^0.8; - -/// @title Counter -/// @author CoW DAO developers -/// @notice Simple counter contract used by the template. -contract Counter { - /// @notice The stored counter value. - uint256 public number; - - /// @notice Set the counter to a new value. - /// @param newNumber The value to store. - function setNumber(uint256 newNumber) public { - number = newNumber; - } - - /// @notice Increase the counter by one. - function increment() public { - ++number; - } -} diff --git a/src/Solver7702Delegate.sol b/src/Solver7702Delegate.sol new file mode 100644 index 0000000..a6ebc25 --- /dev/null +++ b/src/Solver7702Delegate.sol @@ -0,0 +1,87 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 +pragma solidity ^0.8.34; + +/// @title Solver7702Delegate +/// @author CoW Foundation +/// @notice ERC-7702 delegation target for solver EOAs +contract Solver7702Delegate { + /// @notice Error thrown when a caller is unauthorized + error Unauthorized(address sender); + + /// @notice Address of the first approved caller + address private immutable APPROVED_CALLER_0; + + /// @notice Address of the second approved caller + address private immutable APPROVED_CALLER_1; + + /// @notice Address of the third approved caller + address private immutable APPROVED_CALLER_2; + + /// @notice Address of the fourth approved caller + address private immutable APPROVED_CALLER_3; + + /// @notice Address of the fifth approved caller + address private immutable APPROVED_CALLER_4; + + /// @notice Constructor to initialize the approved callers + /// @param approvedCallers The addresses of the approved callers + constructor(address[5] memory approvedCallers) { + APPROVED_CALLER_0 = approvedCallers[0]; + APPROVED_CALLER_1 = approvedCallers[1]; + APPROVED_CALLER_2 = approvedCallers[2]; + APPROVED_CALLER_3 = approvedCallers[3]; + APPROVED_CALLER_4 = approvedCallers[4]; + } + + /// @notice Fallback function to handle calls to the delegate + fallback() external payable { + // Simply receive ETH + if (msg.data.length < 20) return; + + // Possibly short circuit by recognizing one of the approved callers + if (msg.sender == APPROVED_CALLER_0) return _callThrough(); + if (msg.sender == APPROVED_CALLER_1) return _callThrough(); + if (msg.sender == APPROVED_CALLER_2) return _callThrough(); + if (msg.sender == APPROVED_CALLER_3) return _callThrough(); + if (msg.sender == APPROVED_CALLER_4) return _callThrough(); + + // Revert if caller is unauthorized + revert Unauthorized(msg.sender); + } + + function _callThrough() internal { + // For our purposes, the target address is encoded as the first 20 bytes of the input data + address target = address(bytes20(msg.data[0:20])); + + assembly { + // Extract calldata in range (target, len(msg.data)). + // We take full control of memory in this inline assembly block because it will not return to Solidity code. + // This is why we overwrite the Solidity scratch pad at memory position 0. + calldatacopy(0x00, 20, sub(calldatasize(), 20)) + + // Call the implementation + let result := + call( + gas(), // gas - forward all of it + target, // target to call + callvalue(), // value - forward all Ether + 0x00, // input offset - pointer to calldata + sub(calldatasize(), 20), // input size - length of calldata + 0x00, // output offset - 0 because we don't know the size yet + 0x00 // output size - 0 because we don't know the size yet + ) + + // Copy return data into memory + returndatacopy(0x00, 0x00, returndatasize()) + + // Handle return data, 0 = revert / 1 = success + switch result + case 0 { + revert(0x00, returndatasize()) + } + default { + return(0x00, returndatasize()) + } + } + } +} diff --git a/test/Counter.t.sol b/test/Counter.t.sol deleted file mode 100644 index f2df65b..0000000 --- a/test/Counter.t.sol +++ /dev/null @@ -1,25 +0,0 @@ -// SPDX-License-Identifier: MIT OR Apache-2.0 -pragma solidity ^0.8; - -import {Counter} from "../src/Counter.sol"; -import {Test} from "forge-std/Test.sol"; - -contract CounterTest is Test { - Counter public counter; - - function setUp() public { - counter = new Counter(); - counter.setNumber(0); - } - - function test_Increment() public { - counter.increment(); - vm.snapshotGasLastCall("increment - success"); - assertEq(counter.number(), 1); - } - - function testFuzz_SetNumber(uint256 x) public { - counter.setNumber(x); - assertEq(counter.number(), x); - } -}