Skip to content

Commit edd6b6b

Browse files
authored
feat: EIP-6492 support (#78)
1 parent 1319404 commit edd6b6b

File tree

16 files changed

+917
-86
lines changed

16 files changed

+917
-86
lines changed

.github/workflows/ci.yaml

-4
Original file line numberDiff line numberDiff line change
@@ -56,10 +56,6 @@ jobs:
5656

5757
- name: Install Foundry
5858
uses: foundry-rs/foundry-toolchain@v1
59-
60-
# pre-build contracts to avoid race condition installing solc during `forge create` in tests
61-
- name: Build contracts
62-
run: forge build -C contracts --cache-path=target/.forge/cache --out=target/.forge/out
6359

6460
- uses: Swatinem/rust-cache@v2
6561

README.md

+7-2
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,14 @@ The core Relay client. Provides access to all available Relay RPC methods to bui
1515

1616
Provides all of the Relay domain types (e.g. `ClientId`, `ProjectId` etc.) as well as auth token generation and validation functionality.
1717

18-
### Test dependencies
18+
### `cacao` feature
1919

20-
Foundry is required to be installed to your system for testing: <https://book.getfoundry.sh/getting-started/installation>
20+
To aid IDE integration you may want to add this to your local `relay_rpc/Cargo.toml` file:
21+
22+
```toml
23+
[features]
24+
default = ["cacao"]
25+
```
2126

2227
# License
2328

blockchain_api/src/lib.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
pub use reqwest::Error;
22
use {
3-
relay_rpc::{auth::cacao::signature::eip1271::get_rpc_url::GetRpcUrl, domain::ProjectId},
3+
relay_rpc::{auth::cacao::signature::get_rpc_url::GetRpcUrl, domain::ProjectId},
44
serde::Deserialize,
55
std::{collections::HashSet, convert::Infallible, sync::Arc, time::Duration},
66
tokio::{sync::RwLock, task::JoinHandle},

relay_rpc/Cargo.toml

+17-11
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,16 @@ license = "Apache-2.0"
88
cacao = [
99
"dep:k256",
1010
"dep:sha3",
11-
"dep:alloy-providers",
11+
"dep:alloy-provider",
1212
"dep:alloy-transport",
1313
"dep:alloy-transport-http",
1414
"dep:alloy-rpc-types",
1515
"dep:alloy-json-rpc",
1616
"dep:alloy-json-abi",
1717
"dep:alloy-sol-types",
1818
"dep:alloy-primitives",
19-
"dep:alloy-node-bindings"
19+
"dep:alloy-node-bindings",
20+
"dep:alloy-contract"
2021
]
2122

2223
[dependencies]
@@ -45,19 +46,24 @@ k256 = { version = "0.13", optional = true }
4546
sha3 = { version = "0.10", optional = true }
4647
sha2 = { version = "0.10.6" }
4748
url = "2"
48-
alloy-providers = { git = "https://github.com/alloy-rs/alloy.git", rev = "e6f98e1", optional = true }
49-
alloy-transport = { git = "https://github.com/alloy-rs/alloy.git", rev = "e6f98e1", optional = true }
50-
alloy-transport-http = { git = "https://github.com/alloy-rs/alloy.git", rev = "e6f98e1", optional = true }
51-
alloy-rpc-types = { git = "https://github.com/alloy-rs/alloy.git", rev = "e6f98e1", optional = true }
52-
alloy-json-rpc = { git = "https://github.com/alloy-rs/alloy.git", rev = "e6f98e1", optional = true }
53-
alloy-node-bindings = { git = "https://github.com/alloy-rs/alloy.git", rev = "e6f98e1", optional = true }
54-
alloy-json-abi = { version = "0.6.2", optional = true }
55-
alloy-sol-types = { version = "0.6.2", optional = true }
56-
alloy-primitives = { version = "0.6.2", optional = true }
49+
alloy-provider = { git = "https://github.com/alloy-rs/alloy.git", rev = "d68a6b7", optional = true }
50+
alloy-transport = { git = "https://github.com/alloy-rs/alloy.git", rev = "d68a6b7", optional = true }
51+
alloy-transport-http = { git = "https://github.com/alloy-rs/alloy.git", rev = "d68a6b7", optional = true }
52+
alloy-rpc-types = { git = "https://github.com/alloy-rs/alloy.git", rev = "d68a6b7", optional = true }
53+
alloy-json-rpc = { git = "https://github.com/alloy-rs/alloy.git", rev = "d68a6b7", optional = true }
54+
alloy-node-bindings = { git = "https://github.com/alloy-rs/alloy.git", rev = "d68a6b7", optional = true }
55+
alloy-contract = { git = "https://github.com/alloy-rs/alloy.git", rev = "d68a6b7", optional = true }
56+
alloy-json-abi = { version = "0.7.0", optional = true }
57+
alloy-sol-types = { version = "0.7.0", optional = true }
58+
alloy-primitives = { version = "0.7.0", optional = true }
5759
strum = { version = "0.26", features = ["strum_macros", "derive"] }
5860

5961
[dev-dependencies]
6062
tokio = { version = "1.35.1", features = ["test-util", "macros"] }
6163

64+
[build-dependencies]
65+
serde_json = "1.0"
66+
hex = "0.4.3"
67+
6268
[lints.clippy]
6369
indexing_slicing = "deny"

relay_rpc/build.rs

+98
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
use {
2+
serde_json::Value,
3+
std::process::{Command, Stdio},
4+
};
5+
6+
fn main() {
7+
#[cfg(feature = "cacao")]
8+
build_contracts();
9+
}
10+
11+
fn build_contracts() {
12+
println!("cargo::rerun-if-changed=contracts");
13+
install_foundry();
14+
compile_contracts();
15+
extract_bytecodes();
16+
}
17+
18+
fn format_foundry_dir(path: &str) -> String {
19+
format!(
20+
"{}/../../../../.foundry/{}",
21+
std::env::var("OUT_DIR").unwrap(),
22+
path
23+
)
24+
}
25+
26+
fn install_foundry() {
27+
let bin_folder = format_foundry_dir("bin");
28+
std::fs::remove_dir_all(&bin_folder).ok();
29+
std::fs::create_dir_all(&bin_folder).unwrap();
30+
let output = Command::new("bash")
31+
.args(["-c", &format!("curl https://raw.githubusercontent.com/foundry-rs/foundry/e0ea59cae26d945445d9cf21fdf22f4a18ac5bb2/foundryup/foundryup | FOUNDRY_DIR={} bash", format_foundry_dir(""))])
32+
.stdout(Stdio::piped())
33+
.stderr(Stdio::piped())
34+
.spawn()
35+
.unwrap()
36+
.wait_with_output()
37+
.unwrap();
38+
println!("foundryup status: {:?}", output.status);
39+
let stdout = String::from_utf8(output.stdout).unwrap();
40+
println!("foundryup stdout: {stdout:?}");
41+
let stderr = String::from_utf8(output.stderr).unwrap();
42+
println!("foundryup stderr: {stderr:?}");
43+
assert!(output.status.success());
44+
}
45+
46+
fn compile_contracts() {
47+
let output = Command::new(format_foundry_dir("bin/forge"))
48+
.args([
49+
"build",
50+
"--contracts=relay_rpc/contracts",
51+
"--cache-path",
52+
&format_foundry_dir("forge/cache"),
53+
"--out",
54+
&format_foundry_dir("forge/out"),
55+
])
56+
.stdout(Stdio::piped())
57+
.stderr(Stdio::piped())
58+
.spawn()
59+
.unwrap()
60+
.wait_with_output()
61+
.unwrap();
62+
println!("forge status: {:?}", output.status);
63+
let stdout = String::from_utf8(output.stdout).unwrap();
64+
println!("forge stdout: {stdout:?}");
65+
let stderr = String::from_utf8(output.stderr).unwrap();
66+
println!("forge stderr: {stderr:?}");
67+
assert!(output.status.success());
68+
}
69+
70+
const EIP6492_FILE: &str = "forge/out/Eip6492.sol/ValidateSigOffchain.json";
71+
const EIP6492_BYTECODE_FILE: &str = "forge/out/Eip6492.sol/ValidateSigOffchain.bytecode";
72+
const EIP1271_MOCK_FILE: &str = "forge/out/Eip1271Mock.sol/Eip1271Mock.json";
73+
const EIP1271_MOCK_BYTECODE_FILE: &str = "forge/out/Eip1271Mock.sol/Eip1271Mock.bytecode";
74+
fn extract_bytecodes() {
75+
extract_bytecode(
76+
&format_foundry_dir(EIP6492_FILE),
77+
&format_foundry_dir(EIP6492_BYTECODE_FILE),
78+
);
79+
extract_bytecode(
80+
&format_foundry_dir(EIP1271_MOCK_FILE),
81+
&format_foundry_dir(EIP1271_MOCK_BYTECODE_FILE),
82+
);
83+
}
84+
85+
fn extract_bytecode(input_file: &str, output_file: &str) {
86+
let contents = serde_json::from_slice::<Value>(&std::fs::read(input_file).unwrap()).unwrap();
87+
let bytecode = contents
88+
.get("bytecode")
89+
.unwrap()
90+
.get("object")
91+
.unwrap()
92+
.as_str()
93+
.unwrap()
94+
.strip_prefix("0x")
95+
.unwrap();
96+
let bytecode = hex::decode(bytecode).unwrap();
97+
std::fs::write(output_file, bytecode).unwrap();
98+
}

relay_rpc/contracts/Create2.sol

+49
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
// https://github.com/Genesis3800/CREATE2Factory/blob/b202029eadc0299e6e5923dd90db4200c2f7955a/src/Create2.sol
2+
3+
// SPDX-License-Identifier: MIT
4+
pragma solidity ^0.8.20;
5+
6+
contract Create2 {
7+
8+
error Create2InsufficientBalance(uint256 received, uint256 minimumNeeded);
9+
10+
error Create2EmptyBytecode();
11+
12+
error Create2FailedDeployment();
13+
14+
function deploy(uint256 amount, bytes32 salt, bytes memory bytecode) external payable returns (address addr) {
15+
16+
if (msg.value < amount) {
17+
revert Create2InsufficientBalance(msg.value, amount);
18+
}
19+
20+
if (bytecode.length == 0) {
21+
revert Create2EmptyBytecode();
22+
}
23+
24+
assembly {
25+
addr := create2(amount, add(bytecode, 0x20), mload(bytecode), salt)
26+
}
27+
28+
if (addr == address(0)) {
29+
revert Create2FailedDeployment();
30+
}
31+
}
32+
33+
function computeAddress(bytes32 salt, bytes32 bytecodeHash) external view returns (address addr) {
34+
35+
address contractAddress = address(this);
36+
37+
assembly {
38+
let ptr := mload(0x40)
39+
40+
mstore(add(ptr, 0x40), bytecodeHash)
41+
mstore(add(ptr, 0x20), salt)
42+
mstore(ptr, contractAddress)
43+
let start := add(ptr, 0x0b)
44+
mstore8(start, 0xff)
45+
addr := keccak256(start, 85)
46+
}
47+
}
48+
49+
}

contracts/Eip1271Mock.sol relay_rpc/contracts/Eip1271Mock.sol

+4-4
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,10 @@ pragma solidity ^0.8.25;
33
// https://eips.ethereum.org/EIPS/eip-1271#reference-implementation
44

55
contract Eip1271Mock {
6-
address owner;
6+
address owner_eoa;
77

8-
constructor() {
9-
owner = msg.sender;
8+
constructor(address _owner_eoa) {
9+
owner_eoa = _owner_eoa;
1010
}
1111

1212
/**
@@ -17,7 +17,7 @@ contract Eip1271Mock {
1717
bytes calldata _signature
1818
) external view returns (bytes4) {
1919
// Validate signatures
20-
if (recoverSigner(_hash, _signature) == owner) {
20+
if (recoverSigner(_hash, _signature) == owner_eoa) {
2121
return 0x1626ba7e;
2222
} else {
2323
return 0xffffffff;

relay_rpc/contracts/Eip6492.sol

+111
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
// As per ERC-1271
2+
interface IERC1271Wallet {
3+
function isValidSignature(bytes32 hash, bytes calldata signature) external view returns (bytes4 magicValue);
4+
}
5+
6+
error ERC1271Revert(bytes error);
7+
error ERC6492DeployFailed(bytes error);
8+
9+
contract UniversalSigValidator {
10+
bytes32 private constant ERC6492_DETECTION_SUFFIX = 0x6492649264926492649264926492649264926492649264926492649264926492;
11+
bytes4 private constant ERC1271_SUCCESS = 0x1626ba7e;
12+
13+
function isValidSigImpl(
14+
address _signer,
15+
bytes32 _hash,
16+
bytes calldata _signature,
17+
bool allowSideEffects,
18+
bool tryPrepare
19+
) public returns (bool) {
20+
uint contractCodeLen = address(_signer).code.length;
21+
bytes memory sigToValidate;
22+
// The order here is strictly defined in https://eips.ethereum.org/EIPS/eip-6492
23+
// - ERC-6492 suffix check and verification first, while being permissive in case the contract is already deployed; if the contract is deployed we will check the sig against the deployed version, this allows 6492 signatures to still be validated while taking into account potential key rotation
24+
// - ERC-1271 verification if there's contract code
25+
// - finally, ecrecover
26+
bool isCounterfactual = bytes32(_signature[_signature.length-32:_signature.length]) == ERC6492_DETECTION_SUFFIX;
27+
if (isCounterfactual) {
28+
address create2Factory;
29+
bytes memory factoryCalldata;
30+
(create2Factory, factoryCalldata, sigToValidate) = abi.decode(_signature[0:_signature.length-32], (address, bytes, bytes));
31+
32+
if (contractCodeLen == 0 || tryPrepare) {
33+
(bool success, bytes memory err) = create2Factory.call(factoryCalldata);
34+
if (!success) revert ERC6492DeployFailed(err);
35+
}
36+
} else {
37+
sigToValidate = _signature;
38+
}
39+
40+
// Try ERC-1271 verification
41+
if (isCounterfactual || contractCodeLen > 0) {
42+
try IERC1271Wallet(_signer).isValidSignature(_hash, sigToValidate) returns (bytes4 magicValue) {
43+
bool isValid = magicValue == ERC1271_SUCCESS;
44+
45+
// retry, but this time assume the prefix is a prepare call
46+
if (!isValid && !tryPrepare && contractCodeLen > 0) {
47+
return isValidSigImpl(_signer, _hash, _signature, allowSideEffects, true);
48+
}
49+
50+
if (contractCodeLen == 0 && isCounterfactual && !allowSideEffects) {
51+
// if the call had side effects we need to return the
52+
// result using a `revert` (to undo the state changes)
53+
assembly {
54+
mstore(0, isValid)
55+
revert(31, 1)
56+
}
57+
}
58+
59+
return isValid;
60+
} catch (bytes memory err) {
61+
// retry, but this time assume the prefix is a prepare call
62+
if (!tryPrepare && contractCodeLen > 0) {
63+
return isValidSigImpl(_signer, _hash, _signature, allowSideEffects, true);
64+
}
65+
66+
revert ERC1271Revert(err);
67+
}
68+
}
69+
70+
// ecrecover verification
71+
require(_signature.length == 65, 'SignatureValidator#recoverSigner: invalid signature length');
72+
bytes32 r = bytes32(_signature[0:32]);
73+
bytes32 s = bytes32(_signature[32:64]);
74+
uint8 v = uint8(_signature[64]);
75+
if (v != 27 && v != 28) {
76+
revert('SignatureValidator: invalid signature v value');
77+
}
78+
return ecrecover(_hash, v, r, s) == _signer;
79+
}
80+
81+
function isValidSigWithSideEffects(address _signer, bytes32 _hash, bytes calldata _signature)
82+
external returns (bool)
83+
{
84+
return this.isValidSigImpl(_signer, _hash, _signature, true, false);
85+
}
86+
87+
function isValidSig(address _signer, bytes32 _hash, bytes calldata _signature)
88+
external returns (bool)
89+
{
90+
try this.isValidSigImpl(_signer, _hash, _signature, false, false) returns (bool isValid) { return isValid; }
91+
catch (bytes memory error) {
92+
// in order to avoid side effects from the contract getting deployed, the entire call will revert with a single byte result
93+
uint len = error.length;
94+
if (len == 1) return error[0] == 0x01;
95+
// all other errors are simply forwarded, but in custom formats so that nothing else can revert with a single byte in the call
96+
else assembly { revert(error, len) }
97+
}
98+
}
99+
}
100+
101+
// this is a helper so we can perform validation in a single eth_call without pre-deploying a singleton
102+
contract ValidateSigOffchain {
103+
constructor (address _signer, bytes32 _hash, bytes memory _signature) {
104+
UniversalSigValidator validator = new UniversalSigValidator();
105+
bool isValidSig = validator.isValidSigWithSideEffects(_signer, _hash, _signature);
106+
assembly {
107+
mstore(0, isValidSig)
108+
return(31, 1)
109+
}
110+
}
111+
}

0 commit comments

Comments
 (0)