├── .github └── workflows │ └── test.yml ├── .gitignore ├── .gitmodules ├── .vscode └── settings.json ├── LICENSE ├── Makefile ├── README.md ├── bin ├── anvil ├── forge └── solc ├── foundry.toml ├── src ├── AbstractInvoker.sol ├── Auth.sol ├── BatchExecutor.sol └── Invoker.sol └── test ├── Auth.t.sol ├── Invoker.t.sol └── utils.sol /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | 3 | on: workflow_dispatch 4 | 5 | env: 6 | FOUNDRY_PROFILE: ci 7 | 8 | jobs: 9 | check: 10 | strategy: 11 | fail-fast: true 12 | 13 | name: Foundry project 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v4 17 | with: 18 | submodules: recursive 19 | 20 | - name: Install Foundry 21 | uses: foundry-rs/foundry-toolchain@v1 22 | with: 23 | version: nightly 24 | 25 | - name: Run Forge build 26 | run: | 27 | forge --version 28 | forge build --sizes 29 | id: build 30 | 31 | - name: Run Forge tests 32 | run: | 33 | forge test -vvv 34 | id: test 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiler files 2 | cache/ 3 | out/ 4 | 5 | # Ignores development broadcast logs 6 | !/broadcast 7 | /broadcast/*/31337/ 8 | /broadcast/**/dry-run/ 9 | 10 | # Docs 11 | docs/ 12 | 13 | # Dotenv file 14 | .env 15 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "lib/forge-std"] 2 | path = lib/forge-std 3 | url = https://github.com/foundry-rs/forge-std 4 | [submodule "lib/foundry"] 5 | path = lib/foundry 6 | url = https://github.com/jxom/foundry 7 | [submodule "lib/solidity"] 8 | path = lib/solidity 9 | url = https://github.com/clabby/solidity 10 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "solidity.compileUsingRemoteVersion": "latest" 3 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024-present Weth, LLC. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | all: build-forge-patch build-anvil-patch build-solc-patch 2 | 3 | .PHONY: build-anvil-patch 4 | build-anvil-patch: 5 | @echo "Building anvil patch..." 6 | @cd lib/foundry && \ 7 | cargo build --bin anvil --release && \ 8 | mkdir -p ../../bin && \ 9 | cp target/release/anvil ../../bin/anvil 10 | @echo "Done, patched anvil binary is located at `bin/anvil` relative to the project root" 11 | 12 | .PHONY: build-forge-patch 13 | build-forge-patch: 14 | @echo "Building forge patch..." 15 | @cd lib/foundry && \ 16 | cargo build --bin forge --release && \ 17 | mkdir -p ../../bin && \ 18 | cp target/release/forge ../../bin/forge 19 | @echo "Done, patched forge binary is located at `bin/forge` relative to the project root" 20 | 21 | .PHONY: build-solc-patch 22 | build-solc-patch: 23 | @echo "Building solc patch..." 24 | @cd lib/solidity && \ 25 | mkdir -p build && \ 26 | cd build && \ 27 | cmake .. && \ 28 | make && \ 29 | mkdir -p ../../../bin && \ 30 | cp solc/solc ../../../bin/solc 31 | @echo "Done, patched solc binary is located at `bin/solc` relative to the project root" 32 | 33 | .PHONY: test 34 | test: 35 | @[[ ! -a ./bin/forge ]] && make build-forge-patch || true 36 | @./bin/forge test -vv 37 | 38 | .PHONY: build 39 | build: 40 | @[[ ! -a ./bin/forge ]] && make build-forge-patch || true 41 | @./bin/forge build 42 | 43 | .PHONY: fmt 44 | fmt: 45 | @[[ ! -a ./bin/forge ]] && make build-forge-patch || true 46 | @./bin/forge fmt 47 | 48 | .PHONY: snapshot 49 | snapshot: 50 | @[[ ! -a ./bin/forge ]] && make build-forge-patch || true 51 | @./bin/forge snapshot 52 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | > [!WARNING] 2 | > THIS REPOSITORY HAS BEEN UPSTREAMED TO [`3074-invokers`](https://github.com/anton-rs/3074-invokers). 3 | 4 | # EIP-3074 Invoker 5 | 6 | Generalized EIP-3074 Invoker with batch transaction support. Inspired by @clabby's work on [`eip-3074-foundry`](https://github.com/clabby/eip-3074-foundry). 7 | 8 | ## Patches 9 | 10 | This repository contains patches (h/t @clabby) of the following repositories to support EIP-3074 opcodes: 11 | 12 | - [`revm`](https://github.com/jxom/revm/tree/jxom/eip-3074) 13 | - [`foundry`](https://github.com/jxom/foundry/tree/jxom/eip-3074) 14 | - [`solc`](https://github.com/clabby/solidity/tree/cl/eip-3074) 15 | 16 | ## Installation 17 | 18 | ``` 19 | git submodule update --init --recursive && make 20 | ``` 21 | 22 | ## Building Contracts 23 | 24 | ``` 25 | make build 26 | ``` 27 | 28 | ## Running Tests 29 | 30 | ``` 31 | make test 32 | ``` 33 | -------------------------------------------------------------------------------- /bin/anvil: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wevm/invoker/df10b1d6d18ee9253d10ebb82ffe82cdfe717d82/bin/anvil -------------------------------------------------------------------------------- /bin/forge: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wevm/invoker/df10b1d6d18ee9253d10ebb82ffe82cdfe717d82/bin/forge -------------------------------------------------------------------------------- /bin/solc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wevm/invoker/df10b1d6d18ee9253d10ebb82ffe82cdfe717d82/bin/solc -------------------------------------------------------------------------------- /foundry.toml: -------------------------------------------------------------------------------- 1 | [profile.default] 2 | src = "src" 3 | out = "out" 4 | libs = ["lib"] 5 | ffi = true 6 | prague = true 7 | remappings = [ 8 | "forge-std/=lib/forge-std/src/", 9 | ] 10 | solc = "bin/solc" 11 | -------------------------------------------------------------------------------- /src/AbstractInvoker.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.20; 3 | 4 | import {Auth} from "./Auth.sol"; 5 | 6 | /// @title Base Invoker interface & implementation. 7 | /// @author jxom 8 | /// @notice Shared functionality and interfaces for Invoker contracts. 9 | /// @custom:experimental This is an experimental contract. 10 | abstract contract AbstractInvoker is Auth { 11 | /// @dev This function must be implemented by the invoker implementation. 12 | function getCommit(bytes calldata data, address authority) virtual public view returns (bytes32 commit); 13 | 14 | /// @dev This function must be implemented by the invoker implementation. 15 | function exec(bytes calldata data, address authority) virtual internal; 16 | 17 | /// @notice Executes data on behalf of the authority, provided a signature that 18 | /// was signed by the authority. 19 | /// 20 | /// @param data Execution data. 21 | /// @param authority The authority to execute the calls on behalf of. 22 | /// @param signature The signature of the auth message signed by the authority. 23 | function execute(bytes calldata data, address authority, Signature calldata signature) external payable { 24 | auth(authority, getCommit(data, authority), signature); 25 | exec(data, authority); 26 | } 27 | 28 | /// @notice Computes the hash of the auth message. 29 | /// 30 | /// @param data Execution data. 31 | /// @param authority The authority to execute the data on behalf of. 32 | /// @param nonce The nonce of the authority. 33 | /// 34 | /// @return hash The hash of the auth message. 35 | function getAuthMessageHash(bytes calldata data, address authority, uint256 nonce) external view returns (bytes32) { 36 | return getAuthMessageHash(getCommit(data, authority), nonce); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Auth.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.20; 3 | 4 | /// @title Simple abstraction over the EIP-3074 AUTH and AUTHCALL opcodes. 5 | /// @author jxom 6 | /// @notice Use this contract to authenticate and execute calls with the AUTH and AUTHCALL opcodes, 7 | /// along with auth message hash computation. 8 | /// @custom:experimental This is an experimental contract. 9 | contract Auth { 10 | uint8 constant MAGIC = 0x04; 11 | 12 | error InvalidAuthArguments(); 13 | 14 | struct Call { 15 | bytes data; 16 | address to; 17 | uint256 value; 18 | } 19 | 20 | struct Signature { 21 | uint8 yParity; 22 | bytes32 r; 23 | bytes32 s; 24 | } 25 | 26 | /// @notice Computes the hash of the auth message, to be signed by the authority. 27 | /// 28 | /// @param nonce The nonce of the authority. 29 | /// @param commit The commit. 30 | /// 31 | /// @return hash Keccak256 hash of the auth message. 32 | function getAuthMessageHash(bytes32 commit, uint256 nonce) public view returns (bytes32) { 33 | bytes32 chainId = bytes32(block.chainid); 34 | bytes32 invokerAddress = bytes32(uint256(uint160(address(this)))); 35 | return keccak256(abi.encodePacked(MAGIC, chainId, nonce, invokerAddress, commit)); 36 | } 37 | 38 | /// @notice Authorize the sender to send calls on behalf of the authority. 39 | /// 40 | /// @param authority The authority to authorize with. 41 | /// @param commit The commit. 42 | /// @param signature The signature of the auth message. 43 | /// 44 | /// @return success True if the authorization is successful. 45 | function auth(address authority, bytes32 commit, Signature memory signature) public returns (bool success) { 46 | bytes memory args = abi.encodePacked(signature.yParity, signature.r, signature.s, commit); 47 | assembly { 48 | success := auth(authority, add(args, 0x20), mload(args)) 49 | } 50 | if (!success) revert InvalidAuthArguments(); 51 | } 52 | 53 | /// @notice Executes a call on behalf of the authority. 54 | /// 55 | /// @param call The call to execute. 56 | /// 57 | /// @return success True if the call is successful. 58 | function authcall(Call calldata call) public returns (bool success) { 59 | address to = call.to; 60 | uint256 value = call.value; 61 | bytes memory data = call.data; 62 | 63 | assembly { 64 | success := authcall(gas(), to, value, 0, add(data, 0x20), mload(data), 0, 0) 65 | if iszero(success) { 66 | returndatacopy(0, 0, returndatasize()) 67 | revert(0, returndatasize()) 68 | } 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/BatchExecutor.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.20; 3 | 4 | import {Auth} from "./Auth.sol"; 5 | import {AbstractInvoker} from "./AbstractInvoker.sol"; 6 | 7 | // Adapted from https://github.com/safe-global/safe-contracts/blob/main/contracts/libraries/MultiSendCallOnly.sol 8 | contract BatchExecutor { 9 | function executeCalls(bytes memory calls) public payable { 10 | assembly { 11 | let length := mload(calls) 12 | let i := 0x20 13 | for { 14 | // Pre block is not used in "while mode" 15 | } lt(i, length) { 16 | // Post block is not used in "while mode" 17 | } { 18 | // First byte of the data is the operation. 19 | // We shift by 248 bits (256 - 8 [operation byte]) it right since mload will always load 32 bytes (a word). 20 | // This will also zero out unused data. 21 | let operation := shr(0xf8, mload(add(calls, i))) 22 | // We offset the load address by 1 byte (operation byte) 23 | // We shift it right by 96 bits (256 - 160 [20 address bytes]) to right-align the data and zero out unused data. 24 | let to := shr(0x60, mload(add(calls, add(i, 0x01)))) 25 | // Defaults `to` to `address(this)` if `address(0)` is provided. 26 | to := or(to, mul(iszero(to), address())) 27 | // We offset the load address by 21 byte (operation byte + 20 address bytes) 28 | let value := mload(add(calls, add(i, 0x15))) 29 | // We offset the load address by 53 byte (operation byte + 20 address bytes + 32 value bytes) 30 | let dataLength := mload(add(calls, add(i, 0x35))) 31 | // We offset the load address by 85 byte (operation byte + 20 address bytes + 32 value bytes + 32 data length bytes) 32 | let data := add(calls, add(i, 0x55)) 33 | let success := 0 34 | switch operation 35 | // This version does not allow regular calls 36 | case 0 { revert(0, 0) } 37 | // This version does not allow delegatecalls 38 | case 1 { revert(0, 0) } 39 | case 2 { success := authcall(gas(), to, value, 0, data, dataLength, 0, 0) } 40 | if eq(success, 0) { 41 | let errorLength := returndatasize() 42 | returndatacopy(0, 0, errorLength) 43 | revert(0, errorLength) 44 | } 45 | // Next entry starts at 85 byte + data length 46 | i := add(i, add(0x55, dataLength)) 47 | } 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/Invoker.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.20; 3 | 4 | import {Auth} from "./Auth.sol"; 5 | import {AbstractInvoker} from "./AbstractInvoker.sol"; 6 | import {BatchExecutor} from "./BatchExecutor.sol"; 7 | 8 | /// @title Invoker with batch call support & replay protection. 9 | /// @author jxom 10 | /// @notice Use this contract to execute a batch of calls on behalf of an authority. 11 | /// @custom:experimental This is an experimental contract. 12 | contract Invoker is AbstractInvoker, BatchExecutor { 13 | /// @notice Nonces of the authorities. 14 | mapping(address => uint256) public nonces; 15 | 16 | /// @notice Computes the commit of a batch. 17 | /// 18 | /// @param data Calls to execute in format of packed bytes of: 19 | /// `operation` (uint8): operation type – must equal uint8(2) for AUTHCALL. 20 | /// `to` (address): address of the recipient. 21 | /// `value` (uint256): value in wei to send. 22 | /// `dataLength` (uint256): length of the data. 23 | /// `data` (bytes): calldata to send. 24 | /// @param authority The authority to execute the calls on behalf of. 25 | /// 26 | /// @return commit The commit of the batch. 27 | function getCommit(bytes calldata data, address authority) override public view returns (bytes32 commit) { 28 | return keccak256(abi.encodePacked(data, nonces[authority])); 29 | } 30 | 31 | /// @notice Executes calls on behalf of the authority, provided a signature that 32 | /// was signed by the authority. 33 | /// 34 | /// @param data Calls to execute in format of packed bytes of: 35 | /// `operation` (uint8): operation type – must equal uint8(2) for AUTHCALL. 36 | /// `to` (address): address of the recipient. 37 | /// `value` (uint256): value in wei to send. 38 | /// `dataLength` (uint256): length of the data. 39 | /// `data` (bytes): calldata to send. 40 | /// @param authority The authority to execute the calls on behalf of. 41 | function exec(bytes calldata data, address authority) override internal { 42 | nonces[authority]++; 43 | executeCalls(data); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /test/Auth.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8.20; 3 | 4 | import {Test, console2} from "forge-std/Test.sol"; 5 | import {VmSafe} from "forge-std/Vm.sol"; 6 | import {Auth} from "../src/Auth.sol"; 7 | import {vToYParity} from "./utils.sol"; 8 | 9 | contract AuthTest is Test { 10 | Auth public target; 11 | VmSafe.Wallet public authority; 12 | 13 | function setUp() public { 14 | target = new Auth(); 15 | authority = vm.createWallet("authority"); 16 | vm.label(address(target), "auth"); 17 | vm.label(authority.addr, "authority"); 18 | } 19 | 20 | function test_auth(bytes32 commit) external { 21 | uint64 nonce = vm.getNonce(address(authority.addr)); 22 | 23 | bytes32 hash = target.getAuthMessageHash(commit, nonce); 24 | (uint8 v, bytes32 r, bytes32 s) = vm.sign(authority.privateKey, hash); 25 | 26 | bool success = target.auth(authority.addr, commit, Auth.Signature({yParity: vToYParity(v), r: r, s: s})); 27 | assertTrue(success); 28 | } 29 | 30 | function test_auth_revert_invalidCommit(bytes32 commit) external { 31 | uint64 nonce = vm.getNonce(address(authority.addr)); 32 | 33 | bytes32 hash = target.getAuthMessageHash(commit, nonce); 34 | (uint8 v, bytes32 r, bytes32 s) = vm.sign(authority.privateKey, hash); 35 | 36 | bytes32 invalidCommit = keccak256("lol"); 37 | 38 | vm.expectRevert(Auth.InvalidAuthArguments.selector); 39 | target.auth(authority.addr, invalidCommit, Auth.Signature({yParity: vToYParity(v), r: r, s: s})); 40 | } 41 | 42 | function test_auth_revert_invalidAuthority(bytes32 commit) external { 43 | uint64 nonce = vm.getNonce(address(authority.addr)); 44 | 45 | bytes32 hash = target.getAuthMessageHash(commit, nonce); 46 | (uint8 v, bytes32 r, bytes32 s) = vm.sign(authority.privateKey, hash); 47 | 48 | address invalidAuthority = address(0); 49 | 50 | vm.expectRevert(Auth.InvalidAuthArguments.selector); 51 | target.auth(invalidAuthority, commit, Auth.Signature({yParity: vToYParity(v), r: r, s: s})); 52 | } 53 | 54 | function test_authCall_revert_noAuth() external { 55 | vm.expectRevert(); 56 | target.authcall(Auth.Call({to: address(0), value: 0, data: "0x"})); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /test/Invoker.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8.20; 3 | 4 | import {Test, console2} from "forge-std/Test.sol"; 5 | import {VmSafe} from "forge-std/Vm.sol"; 6 | import {Auth} from "../src/Auth.sol"; 7 | import {Invoker} from "../src/Invoker.sol"; 8 | import {vToYParity} from "./utils.sol"; 9 | 10 | contract Example { 11 | error UnexpectedSender(address expected, address actual); 12 | 13 | mapping(address => uint256) public counter; 14 | mapping(address => uint256) public values; 15 | 16 | function increment() public payable { 17 | counter[msg.sender] += 1; 18 | values[msg.sender] += msg.value; 19 | } 20 | 21 | function expectSender(address expected) public payable { 22 | if (msg.sender != expected) { 23 | revert UnexpectedSender(expected, msg.sender); 24 | } 25 | } 26 | } 27 | 28 | contract InvokerTest is Test { 29 | Invoker public invoker; 30 | Example public example; 31 | VmSafe.Wallet public sender; 32 | VmSafe.Wallet public recipient; 33 | 34 | uint8 authcall = 2; 35 | 36 | function setUp() public { 37 | invoker = new Invoker(); 38 | example = new Example(); 39 | sender = vm.createWallet("sender"); 40 | recipient = vm.createWallet("recipient"); 41 | vm.label(address(invoker), "invoker"); 42 | vm.label(sender.addr, "sender"); 43 | vm.label(recipient.addr, "recipient"); 44 | } 45 | 46 | function test_execute_data() external { 47 | bytes memory data = abi.encodeWithSelector(Example.increment.selector); 48 | bytes memory calls; 49 | calls = abi.encodePacked( 50 | authcall, 51 | address(example), 52 | uint256(0), 53 | data.length, 54 | data 55 | ); 56 | calls = abi.encodePacked( 57 | calls, 58 | authcall, 59 | address(example), 60 | uint256(0), 61 | data.length, 62 | data 63 | ); 64 | calls = abi.encodePacked( 65 | calls, 66 | authcall, 67 | address(example), 68 | uint256(0), 69 | data.length, 70 | data 71 | ); 72 | 73 | bytes32 hash = invoker.getAuthMessageHash(calls, sender.addr, vm.getNonce(address(sender.addr))); 74 | (uint8 v, bytes32 r, bytes32 s) = vm.sign(sender.privateKey, hash); 75 | invoker.execute(calls, sender.addr, Auth.Signature({yParity: vToYParity(v), r: r, s: s})); 76 | 77 | assertEq(example.counter(sender.addr), 3); 78 | assertEq(example.values(sender.addr), 0); 79 | } 80 | 81 | function test_execute_value() external { 82 | vm.deal(sender.addr, 1 ether); 83 | 84 | bytes memory calls; 85 | calls = abi.encodePacked( 86 | authcall, 87 | recipient.addr, 88 | uint256(0.5 ether), 89 | uint256(0) 90 | ); 91 | calls = abi.encodePacked( 92 | calls, 93 | authcall, 94 | recipient.addr, 95 | uint256(0.5 ether), 96 | uint256(0) 97 | ); 98 | 99 | bytes32 hash = invoker.getAuthMessageHash(calls, sender.addr, vm.getNonce(address(sender.addr))); 100 | (uint8 v, bytes32 r, bytes32 s) = vm.sign(sender.privateKey, hash); 101 | 102 | invoker.execute(calls, sender.addr, Auth.Signature({yParity: vToYParity(v), r: r, s: s})); 103 | 104 | assertEq(address(sender.addr).balance, 0 ether); 105 | assertEq(address(recipient.addr).balance, 1 ether); 106 | } 107 | 108 | function test_execute_dataAndValue() external { 109 | vm.deal(sender.addr, 6 ether); 110 | 111 | bytes memory data = abi.encodeWithSelector(Example.increment.selector); 112 | bytes memory calls; 113 | calls = abi.encodePacked( 114 | authcall, 115 | address(example), 116 | uint256(1 ether), 117 | data.length, 118 | data 119 | ); 120 | calls = abi.encodePacked( 121 | calls, 122 | authcall, 123 | address(example), 124 | uint256(2 ether), 125 | data.length, 126 | data 127 | ); 128 | calls = abi.encodePacked( 129 | calls, 130 | authcall, 131 | address(example), 132 | uint256(3 ether), 133 | data.length, 134 | data 135 | ); 136 | 137 | bytes32 hash = invoker.getAuthMessageHash(calls, sender.addr, vm.getNonce(address(sender.addr))); 138 | (uint8 v, bytes32 r, bytes32 s) = vm.sign(sender.privateKey, hash); 139 | invoker.execute(calls, sender.addr, Auth.Signature({yParity: vToYParity(v), r: r, s: s})); 140 | 141 | assertEq(example.counter(sender.addr), 3); 142 | assertEq(example.values(sender.addr), 6 ether); 143 | } 144 | 145 | function test_execute_revert_invalidSender() external { 146 | bytes memory data_1 = abi.encodeWithSelector(Example.increment.selector); 147 | bytes memory data_2 = abi.encodeWithSelector(Example.expectSender.selector, address(0)); 148 | 149 | bytes memory calls; 150 | calls = abi.encodePacked( 151 | authcall, 152 | address(example), 153 | uint256(0), 154 | data_1.length, 155 | data_1 156 | ); 157 | calls = abi.encodePacked( 158 | calls, 159 | authcall, 160 | address(example), 161 | uint256(0), 162 | data_2.length, 163 | data_2 164 | ); 165 | 166 | bytes32 hash = invoker.getAuthMessageHash(calls, sender.addr, vm.getNonce(address(sender.addr))); 167 | (uint8 v, bytes32 r, bytes32 s) = vm.sign(sender.privateKey, hash); 168 | 169 | vm.expectRevert(abi.encodeWithSelector(Example.UnexpectedSender.selector, address(0), address(sender.addr))); 170 | invoker.execute(calls, sender.addr, Auth.Signature({yParity: vToYParity(v), r: r, s: s})); 171 | 172 | assertEq(example.counter(sender.addr), 0); 173 | } 174 | 175 | function test_execute_revert_invalidAuthority() external { 176 | vm.deal(sender.addr, 1 ether); 177 | vm.deal(recipient.addr, 1 ether); 178 | 179 | bytes memory calls; 180 | calls = abi.encodePacked( 181 | authcall, 182 | recipient.addr, 183 | uint256(0.5 ether), 184 | uint256(0) 185 | ); 186 | calls = abi.encodePacked( 187 | calls, 188 | authcall, 189 | recipient.addr, 190 | uint256(0.5 ether), 191 | uint256(0) 192 | ); 193 | 194 | bytes32 hash = invoker.getAuthMessageHash(calls, sender.addr, vm.getNonce(address(sender.addr))); 195 | (uint8 v, bytes32 r, bytes32 s) = vm.sign(sender.privateKey, hash); 196 | 197 | vm.expectRevert(Auth.InvalidAuthArguments.selector); 198 | invoker.execute(calls, recipient.addr, Auth.Signature({yParity: vToYParity(v), r: r, s: s})); 199 | 200 | assertEq(address(sender.addr).balance, 1 ether); 201 | assertEq(address(recipient.addr).balance, 1 ether); 202 | } 203 | 204 | function test_execute_revert_invalidSignature() external { 205 | vm.deal(sender.addr, 1 ether); 206 | vm.deal(recipient.addr, 1 ether); 207 | 208 | bytes memory calls; 209 | calls = abi.encodePacked( 210 | authcall, 211 | recipient.addr, 212 | uint256(0.5 ether), 213 | uint256(0) 214 | ); 215 | calls = abi.encodePacked( 216 | calls, 217 | authcall, 218 | recipient.addr, 219 | uint256(0.5 ether), 220 | uint256(0) 221 | ); 222 | 223 | 224 | VmSafe.Wallet memory badGuy = vm.createWallet("badGuy"); 225 | bytes memory calls_fake; 226 | calls_fake = abi.encodePacked( 227 | authcall, 228 | badGuy.addr, 229 | uint256(0.5 ether), 230 | uint256(0) 231 | ); 232 | calls_fake = abi.encodePacked( 233 | calls_fake, 234 | authcall, 235 | badGuy.addr, 236 | uint256(0.5 ether), 237 | uint256(0) 238 | ); 239 | 240 | bytes32 hash = invoker.getAuthMessageHash(calls, sender.addr, vm.getNonce(address(sender.addr))); 241 | (uint8 v, bytes32 r, bytes32 s) = vm.sign(sender.privateKey, hash); 242 | 243 | vm.expectRevert(Auth.InvalidAuthArguments.selector); 244 | invoker.execute(calls_fake, sender.addr, Auth.Signature({yParity: vToYParity(v), r: r, s: s})); 245 | 246 | assertEq(address(sender.addr).balance, 1 ether); 247 | assertEq(address(recipient.addr).balance, 1 ether); 248 | } 249 | 250 | function test_execute_revert_revoke() external { 251 | vm.deal(sender.addr, 1 ether); 252 | 253 | bytes memory calls; 254 | calls = abi.encodePacked( 255 | authcall, 256 | recipient.addr, 257 | uint256(0.5 ether), 258 | uint256(0) 259 | ); 260 | calls = abi.encodePacked( 261 | calls, 262 | authcall, 263 | recipient.addr, 264 | uint256(0.5 ether), 265 | uint256(0) 266 | ); 267 | 268 | bytes32 hash = invoker.getAuthMessageHash(calls, sender.addr, vm.getNonce(address(sender.addr))); 269 | (uint8 v, bytes32 r, bytes32 s) = vm.sign(sender.privateKey, hash); 270 | 271 | invoker.execute(calls, sender.addr, Auth.Signature({yParity: vToYParity(v), r: r, s: s})); 272 | 273 | assertEq(address(sender.addr).balance, 0 ether); 274 | assertEq(address(recipient.addr).balance, 1 ether); 275 | 276 | // revoke by setting nonce 277 | vm.setNonce(address(sender.addr), vm.getNonce(address(sender.addr)) + 1); 278 | 279 | vm.expectRevert(Auth.InvalidAuthArguments.selector); 280 | invoker.execute(calls, sender.addr, Auth.Signature({yParity: vToYParity(v), r: r, s: s})); 281 | 282 | assertEq(address(sender.addr).balance, 0 ether); 283 | assertEq(address(recipient.addr).balance, 1 ether); 284 | } 285 | 286 | function test_execute_revert_invalidNonce() external { 287 | vm.deal(sender.addr, 1 ether); 288 | 289 | bytes memory calls; 290 | calls = abi.encodePacked( 291 | authcall, 292 | recipient.addr, 293 | uint256(0.5 ether), 294 | uint256(0) 295 | ); 296 | calls = abi.encodePacked( 297 | calls, 298 | authcall, 299 | recipient.addr, 300 | uint256(0.5 ether), 301 | uint256(0) 302 | ); 303 | 304 | bytes32 hash = invoker.getAuthMessageHash(calls, sender.addr, vm.getNonce(address(sender.addr))); 305 | (uint8 v, bytes32 r, bytes32 s) = vm.sign(sender.privateKey, hash); 306 | 307 | invoker.execute(calls, sender.addr, Auth.Signature({yParity: vToYParity(v), r: r, s: s})); 308 | 309 | assertEq(address(sender.addr).balance, 0 ether); 310 | assertEq(address(recipient.addr).balance, 1 ether); 311 | 312 | vm.expectRevert(Auth.InvalidAuthArguments.selector); 313 | invoker.execute(calls, sender.addr, Auth.Signature({yParity: vToYParity(v), r: r, s: s})); 314 | 315 | assertEq(address(sender.addr).balance, 0 ether); 316 | assertEq(address(recipient.addr).balance, 1 ether); 317 | } 318 | } 319 | -------------------------------------------------------------------------------- /test/utils.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicensed 2 | pragma solidity ^0.8.20; 3 | 4 | function vToYParity(uint8 v) pure returns (uint8 yParity_) { 5 | assembly { 6 | switch lt(v, 35) 7 | case true { yParity_ := eq(v, 28) } 8 | default { yParity_ := mod(sub(v, 35), 2) } 9 | } 10 | } 11 | --------------------------------------------------------------------------------