Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: EIP-6492 support #78

Merged
merged 11 commits into from
Apr 26, 2024
Merged
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
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No longer need this here because this is called in build.rs

- 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 @@ -7,15 +7,16 @@ edition = "2021"
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 @@ -44,19 +45,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
Loading