Skip to content

Commit

Permalink
feat: EIP-6492 support (#78)
Browse files Browse the repository at this point in the history
  • Loading branch information
chris13524 committed Apr 26, 2024
1 parent 1319404 commit edd6b6b
Show file tree
Hide file tree
Showing 16 changed files with 917 additions and 86 deletions.
4 changes: 0 additions & 4 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -56,10 +56,6 @@ jobs:

- name: Install Foundry
uses: foundry-rs/foundry-toolchain@v1

# pre-build contracts to avoid race condition installing solc during `forge create` in tests
- name: Build contracts
run: forge build -C contracts --cache-path=target/.forge/cache --out=target/.forge/out

- uses: Swatinem/rust-cache@v2

Expand Down
9 changes: 7 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,14 @@ The core Relay client. Provides access to all available Relay RPC methods to bui

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

### Test dependencies
### `cacao` feature

Foundry is required to be installed to your system for testing: <https://book.getfoundry.sh/getting-started/installation>
To aid IDE integration you may want to add this to your local `relay_rpc/Cargo.toml` file:

```toml
[features]
default = ["cacao"]
```

# License

Expand Down
2 changes: 1 addition & 1 deletion blockchain_api/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
pub use reqwest::Error;
use {
relay_rpc::{auth::cacao::signature::eip1271::get_rpc_url::GetRpcUrl, domain::ProjectId},
relay_rpc::{auth::cacao::signature::get_rpc_url::GetRpcUrl, domain::ProjectId},
serde::Deserialize,
std::{collections::HashSet, convert::Infallible, sync::Arc, time::Duration},
tokio::{sync::RwLock, task::JoinHandle},
Expand Down
28 changes: 17 additions & 11 deletions relay_rpc/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,16 @@ license = "Apache-2.0"
cacao = [
"dep:k256",
"dep:sha3",
"dep:alloy-providers",
"dep:alloy-provider",
"dep:alloy-transport",
"dep:alloy-transport-http",
"dep:alloy-rpc-types",
"dep:alloy-json-rpc",
"dep:alloy-json-abi",
"dep:alloy-sol-types",
"dep:alloy-primitives",
"dep:alloy-node-bindings"
"dep:alloy-node-bindings",
"dep:alloy-contract"
]

[dependencies]
Expand Down Expand Up @@ -45,19 +46,24 @@ k256 = { version = "0.13", optional = true }
sha3 = { version = "0.10", optional = true }
sha2 = { version = "0.10.6" }
url = "2"
alloy-providers = { git = "https://github.com/alloy-rs/alloy.git", rev = "e6f98e1", optional = true }
alloy-transport = { git = "https://github.com/alloy-rs/alloy.git", rev = "e6f98e1", optional = true }
alloy-transport-http = { git = "https://github.com/alloy-rs/alloy.git", rev = "e6f98e1", optional = true }
alloy-rpc-types = { git = "https://github.com/alloy-rs/alloy.git", rev = "e6f98e1", optional = true }
alloy-json-rpc = { git = "https://github.com/alloy-rs/alloy.git", rev = "e6f98e1", optional = true }
alloy-node-bindings = { git = "https://github.com/alloy-rs/alloy.git", rev = "e6f98e1", optional = true }
alloy-json-abi = { version = "0.6.2", optional = true }
alloy-sol-types = { version = "0.6.2", optional = true }
alloy-primitives = { version = "0.6.2", optional = true }
alloy-provider = { git = "https://github.com/alloy-rs/alloy.git", rev = "d68a6b7", optional = true }
alloy-transport = { git = "https://github.com/alloy-rs/alloy.git", rev = "d68a6b7", optional = true }
alloy-transport-http = { git = "https://github.com/alloy-rs/alloy.git", rev = "d68a6b7", optional = true }
alloy-rpc-types = { git = "https://github.com/alloy-rs/alloy.git", rev = "d68a6b7", optional = true }
alloy-json-rpc = { git = "https://github.com/alloy-rs/alloy.git", rev = "d68a6b7", optional = true }
alloy-node-bindings = { git = "https://github.com/alloy-rs/alloy.git", rev = "d68a6b7", optional = true }
alloy-contract = { git = "https://github.com/alloy-rs/alloy.git", rev = "d68a6b7", optional = true }
alloy-json-abi = { version = "0.7.0", optional = true }
alloy-sol-types = { version = "0.7.0", optional = true }
alloy-primitives = { version = "0.7.0", optional = true }
strum = { version = "0.26", features = ["strum_macros", "derive"] }

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

[build-dependencies]
serde_json = "1.0"
hex = "0.4.3"

[lints.clippy]
indexing_slicing = "deny"
98 changes: 98 additions & 0 deletions relay_rpc/build.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
use {
serde_json::Value,
std::process::{Command, Stdio},
};

fn main() {
#[cfg(feature = "cacao")]
build_contracts();
}

fn build_contracts() {
println!("cargo::rerun-if-changed=contracts");
install_foundry();
compile_contracts();
extract_bytecodes();
}

fn format_foundry_dir(path: &str) -> String {
format!(
"{}/../../../../.foundry/{}",
std::env::var("OUT_DIR").unwrap(),
path
)
}

fn install_foundry() {
let bin_folder = format_foundry_dir("bin");
std::fs::remove_dir_all(&bin_folder).ok();
std::fs::create_dir_all(&bin_folder).unwrap();
let output = Command::new("bash")
.args(["-c", &format!("curl https://raw.githubusercontent.com/foundry-rs/foundry/e0ea59cae26d945445d9cf21fdf22f4a18ac5bb2/foundryup/foundryup | FOUNDRY_DIR={} bash", format_foundry_dir(""))])
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.unwrap()
.wait_with_output()
.unwrap();
println!("foundryup status: {:?}", output.status);
let stdout = String::from_utf8(output.stdout).unwrap();
println!("foundryup stdout: {stdout:?}");
let stderr = String::from_utf8(output.stderr).unwrap();
println!("foundryup stderr: {stderr:?}");
assert!(output.status.success());
}

fn compile_contracts() {
let output = Command::new(format_foundry_dir("bin/forge"))
.args([
"build",
"--contracts=relay_rpc/contracts",
"--cache-path",
&format_foundry_dir("forge/cache"),
"--out",
&format_foundry_dir("forge/out"),
])
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.unwrap()
.wait_with_output()
.unwrap();
println!("forge status: {:?}", output.status);
let stdout = String::from_utf8(output.stdout).unwrap();
println!("forge stdout: {stdout:?}");
let stderr = String::from_utf8(output.stderr).unwrap();
println!("forge stderr: {stderr:?}");
assert!(output.status.success());
}

const EIP6492_FILE: &str = "forge/out/Eip6492.sol/ValidateSigOffchain.json";
const EIP6492_BYTECODE_FILE: &str = "forge/out/Eip6492.sol/ValidateSigOffchain.bytecode";
const EIP1271_MOCK_FILE: &str = "forge/out/Eip1271Mock.sol/Eip1271Mock.json";
const EIP1271_MOCK_BYTECODE_FILE: &str = "forge/out/Eip1271Mock.sol/Eip1271Mock.bytecode";
fn extract_bytecodes() {
extract_bytecode(
&format_foundry_dir(EIP6492_FILE),
&format_foundry_dir(EIP6492_BYTECODE_FILE),
);
extract_bytecode(
&format_foundry_dir(EIP1271_MOCK_FILE),
&format_foundry_dir(EIP1271_MOCK_BYTECODE_FILE),
);
}

fn extract_bytecode(input_file: &str, output_file: &str) {
let contents = serde_json::from_slice::<Value>(&std::fs::read(input_file).unwrap()).unwrap();
let bytecode = contents
.get("bytecode")
.unwrap()
.get("object")
.unwrap()
.as_str()
.unwrap()
.strip_prefix("0x")
.unwrap();
let bytecode = hex::decode(bytecode).unwrap();
std::fs::write(output_file, bytecode).unwrap();
}
49 changes: 49 additions & 0 deletions relay_rpc/contracts/Create2.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
// https://github.com/Genesis3800/CREATE2Factory/blob/b202029eadc0299e6e5923dd90db4200c2f7955a/src/Create2.sol

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

contract Create2 {

error Create2InsufficientBalance(uint256 received, uint256 minimumNeeded);

error Create2EmptyBytecode();

error Create2FailedDeployment();

function deploy(uint256 amount, bytes32 salt, bytes memory bytecode) external payable returns (address addr) {

if (msg.value < amount) {
revert Create2InsufficientBalance(msg.value, amount);
}

if (bytecode.length == 0) {
revert Create2EmptyBytecode();
}

assembly {
addr := create2(amount, add(bytecode, 0x20), mload(bytecode), salt)
}

if (addr == address(0)) {
revert Create2FailedDeployment();
}
}

function computeAddress(bytes32 salt, bytes32 bytecodeHash) external view returns (address addr) {

address contractAddress = address(this);

assembly {
let ptr := mload(0x40)

mstore(add(ptr, 0x40), bytecodeHash)
mstore(add(ptr, 0x20), salt)
mstore(ptr, contractAddress)
let start := add(ptr, 0x0b)
mstore8(start, 0xff)
addr := keccak256(start, 85)
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@ pragma solidity ^0.8.25;
// https://eips.ethereum.org/EIPS/eip-1271#reference-implementation

contract Eip1271Mock {
address owner;
address owner_eoa;

constructor() {
owner = msg.sender;
constructor(address _owner_eoa) {
owner_eoa = _owner_eoa;
}

/**
Expand All @@ -17,7 +17,7 @@ contract Eip1271Mock {
bytes calldata _signature
) external view returns (bytes4) {
// Validate signatures
if (recoverSigner(_hash, _signature) == owner) {
if (recoverSigner(_hash, _signature) == owner_eoa) {
return 0x1626ba7e;
} else {
return 0xffffffff;
Expand Down
111 changes: 111 additions & 0 deletions relay_rpc/contracts/Eip6492.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
// As per ERC-1271
interface IERC1271Wallet {
function isValidSignature(bytes32 hash, bytes calldata signature) external view returns (bytes4 magicValue);
}

error ERC1271Revert(bytes error);
error ERC6492DeployFailed(bytes error);

contract UniversalSigValidator {
bytes32 private constant ERC6492_DETECTION_SUFFIX = 0x6492649264926492649264926492649264926492649264926492649264926492;
bytes4 private constant ERC1271_SUCCESS = 0x1626ba7e;

function isValidSigImpl(
address _signer,
bytes32 _hash,
bytes calldata _signature,
bool allowSideEffects,
bool tryPrepare
) public returns (bool) {
uint contractCodeLen = address(_signer).code.length;
bytes memory sigToValidate;
// The order here is strictly defined in https://eips.ethereum.org/EIPS/eip-6492
// - 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
// - ERC-1271 verification if there's contract code
// - finally, ecrecover
bool isCounterfactual = bytes32(_signature[_signature.length-32:_signature.length]) == ERC6492_DETECTION_SUFFIX;
if (isCounterfactual) {
address create2Factory;
bytes memory factoryCalldata;
(create2Factory, factoryCalldata, sigToValidate) = abi.decode(_signature[0:_signature.length-32], (address, bytes, bytes));

if (contractCodeLen == 0 || tryPrepare) {
(bool success, bytes memory err) = create2Factory.call(factoryCalldata);
if (!success) revert ERC6492DeployFailed(err);
}
} else {
sigToValidate = _signature;
}

// Try ERC-1271 verification
if (isCounterfactual || contractCodeLen > 0) {
try IERC1271Wallet(_signer).isValidSignature(_hash, sigToValidate) returns (bytes4 magicValue) {
bool isValid = magicValue == ERC1271_SUCCESS;

// retry, but this time assume the prefix is a prepare call
if (!isValid && !tryPrepare && contractCodeLen > 0) {
return isValidSigImpl(_signer, _hash, _signature, allowSideEffects, true);
}

if (contractCodeLen == 0 && isCounterfactual && !allowSideEffects) {
// if the call had side effects we need to return the
// result using a `revert` (to undo the state changes)
assembly {
mstore(0, isValid)
revert(31, 1)
}
}

return isValid;
} catch (bytes memory err) {
// retry, but this time assume the prefix is a prepare call
if (!tryPrepare && contractCodeLen > 0) {
return isValidSigImpl(_signer, _hash, _signature, allowSideEffects, true);
}

revert ERC1271Revert(err);
}
}

// ecrecover verification
require(_signature.length == 65, 'SignatureValidator#recoverSigner: invalid signature length');
bytes32 r = bytes32(_signature[0:32]);
bytes32 s = bytes32(_signature[32:64]);
uint8 v = uint8(_signature[64]);
if (v != 27 && v != 28) {
revert('SignatureValidator: invalid signature v value');
}
return ecrecover(_hash, v, r, s) == _signer;
}

function isValidSigWithSideEffects(address _signer, bytes32 _hash, bytes calldata _signature)
external returns (bool)
{
return this.isValidSigImpl(_signer, _hash, _signature, true, false);
}

function isValidSig(address _signer, bytes32 _hash, bytes calldata _signature)
external returns (bool)
{
try this.isValidSigImpl(_signer, _hash, _signature, false, false) returns (bool isValid) { return isValid; }
catch (bytes memory error) {
// in order to avoid side effects from the contract getting deployed, the entire call will revert with a single byte result
uint len = error.length;
if (len == 1) return error[0] == 0x01;
// all other errors are simply forwarded, but in custom formats so that nothing else can revert with a single byte in the call
else assembly { revert(error, len) }
}
}
}

// this is a helper so we can perform validation in a single eth_call without pre-deploying a singleton
contract ValidateSigOffchain {
constructor (address _signer, bytes32 _hash, bytes memory _signature) {
UniversalSigValidator validator = new UniversalSigValidator();
bool isValidSig = validator.isValidSigWithSideEffects(_signer, _hash, _signature);
assembly {
mstore(0, isValidSig)
return(31, 1)
}
}
}
Loading

0 comments on commit edd6b6b

Please sign in to comment.