mirror of
https://github.com/serai-dex/serai.git
synced 2025-02-02 03:06:31 +00:00
Add Serai key confirmation to prevent rotating to an unusable key
Also updates alloy to the latest version
This commit is contained in:
parent
8013c56195
commit
3192370484
18 changed files with 679 additions and 326 deletions
618
Cargo.lock
generated
618
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
|
@ -6,7 +6,7 @@ license = "MIT"
|
|||
repository = "https://github.com/serai-dex/serai/tree/develop/networks/ethereum/alloy-simple-request-transport"
|
||||
authors = ["Luke Parker <lukeparker5132@gmail.com>"]
|
||||
edition = "2021"
|
||||
rust-version = "1.78"
|
||||
rust-version = "1.81"
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
all-features = true
|
||||
|
@ -16,13 +16,13 @@ rustdoc-args = ["--cfg", "docsrs"]
|
|||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
tower = "0.4"
|
||||
tower = "0.5"
|
||||
|
||||
serde_json = { version = "1", default-features = false }
|
||||
simple-request = { path = "../../../common/request", version = "0.1", default-features = false }
|
||||
|
||||
alloy-json-rpc = { version = "0.3", default-features = false }
|
||||
alloy-transport = { version = "0.3", default-features = false }
|
||||
alloy-json-rpc = { version = "0.7", default-features = false }
|
||||
alloy-transport = { version = "0.7", default-features = false }
|
||||
|
||||
[features]
|
||||
default = ["tls"]
|
||||
|
|
|
@ -33,10 +33,10 @@ alloy-core = { version = "0.8", default-features = false }
|
|||
alloy-sol-types = { version = "0.8", default-features = false }
|
||||
|
||||
alloy-simple-request-transport = { path = "../../../networks/ethereum/alloy-simple-request-transport", default-features = false }
|
||||
alloy-rpc-types-eth = { version = "0.3", default-features = false }
|
||||
alloy-rpc-client = { version = "0.3", default-features = false }
|
||||
alloy-provider = { version = "0.3", default-features = false }
|
||||
alloy-rpc-types-eth = { version = "0.7", default-features = false }
|
||||
alloy-rpc-client = { version = "0.7", default-features = false }
|
||||
alloy-provider = { version = "0.7", default-features = false }
|
||||
|
||||
alloy-node-bindings = { version = "0.3", default-features = false }
|
||||
alloy-node-bindings = { version = "0.7", default-features = false }
|
||||
|
||||
tokio = { version = "1", default-features = false, features = ["macros"] }
|
||||
|
|
|
@ -8,6 +8,7 @@ authors = ["Luke Parker <lukeparker5132@gmail.com>"]
|
|||
keywords = []
|
||||
edition = "2021"
|
||||
publish = false
|
||||
rust-version = "1.81"
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
all-features = true
|
||||
|
@ -33,11 +34,11 @@ k256 = { version = "^0.13.1", default-features = false, features = ["std"] }
|
|||
alloy-core = { version = "0.8", default-features = false }
|
||||
alloy-rlp = { version = "0.3", default-features = false }
|
||||
|
||||
alloy-rpc-types-eth = { version = "0.3", default-features = false }
|
||||
alloy-transport = { version = "0.3", default-features = false }
|
||||
alloy-rpc-types-eth = { version = "0.7", default-features = false }
|
||||
alloy-transport = { version = "0.7", default-features = false }
|
||||
alloy-simple-request-transport = { path = "../../networks/ethereum/alloy-simple-request-transport", default-features = false }
|
||||
alloy-rpc-client = { version = "0.3", default-features = false }
|
||||
alloy-provider = { version = "0.3", default-features = false }
|
||||
alloy-rpc-client = { version = "0.7", default-features = false }
|
||||
alloy-provider = { version = "0.7", default-features = false }
|
||||
|
||||
serai-client = { path = "../../substrate/client", default-features = false, features = ["ethereum"] }
|
||||
|
||||
|
|
|
@ -7,7 +7,7 @@ repository = "https://github.com/serai-dex/serai/tree/develop/processor/ethereum
|
|||
authors = ["Luke Parker <lukeparker5132@gmail.com>"]
|
||||
edition = "2021"
|
||||
publish = false
|
||||
rust-version = "1.79"
|
||||
rust-version = "1.81"
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
all-features = true
|
||||
|
@ -18,15 +18,16 @@ workspace = true
|
|||
|
||||
[dependencies]
|
||||
alloy-core = { version = "0.8", default-features = false }
|
||||
alloy-consensus = { version = "0.3", default-features = false }
|
||||
|
||||
alloy-sol-types = { version = "0.8", default-features = false }
|
||||
alloy-sol-macro = { version = "0.8", default-features = false }
|
||||
|
||||
alloy-rpc-types-eth = { version = "0.3", default-features = false }
|
||||
alloy-transport = { version = "0.3", default-features = false }
|
||||
alloy-consensus = { version = "0.7", default-features = false }
|
||||
|
||||
alloy-rpc-types-eth = { version = "0.7", default-features = false }
|
||||
alloy-transport = { version = "0.7", default-features = false }
|
||||
alloy-simple-request-transport = { path = "../../../networks/ethereum/alloy-simple-request-transport", default-features = false }
|
||||
alloy-provider = { version = "0.3", default-features = false }
|
||||
alloy-provider = { version = "0.7", default-features = false }
|
||||
|
||||
ethereum-primitives = { package = "serai-processor-ethereum-primitives", path = "../primitives", default-features = false }
|
||||
|
||||
|
|
|
@ -49,7 +49,7 @@ impl Deployer {
|
|||
// 100 gwei
|
||||
gas_price: 100_000_000_000u128,
|
||||
// TODO: Use a more accurate gas limit
|
||||
gas_limit: 1_000_000u128,
|
||||
gas_limit: 1_000_000u64,
|
||||
to: TxKind::Create,
|
||||
value: U256::ZERO,
|
||||
input: bytecode,
|
||||
|
|
|
@ -7,7 +7,7 @@ repository = "https://github.com/serai-dex/serai/tree/develop/processor/ethereum
|
|||
authors = ["Luke Parker <lukeparker5132@gmail.com>"]
|
||||
edition = "2021"
|
||||
publish = false
|
||||
rust-version = "1.79"
|
||||
rust-version = "1.81"
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
all-features = true
|
||||
|
@ -22,9 +22,9 @@ alloy-core = { version = "0.8", default-features = false }
|
|||
alloy-sol-types = { version = "0.8", default-features = false }
|
||||
alloy-sol-macro = { version = "0.8", default-features = false }
|
||||
|
||||
alloy-rpc-types-eth = { version = "0.3", default-features = false }
|
||||
alloy-transport = { version = "0.3", default-features = false }
|
||||
alloy-rpc-types-eth = { version = "0.7", default-features = false }
|
||||
alloy-transport = { version = "0.7", default-features = false }
|
||||
alloy-simple-request-transport = { path = "../../../networks/ethereum/alloy-simple-request-transport", default-features = false }
|
||||
alloy-provider = { version = "0.3", default-features = false }
|
||||
alloy-provider = { version = "0.7", default-features = false }
|
||||
|
||||
tokio = { version = "1", default-features = false, features = ["rt"] }
|
||||
|
|
|
@ -8,7 +8,7 @@ use alloy_core::primitives::{Address, B256, U256};
|
|||
|
||||
use alloy_sol_types::{SolInterface, SolEvent};
|
||||
|
||||
use alloy_rpc_types_eth::Filter;
|
||||
use alloy_rpc_types_eth::{Filter, TransactionTrait};
|
||||
use alloy_transport::{TransportErrorKind, RpcError};
|
||||
use alloy_simple_request_transport::SimpleRequest;
|
||||
use alloy_provider::{Provider, RootProvider};
|
||||
|
@ -66,7 +66,7 @@ impl Erc20 {
|
|||
// If this is a top-level call...
|
||||
// Don't validate the encoding as this can't be re-encoded to an identical bytestring due
|
||||
// to the `InInstruction` appended after the call itself
|
||||
if let Ok(call) = IERC20Calls::abi_decode(&transaction.input, false) {
|
||||
if let Ok(call) = IERC20Calls::abi_decode(transaction.inner.input(), false) {
|
||||
// Extract the top-level call's from/to/value
|
||||
let (from, call_to, value) = match call {
|
||||
IERC20Calls::transfer(transferCall { to, value }) => (transaction.from, to, value),
|
||||
|
@ -92,7 +92,7 @@ impl Erc20 {
|
|||
// Find the log for this transfer
|
||||
for log in receipt.inner.logs() {
|
||||
// If this log was emitted by a different contract, continue
|
||||
if Some(log.address()) != transaction.to {
|
||||
if Some(log.address()) != transaction.inner.to() {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
@ -122,7 +122,7 @@ impl Erc20 {
|
|||
|
||||
// Read the data appended after
|
||||
let encoded = call.abi_encode();
|
||||
let data = transaction.input.as_ref()[encoded.len() ..].to_vec();
|
||||
let data = transaction.inner.input().as_ref()[encoded.len() ..].to_vec();
|
||||
|
||||
return Ok(Some(TopLevelTransfer {
|
||||
id: (*transaction_id, log_index),
|
||||
|
|
|
@ -7,7 +7,7 @@ repository = "https://github.com/serai-dex/serai/tree/develop/processor/ethereum
|
|||
authors = ["Luke Parker <lukeparker5132@gmail.com>"]
|
||||
edition = "2021"
|
||||
publish = false
|
||||
rust-version = "1.79"
|
||||
rust-version = "1.81"
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
all-features = true
|
||||
|
@ -21,4 +21,4 @@ group = { version = "0.13", default-features = false }
|
|||
k256 = { version = "^0.13.1", default-features = false, features = ["std", "arithmetic"] }
|
||||
|
||||
alloy-core = { version = "0.8", default-features = false }
|
||||
alloy-consensus = { version = "0.3", default-features = false, features = ["k256"] }
|
||||
alloy-consensus = { version = "0.7", default-features = false, features = ["k256"] }
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
use group::ff::PrimeField;
|
||||
use k256::Scalar;
|
||||
|
||||
use alloy_core::primitives::{Parity, Signature};
|
||||
use alloy_core::primitives::PrimitiveSignature;
|
||||
use alloy_consensus::{SignableTransaction, Signed, TxLegacy};
|
||||
|
||||
/// The Keccak256 hash function.
|
||||
|
@ -34,8 +34,8 @@ pub fn deterministically_sign(tx: &TxLegacy) -> Signed<TxLegacy> {
|
|||
// Create the signature
|
||||
let r_bytes: [u8; 32] = r.to_repr().into();
|
||||
let s_bytes: [u8; 32] = s.to_repr().into();
|
||||
let v = Parity::NonEip155(false);
|
||||
let signature = Signature::from_scalars_and_parity(r_bytes.into(), s_bytes.into(), v).unwrap();
|
||||
let signature =
|
||||
PrimitiveSignature::from_scalars_and_parity(r_bytes.into(), s_bytes.into(), false);
|
||||
|
||||
// Check if this is a valid signature
|
||||
let tx = tx.clone().into_signed(signature);
|
||||
|
|
|
@ -7,7 +7,7 @@ repository = "https://github.com/serai-dex/serai/tree/develop/processor/ethereum
|
|||
authors = ["Luke Parker <lukeparker5132@gmail.com>"]
|
||||
edition = "2021"
|
||||
publish = false
|
||||
rust-version = "1.79"
|
||||
rust-version = "1.81"
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
all-features = true
|
||||
|
@ -24,12 +24,12 @@ alloy-core = { version = "0.8", default-features = false }
|
|||
alloy-sol-types = { version = "0.8", default-features = false }
|
||||
alloy-sol-macro = { version = "0.8", default-features = false }
|
||||
|
||||
alloy-consensus = { version = "0.3", default-features = false }
|
||||
alloy-consensus = { version = "0.7", default-features = false }
|
||||
|
||||
alloy-rpc-types-eth = { version = "0.3", default-features = false }
|
||||
alloy-transport = { version = "0.3", default-features = false }
|
||||
alloy-rpc-types-eth = { version = "0.7", default-features = false }
|
||||
alloy-transport = { version = "0.7", default-features = false }
|
||||
alloy-simple-request-transport = { path = "../../../networks/ethereum/alloy-simple-request-transport", default-features = false }
|
||||
alloy-provider = { version = "0.3", default-features = false }
|
||||
alloy-provider = { version = "0.7", default-features = false }
|
||||
|
||||
ethereum-schnorr = { package = "ethereum-schnorr-contract", path = "../../../networks/ethereum/schnorr", default-features = false }
|
||||
|
||||
|
@ -53,8 +53,8 @@ rand_core = { version = "0.6", default-features = false, features = ["std"] }
|
|||
|
||||
k256 = { version = "0.13", default-features = false, features = ["std"] }
|
||||
|
||||
alloy-rpc-client = { version = "0.3", default-features = false }
|
||||
alloy-node-bindings = { version = "0.3", default-features = false }
|
||||
alloy-rpc-client = { version = "0.7", default-features = false }
|
||||
alloy-node-bindings = { version = "0.7", default-features = false }
|
||||
|
||||
tokio = { version = "1.0", default-features = false, features = ["rt-multi-thread", "macros"] }
|
||||
|
||||
|
|
|
@ -26,6 +26,13 @@ fn main() {
|
|||
fs::create_dir(&artifacts_path).unwrap();
|
||||
}
|
||||
|
||||
build_solidity_contracts::build(
|
||||
&["../../../networks/ethereum/schnorr/contracts", "../erc20/contracts", "contracts"],
|
||||
"contracts",
|
||||
&artifacts_path,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// This cannot be handled with the sol! macro. The Router requires an import
|
||||
// https://github.com/alloy-rs/core/issues/602
|
||||
sol(
|
||||
|
|
|
@ -5,6 +5,11 @@ pragma solidity ^0.8.26;
|
|||
/// @author Luke Parker <lukeparker@serai.exchange>
|
||||
/// @notice Intakes coins for the Serai network and handles relaying batches of transfers out
|
||||
interface IRouterWithoutCollisions {
|
||||
/// @notice Emitted when the next key for Serai's Ethereum validators is set
|
||||
/// @param nonce The nonce consumed to update this key
|
||||
/// @param key The key updated to
|
||||
event NextSeraiKeySet(uint256 indexed nonce, bytes32 indexed key);
|
||||
|
||||
/// @notice Emitted when the key for Serai's Ethereum validators is updated
|
||||
/// @param nonce The nonce consumed to update this key
|
||||
/// @param key The key updated to
|
||||
|
@ -39,6 +44,9 @@ interface IRouterWithoutCollisions {
|
|||
/// @param coin The coin which escaped
|
||||
event Escaped(address indexed coin);
|
||||
|
||||
/// @notice The key for Serai was invalid
|
||||
/// @dev This is incomplete and not always guaranteed to be thrown upon an invalid key
|
||||
error InvalidSeraiKey();
|
||||
/// @notice The contract has had its escape hatch invoked and won't accept further actions
|
||||
error EscapeHatchInvoked();
|
||||
/// @notice The signature was invalid
|
||||
|
@ -86,8 +94,15 @@ interface IRouterWithoutCollisions {
|
|||
/// return The next nonce to use by an action published to this contract
|
||||
function nextNonce() external view returns (uint256);
|
||||
|
||||
/// @notice Fetch the next key for Serai's Ethereum validator set
|
||||
/// @return The next key for Serai's Ethereum validator set or bytes32(0) if none is currently set
|
||||
function nextSeraiKey() external view returns (bytes32);
|
||||
|
||||
/// @notice Fetch the current key for Serai's Ethereum validator set
|
||||
/// @return The current key for Serai's Ethereum validator set
|
||||
/**
|
||||
* @return The current key for Serai's Ethereum validator set or bytes32(0) if none is currently
|
||||
* set
|
||||
*/
|
||||
function seraiKey() external view returns (bytes32);
|
||||
|
||||
/// @notice Fetch the address escaped to
|
||||
|
@ -134,15 +149,25 @@ interface IRouter is IRouterWithoutCollisions {
|
|||
}
|
||||
|
||||
/// @notice Update the key representing Serai's Ethereum validators
|
||||
/// @dev This assumes the key is correct. No checks on it are performed
|
||||
/**
|
||||
* @dev This does not validate the passed-in key as much as possible. This is accepted as the key
|
||||
* won't actually be rotated to until it provides a signature confirming the update however
|
||||
* (proving signatures can be made by the key in question and verified via our Schnorr
|
||||
* contract).
|
||||
*/
|
||||
// @param signature The signature by the current key authorizing this update
|
||||
/// @param signature The signature by the current key authorizing this update
|
||||
/// @param newSeraiKey The key to update to
|
||||
function updateSeraiKey(Signature calldata signature, bytes32 newSeraiKey) external;
|
||||
/// @param nextSeraiKeyVar The key to update to, once it confirms the update
|
||||
function updateSeraiKey(Signature calldata signature, bytes32 nextSeraiKeyVar) external;
|
||||
|
||||
/// @notice Confirm the next key representing Serai's Ethereum validators, updating to it
|
||||
/// @param signature The signature by the next key confirming its validity
|
||||
function confirmNextSeraiKey(Signature calldata signature) external;
|
||||
|
||||
/// @notice Execute a batch of `OutInstruction`s
|
||||
/**
|
||||
* @dev All `OutInstruction`s in a batch are only for a single coin to simplify handling of the
|
||||
* fee
|
||||
* fee
|
||||
*/
|
||||
/// @param signature The signature by the current key for Serai's Ethereum validators
|
||||
/// @param coin The coin all of these `OutInstruction`s are for
|
||||
|
|
|
@ -50,6 +50,12 @@ contract Router is IRouterWithoutCollisions {
|
|||
*/
|
||||
uint256 private _nextNonce;
|
||||
|
||||
/**
|
||||
* @dev The next public key for Serai's Ethereum validator set, in the form the Schnorr library
|
||||
* expects
|
||||
*/
|
||||
bytes32 private _nextSeraiKey;
|
||||
|
||||
/**
|
||||
* @dev The current public key for Serai's Ethereum validator set, in the form the Schnorr library
|
||||
* expects
|
||||
|
@ -59,12 +65,16 @@ contract Router is IRouterWithoutCollisions {
|
|||
/// @dev The address escaped to
|
||||
address private _escapedTo;
|
||||
|
||||
/// @dev Updates the Serai key. This does not update `_nextNonce`
|
||||
/// @param nonceUpdatedWith The nonce used to update the key
|
||||
/// @param newSeraiKey The key updated to
|
||||
function _updateSeraiKey(uint256 nonceUpdatedWith, bytes32 newSeraiKey) private {
|
||||
_seraiKey = newSeraiKey;
|
||||
emit SeraiKeyUpdated(nonceUpdatedWith, newSeraiKey);
|
||||
/// @dev Set the next Serai key. This does not read from/write to `_nextNonce`
|
||||
/// @param nonceUpdatedWith The nonce used to set the next key
|
||||
/// @param nextSeraiKeyVar The key to set as next
|
||||
function _setNextSeraiKey(uint256 nonceUpdatedWith, bytes32 nextSeraiKeyVar) private {
|
||||
// Explicitly disallow 0 so we can always consider 0 as None and non-zero as Some
|
||||
if (nextSeraiKeyVar == bytes32(0)) {
|
||||
revert InvalidSeraiKey();
|
||||
}
|
||||
_nextSeraiKey = nextSeraiKeyVar;
|
||||
emit NextSeraiKeySet(nonceUpdatedWith, nextSeraiKeyVar);
|
||||
}
|
||||
|
||||
/// @notice The constructor for the relayer
|
||||
|
@ -74,8 +84,10 @@ contract Router is IRouterWithoutCollisions {
|
|||
// This is incompatible with any networks which don't have their nonces start at 0
|
||||
_smartContractNonce = 1;
|
||||
|
||||
// Set the Serai key
|
||||
_updateSeraiKey(0, initialSeraiKey);
|
||||
// Set the next Serai key
|
||||
_setNextSeraiKey(0, initialSeraiKey);
|
||||
// Set the current Serai key to None
|
||||
_seraiKey = bytes32(0);
|
||||
|
||||
// We just consumed nonce 0 when setting the initial Serai key
|
||||
_nextNonce = 1;
|
||||
|
@ -90,7 +102,7 @@ contract Router is IRouterWithoutCollisions {
|
|||
* calldata should be signed with the nonce taking the place of the signature's commitment to
|
||||
* its nonce, and the signature solution zeroed.
|
||||
*/
|
||||
function verifySignature()
|
||||
function verifySignature(bytes32 key)
|
||||
private
|
||||
returns (uint256 nonceUsed, bytes memory message, bytes32 messageHash)
|
||||
{
|
||||
|
@ -99,6 +111,15 @@ contract Router is IRouterWithoutCollisions {
|
|||
revert EscapeHatchInvoked();
|
||||
}
|
||||
|
||||
/*
|
||||
If this key isn't set, reject it.
|
||||
|
||||
The Schnorr contract should already reject this public key yet it's best to be explicit.
|
||||
*/
|
||||
if (key == bytes32(0)) {
|
||||
revert InvalidSignature();
|
||||
}
|
||||
|
||||
message = msg.data;
|
||||
uint256 messageLen = message.length;
|
||||
/*
|
||||
|
@ -134,7 +155,7 @@ contract Router is IRouterWithoutCollisions {
|
|||
}
|
||||
|
||||
// Verify the signature
|
||||
if (!Schnorr.verify(_seraiKey, messageHash, signatureC, signatureS)) {
|
||||
if (!Schnorr.verify(key, messageHash, signatureC, signatureS)) {
|
||||
revert InvalidSignature();
|
||||
}
|
||||
|
||||
|
@ -178,22 +199,38 @@ contract Router is IRouterWithoutCollisions {
|
|||
}
|
||||
}
|
||||
|
||||
/// @notice Update the key representing Serai's Ethereum validators
|
||||
/// @notice Start updating the key representing Serai's Ethereum validators
|
||||
/**
|
||||
* @dev This assumes the key is correct. No checks on it are performed.
|
||||
* @dev This does not validate the passed-in key as much as possible. This is accepted as the key
|
||||
* won't actually be rotated to until it provides a signature confirming the update however
|
||||
* (proving signatures can be made by the key in question and verified via our Schnorr
|
||||
* contract).
|
||||
*
|
||||
* The hex bytes are to cause a collision with `IRouter.updateSeraiKey`.
|
||||
*/
|
||||
// @param signature The signature by the current key authorizing this update
|
||||
// @param newSeraiKey The key to update to
|
||||
// @param nextSeraiKey The key to update to
|
||||
function updateSeraiKey5A8542A2() external {
|
||||
(uint256 nonceUsed, bytes memory args,) = verifySignature();
|
||||
(uint256 nonceUsed, bytes memory args,) = verifySignature(_seraiKey);
|
||||
/*
|
||||
We could replace this with a length check (if we don't simply assume the calldata is valid as
|
||||
it was properly signed) + mload to save 24 gas but it's not worth the complexity.
|
||||
*/
|
||||
(,, bytes32 newSeraiKey) = abi.decode(args, (bytes32, bytes32, bytes32));
|
||||
_updateSeraiKey(nonceUsed, newSeraiKey);
|
||||
(,, bytes32 nextSeraiKeyVar) = abi.decode(args, (bytes32, bytes32, bytes32));
|
||||
_setNextSeraiKey(nonceUsed, nextSeraiKeyVar);
|
||||
}
|
||||
|
||||
/// @notice Confirm the next key representing Serai's Ethereum validators, updating to it
|
||||
/// @dev The hex bytes are to cause a collision with `IRouter.confirmSeraiKey`.
|
||||
// @param signature The signature by the next key confirming its validity
|
||||
function confirmNextSeraiKey34AC53AC() external {
|
||||
// Checks
|
||||
bytes32 nextSeraiKeyVar = _nextSeraiKey;
|
||||
(uint256 nonceUsed,,) = verifySignature(nextSeraiKeyVar);
|
||||
// Effects
|
||||
_nextSeraiKey = bytes32(0);
|
||||
_seraiKey = nextSeraiKeyVar;
|
||||
emit SeraiKeyUpdated(nonceUsed, nextSeraiKeyVar);
|
||||
}
|
||||
|
||||
/// @notice Transfer coins into Serai with an instruction
|
||||
|
@ -384,7 +421,7 @@ contract Router is IRouterWithoutCollisions {
|
|||
revert ReenteredExecute();
|
||||
}
|
||||
|
||||
(uint256 nonceUsed, bytes memory args, bytes32 message) = verifySignature();
|
||||
(uint256 nonceUsed, bytes memory args, bytes32 message) = verifySignature(_seraiKey);
|
||||
(,, address coin, uint256 fee, IRouter.OutInstruction[] memory outs) =
|
||||
abi.decode(args, (bytes32, bytes32, address, uint256, IRouter.OutInstruction[]));
|
||||
|
||||
|
@ -481,7 +518,7 @@ contract Router is IRouterWithoutCollisions {
|
|||
// @param escapeTo The address to escape to
|
||||
function escapeHatchDCDD91CC() external {
|
||||
// Verify the signature
|
||||
(, bytes memory args,) = verifySignature();
|
||||
(, bytes memory args,) = verifySignature(_seraiKey);
|
||||
|
||||
(,, address escapeTo) = abi.decode(args, (bytes32, bytes32, address));
|
||||
|
||||
|
@ -526,8 +563,17 @@ contract Router is IRouterWithoutCollisions {
|
|||
return _nextNonce;
|
||||
}
|
||||
|
||||
/// @notice Fetch the next key for Serai's Ethereum validator set
|
||||
/// @return The next key for Serai's Ethereum validator set or bytes32(0) if none is currently set
|
||||
function nextSeraiKey() external view returns (bytes32) {
|
||||
return _nextSeraiKey;
|
||||
}
|
||||
|
||||
/// @notice Fetch the current key for Serai's Ethereum validator set
|
||||
/// @return The current key for Serai's Ethereum validator set
|
||||
/**
|
||||
* @return The current key for Serai's Ethereum validator set or bytes32(0) if none is currently
|
||||
* set
|
||||
*/
|
||||
function seraiKey() external view returns (bytes32) {
|
||||
return _seraiKey;
|
||||
}
|
||||
|
|
|
@ -275,6 +275,11 @@ impl Executed {
|
|||
#[derive(Clone, Debug)]
|
||||
pub struct Router(Arc<RootProvider<SimpleRequest>>, Address);
|
||||
impl Router {
|
||||
const DEPLOYMENT_GAS: u64 = 995_000;
|
||||
const CONFIRM_NEXT_SERAI_KEY_GAS: u64 = 58_000;
|
||||
const UPDATE_SERAI_KEY_GAS: u64 = 61_000;
|
||||
const EXECUTE_BASE_GAS: u64 = 48_000;
|
||||
|
||||
fn code() -> Vec<u8> {
|
||||
const BYTECODE: &[u8] =
|
||||
include_bytes!(concat!(env!("OUT_DIR"), "/serai-processor-ethereum-router/Router.bin"));
|
||||
|
@ -293,7 +298,7 @@ impl Router {
|
|||
/// This transaction assumes the `Deployer` has already been deployed.
|
||||
pub fn deployment_tx(initial_serai_key: &PublicKey) -> TxLegacy {
|
||||
let mut tx = Deployer::deploy_tx(Self::init_code(initial_serai_key));
|
||||
tx.gas_limit = 883654 * 120 / 100;
|
||||
tx.gas_limit = Self::DEPLOYMENT_GAS * 120 / 100;
|
||||
tx
|
||||
}
|
||||
|
||||
|
@ -322,6 +327,25 @@ impl Router {
|
|||
self.1
|
||||
}
|
||||
|
||||
/// Get the message to be signed in order to confirm the next key for Serai.
|
||||
pub fn confirm_next_serai_key_message(nonce: u64) -> Vec<u8> {
|
||||
abi::confirmNextSeraiKeyCall::new((abi::Signature {
|
||||
c: U256::try_from(nonce).unwrap().into(),
|
||||
s: U256::ZERO.into(),
|
||||
},))
|
||||
.abi_encode()
|
||||
}
|
||||
|
||||
/// Construct a transaction to confirm the next key representing Serai.
|
||||
pub fn confirm_next_serai_key(&self, sig: &Signature) -> TxLegacy {
|
||||
TxLegacy {
|
||||
to: TxKind::Call(self.1),
|
||||
input: abi::confirmNextSeraiKeyCall::new((abi::Signature::from(sig),)).abi_encode().into(),
|
||||
gas_limit: Self::CONFIRM_NEXT_SERAI_KEY_GAS * 120 / 100,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the message to be signed in order to update the key for Serai.
|
||||
pub fn update_serai_key_message(nonce: u64, key: &PublicKey) -> Vec<u8> {
|
||||
abi::updateSeraiKeyCall::new((
|
||||
|
@ -341,7 +365,7 @@ impl Router {
|
|||
))
|
||||
.abi_encode()
|
||||
.into(),
|
||||
gas_limit: 40_889 * 120 / 100,
|
||||
gas_limit: Self::UPDATE_SERAI_KEY_GAS * 120 / 100,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
@ -359,14 +383,14 @@ impl Router {
|
|||
|
||||
/// Construct a transaction to execute a batch of `OutInstruction`s.
|
||||
pub fn execute(&self, coin: Coin, fee: U256, outs: OutInstructions, sig: &Signature) -> TxLegacy {
|
||||
let outs_len = outs.0.len();
|
||||
// TODO
|
||||
let gas_limit = Self::EXECUTE_BASE_GAS + outs.0.iter().map(|_| 200_000 + 10_000).sum::<u64>();
|
||||
TxLegacy {
|
||||
to: TxKind::Call(self.1),
|
||||
input: abi::executeCall::new((abi::Signature::from(sig), coin.address(), fee, outs.0))
|
||||
.abi_encode()
|
||||
.into(),
|
||||
// TODO
|
||||
gas_limit: (45_501 + ((200_000 + 10_000) * u128::try_from(outs_len).unwrap())) * 120 / 100,
|
||||
gas_limit: gas_limit * 120 / 100,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
@ -536,7 +560,7 @@ impl Router {
|
|||
|
||||
res.push(Executed::SetKey {
|
||||
nonce: log.nonce.try_into().map_err(|e| {
|
||||
TransportErrorKind::Custom(format!("filtered to convert nonce to u64: {e:?}").into())
|
||||
TransportErrorKind::Custom(format!("failed to convert nonce to u64: {e:?}").into())
|
||||
})?,
|
||||
key: log.key.into(),
|
||||
});
|
||||
|
@ -568,7 +592,7 @@ impl Router {
|
|||
|
||||
res.push(Executed::Batch {
|
||||
nonce: log.nonce.try_into().map_err(|e| {
|
||||
TransportErrorKind::Custom(format!("filtered to convert nonce to u64: {e:?}").into())
|
||||
TransportErrorKind::Custom(format!("failed to convert nonce to u64: {e:?}").into())
|
||||
})?,
|
||||
message_hash: log.messageHash.into(),
|
||||
});
|
||||
|
@ -580,19 +604,40 @@ impl Router {
|
|||
Ok(res)
|
||||
}
|
||||
|
||||
/// Fetch the current key for Serai's Ethereum validators
|
||||
pub async fn key(&self, block: BlockId) -> Result<PublicKey, RpcError<TransportErrorKind>> {
|
||||
let call = TransactionRequest::default()
|
||||
.to(self.1)
|
||||
.input(TransactionInput::new(abi::seraiKeyCall::new(()).abi_encode().into()));
|
||||
async fn fetch_key(
|
||||
&self,
|
||||
block: BlockId,
|
||||
call: Vec<u8>,
|
||||
) -> Result<Option<PublicKey>, RpcError<TransportErrorKind>> {
|
||||
let call = TransactionRequest::default().to(self.1).input(TransactionInput::new(call.into()));
|
||||
let bytes = self.0.call(&call).block(block).await?;
|
||||
let res = abi::seraiKeyCall::abi_decode_returns(&bytes, true)
|
||||
.map_err(|e| TransportErrorKind::Custom(format!("filtered to decode key: {e:?}").into()))?;
|
||||
Ok(
|
||||
PublicKey::from_eth_repr(res._0.into()).ok_or_else(|| {
|
||||
// This is fine as both key calls share a return type
|
||||
let res = abi::nextSeraiKeyCall::abi_decode_returns(&bytes, true)
|
||||
.map_err(|e| TransportErrorKind::Custom(format!("failed to decode key: {e:?}").into()))?;
|
||||
let eth_repr = <[u8; 32]>::from(res._0);
|
||||
Ok(if eth_repr == [0; 32] {
|
||||
None
|
||||
} else {
|
||||
Some(PublicKey::from_eth_repr(eth_repr).ok_or_else(|| {
|
||||
TransportErrorKind::Custom("invalid key set on router".to_string().into())
|
||||
})?,
|
||||
)
|
||||
})?)
|
||||
})
|
||||
}
|
||||
|
||||
/// Fetch the next key for Serai's Ethereum validators
|
||||
pub async fn next_key(
|
||||
&self,
|
||||
block: BlockId,
|
||||
) -> Result<Option<PublicKey>, RpcError<TransportErrorKind>> {
|
||||
self.fetch_key(block, abi::nextSeraiKeyCall::new(()).abi_encode()).await
|
||||
}
|
||||
|
||||
/// Fetch the current key for Serai's Ethereum validators
|
||||
pub async fn key(
|
||||
&self,
|
||||
block: BlockId,
|
||||
) -> Result<Option<PublicKey>, RpcError<TransportErrorKind>> {
|
||||
self.fetch_key(block, abi::seraiKeyCall::new(()).abi_encode()).await
|
||||
}
|
||||
|
||||
/// Fetch the nonce of the next action to execute
|
||||
|
@ -602,7 +647,7 @@ impl Router {
|
|||
.input(TransactionInput::new(abi::nextNonceCall::new(()).abi_encode().into()));
|
||||
let bytes = self.0.call(&call).block(block).await?;
|
||||
let res = abi::nextNonceCall::abi_decode_returns(&bytes, true)
|
||||
.map_err(|e| TransportErrorKind::Custom(format!("filtered to decode nonce: {e:?}").into()))?;
|
||||
.map_err(|e| TransportErrorKind::Custom(format!("failed to decode nonce: {e:?}").into()))?;
|
||||
Ok(u64::try_from(res._0).map_err(|_| {
|
||||
TransportErrorKind::Custom("nonce returned exceeded 2**64".to_string().into())
|
||||
})?)
|
||||
|
@ -615,7 +660,7 @@ impl Router {
|
|||
.input(TransactionInput::new(abi::escapedToCall::new(()).abi_encode().into()));
|
||||
let bytes = self.0.call(&call).block(block).await?;
|
||||
let res = abi::escapedToCall::abi_decode_returns(&bytes, true).map_err(|e| {
|
||||
TransportErrorKind::Custom(format!("filtered to decode the address escaped to: {e:?}").into())
|
||||
TransportErrorKind::Custom(format!("failed to decode the address escaped to: {e:?}").into())
|
||||
})?;
|
||||
Ok(res._0)
|
||||
}
|
||||
|
|
|
@ -37,13 +37,17 @@ fn execute_reentrancy_guard() {
|
|||
#[test]
|
||||
fn selector_collisions() {
|
||||
assert_eq!(
|
||||
crate::_irouter_abi::IRouter::executeCall::SELECTOR,
|
||||
crate::_router_abi::Router::execute4DE42904Call::SELECTOR
|
||||
crate::_irouter_abi::IRouter::confirmNextSeraiKeyCall::SELECTOR,
|
||||
crate::_router_abi::Router::confirmNextSeraiKey34AC53ACCall::SELECTOR
|
||||
);
|
||||
assert_eq!(
|
||||
crate::_irouter_abi::IRouter::updateSeraiKeyCall::SELECTOR,
|
||||
crate::_router_abi::Router::updateSeraiKey5A8542A2Call::SELECTOR
|
||||
);
|
||||
assert_eq!(
|
||||
crate::_irouter_abi::IRouter::executeCall::SELECTOR,
|
||||
crate::_router_abi::Router::execute4DE42904Call::SELECTOR
|
||||
);
|
||||
assert_eq!(
|
||||
crate::_irouter_abi::IRouter::escapeHatchCall::SELECTOR,
|
||||
crate::_router_abi::Router::escapeHatchDCDD91CCCall::SELECTOR
|
||||
|
@ -78,13 +82,13 @@ async fn setup_test(
|
|||
// Get the TX to deploy the Router
|
||||
let mut tx = Router::deployment_tx(&public_key);
|
||||
// Set a gas price (100 gwei)
|
||||
tx.gas_price = 100_000_000_000u128;
|
||||
tx.gas_price = 100_000_000_000;
|
||||
// Sign it
|
||||
let tx = ethereum_primitives::deterministically_sign(&tx);
|
||||
// Publish it
|
||||
let receipt = ethereum_test_primitives::publish_tx(&provider, tx).await;
|
||||
assert!(receipt.status());
|
||||
println!("Router deployment used {} gas:", receipt.gas_used);
|
||||
assert_eq!(u128::from(Router::DEPLOYMENT_GAS), ((receipt.gas_used + 1000) / 1000) * 1000);
|
||||
|
||||
let router = Router::new(provider.clone(), &public_key).await.unwrap().unwrap();
|
||||
|
||||
|
@ -94,7 +98,8 @@ async fn setup_test(
|
|||
#[tokio::test]
|
||||
async fn test_constructor() {
|
||||
let (_anvil, _provider, router, key) = setup_test().await;
|
||||
assert_eq!(router.key(BlockNumberOrTag::Latest.into()).await.unwrap(), key.1);
|
||||
assert_eq!(router.next_key(BlockNumberOrTag::Latest.into()).await.unwrap(), Some(key.1));
|
||||
assert_eq!(router.key(BlockNumberOrTag::Latest.into()).await.unwrap(), None);
|
||||
assert_eq!(router.next_nonce(BlockNumberOrTag::Latest.into()).await.unwrap(), 1);
|
||||
assert_eq!(
|
||||
router.escaped_to(BlockNumberOrTag::Latest.into()).await.unwrap(),
|
||||
|
@ -102,12 +107,54 @@ async fn test_constructor() {
|
|||
);
|
||||
}
|
||||
|
||||
async fn confirm_next_serai_key(
|
||||
provider: &Arc<RootProvider<SimpleRequest>>,
|
||||
router: &Router,
|
||||
nonce: u64,
|
||||
key: (Scalar, PublicKey),
|
||||
) -> TransactionReceipt {
|
||||
let msg = Router::confirm_next_serai_key_message(nonce);
|
||||
|
||||
let nonce = Scalar::random(&mut OsRng);
|
||||
let c = Signature::challenge(ProjectivePoint::GENERATOR * nonce, &key.1, &msg);
|
||||
let s = nonce + (c * key.0);
|
||||
|
||||
let sig = Signature::new(c, s).unwrap();
|
||||
|
||||
let mut tx = router.confirm_next_serai_key(&sig);
|
||||
tx.gas_price = 100_000_000_000;
|
||||
let tx = ethereum_primitives::deterministically_sign(&tx);
|
||||
let receipt = ethereum_test_primitives::publish_tx(provider, tx).await;
|
||||
assert!(receipt.status());
|
||||
assert_eq!(
|
||||
u128::from(Router::CONFIRM_NEXT_SERAI_KEY_GAS),
|
||||
((receipt.gas_used + 1000) / 1000) * 1000
|
||||
);
|
||||
receipt
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_confirm_next_serai_key() {
|
||||
let (_anvil, provider, router, key) = setup_test().await;
|
||||
|
||||
assert_eq!(router.next_key(BlockNumberOrTag::Latest.into()).await.unwrap(), Some(key.1));
|
||||
assert_eq!(router.key(BlockNumberOrTag::Latest.into()).await.unwrap(), None);
|
||||
assert_eq!(router.next_nonce(BlockNumberOrTag::Latest.into()).await.unwrap(), 1);
|
||||
|
||||
let receipt = confirm_next_serai_key(&provider, &router, 1, key).await;
|
||||
|
||||
assert_eq!(router.next_key(receipt.block_hash.unwrap().into()).await.unwrap(), None);
|
||||
assert_eq!(router.key(receipt.block_hash.unwrap().into()).await.unwrap(), Some(key.1));
|
||||
assert_eq!(router.next_nonce(receipt.block_hash.unwrap().into()).await.unwrap(), 2);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_update_serai_key() {
|
||||
let (_anvil, provider, router, key) = setup_test().await;
|
||||
confirm_next_serai_key(&provider, &router, 1, key).await;
|
||||
|
||||
let update_to = test_key().1;
|
||||
let msg = Router::update_serai_key_message(1, &update_to);
|
||||
let msg = Router::update_serai_key_message(2, &update_to);
|
||||
|
||||
let nonce = Scalar::random(&mut OsRng);
|
||||
let c = Signature::challenge(ProjectivePoint::GENERATOR * nonce, &key.1, &msg);
|
||||
|
@ -116,19 +163,22 @@ async fn test_update_serai_key() {
|
|||
let sig = Signature::new(c, s).unwrap();
|
||||
|
||||
let mut tx = router.update_serai_key(&update_to, &sig);
|
||||
tx.gas_price = 100_000_000_000u128;
|
||||
tx.gas_price = 100_000_000_000;
|
||||
let tx = ethereum_primitives::deterministically_sign(&tx);
|
||||
let receipt = ethereum_test_primitives::publish_tx(&provider, tx).await;
|
||||
assert!(receipt.status());
|
||||
println!("update_serai_key used {} gas:", receipt.gas_used);
|
||||
assert_eq!(u128::from(Router::UPDATE_SERAI_KEY_GAS), ((receipt.gas_used + 1000) / 1000) * 1000);
|
||||
|
||||
assert_eq!(router.key(receipt.block_hash.unwrap().into()).await.unwrap(), update_to);
|
||||
assert_eq!(router.next_nonce(receipt.block_hash.unwrap().into()).await.unwrap(), 2);
|
||||
assert_eq!(router.key(receipt.block_hash.unwrap().into()).await.unwrap(), Some(key.1));
|
||||
assert_eq!(router.next_key(receipt.block_hash.unwrap().into()).await.unwrap(), Some(update_to));
|
||||
assert_eq!(router.next_nonce(receipt.block_hash.unwrap().into()).await.unwrap(), 3);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_eth_in_instruction() {
|
||||
let (_anvil, provider, router, _key) = setup_test().await;
|
||||
let (_anvil, provider, router, key) = setup_test().await;
|
||||
// TODO: Do we want to allow InInstructions before any key has been confirmed?
|
||||
confirm_next_serai_key(&provider, &router, 1, key).await;
|
||||
|
||||
let amount = U256::try_from(OsRng.next_u64()).unwrap();
|
||||
let mut in_instruction = vec![0; usize::try_from(OsRng.next_u64() % 256).unwrap()];
|
||||
|
@ -138,8 +188,8 @@ async fn test_eth_in_instruction() {
|
|||
chain_id: None,
|
||||
nonce: 0,
|
||||
// 100 gwei
|
||||
gas_price: 100_000_000_000u128,
|
||||
gas_limit: 1_000_000u128,
|
||||
gas_price: 100_000_000_000,
|
||||
gas_limit: 1_000_000,
|
||||
to: TxKind::Call(router.address()),
|
||||
value: amount,
|
||||
input: crate::abi::inInstructionCall::new((
|
||||
|
@ -200,7 +250,7 @@ async fn publish_outs(
|
|||
let sig = Signature::new(c, s).unwrap();
|
||||
|
||||
let mut tx = router.execute(coin, fee, outs, &sig);
|
||||
tx.gas_price = 100_000_000_000u128;
|
||||
tx.gas_price = 100_000_000_000;
|
||||
let tx = ethereum_primitives::deterministically_sign(&tx);
|
||||
ethereum_test_primitives::publish_tx(provider, tx).await
|
||||
}
|
||||
|
@ -208,6 +258,7 @@ async fn publish_outs(
|
|||
#[tokio::test]
|
||||
async fn test_eth_address_out_instruction() {
|
||||
let (_anvil, provider, router, key) = setup_test().await;
|
||||
confirm_next_serai_key(&provider, &router, 1, key).await;
|
||||
|
||||
let mut amount = U256::try_from(OsRng.next_u64()).unwrap();
|
||||
let mut fee = U256::try_from(OsRng.next_u64()).unwrap();
|
||||
|
@ -218,11 +269,11 @@ async fn test_eth_address_out_instruction() {
|
|||
ethereum_test_primitives::fund_account(&provider, router.address(), amount).await;
|
||||
|
||||
let instructions = OutInstructions::from([].as_slice());
|
||||
let receipt = publish_outs(&provider, &router, key, 1, Coin::Ether, fee, instructions).await;
|
||||
let receipt = publish_outs(&provider, &router, key, 2, Coin::Ether, fee, instructions).await;
|
||||
assert!(receipt.status());
|
||||
println!("empty execute used {} gas:", receipt.gas_used);
|
||||
assert_eq!(u128::from(Router::EXECUTE_BASE_GAS), ((receipt.gas_used + 1000) / 1000) * 1000);
|
||||
|
||||
assert_eq!(router.next_nonce(receipt.block_hash.unwrap().into()).await.unwrap(), 2);
|
||||
assert_eq!(router.next_nonce(receipt.block_hash.unwrap().into()).await.unwrap(), 3);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
|
|
|
@ -7,6 +7,7 @@ repository = "https://github.com/serai-dex/serai/tree/develop/processor/ethereum
|
|||
authors = ["Luke Parker <lukeparker5132@gmail.com>"]
|
||||
edition = "2021"
|
||||
publish = false
|
||||
rust-version = "1.81"
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
all-features = true
|
||||
|
@ -19,10 +20,10 @@ workspace = true
|
|||
k256 = { version = "0.13", default-features = false, features = ["std"] }
|
||||
|
||||
alloy-core = { version = "0.8", default-features = false }
|
||||
alloy-consensus = { version = "0.3", default-features = false, features = ["std"] }
|
||||
alloy-consensus = { version = "0.7", default-features = false, features = ["std"] }
|
||||
|
||||
alloy-rpc-types-eth = { version = "0.3", default-features = false }
|
||||
alloy-rpc-types-eth = { version = "0.7", default-features = false }
|
||||
alloy-simple-request-transport = { path = "../../../networks/ethereum/alloy-simple-request-transport", default-features = false }
|
||||
alloy-provider = { version = "0.3", default-features = false }
|
||||
alloy-provider = { version = "0.7", default-features = false }
|
||||
|
||||
ethereum-primitives = { package = "serai-processor-ethereum-primitives", path = "../primitives", default-features = false }
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
use k256::{elliptic_curve::sec1::ToEncodedPoint, ProjectivePoint};
|
||||
|
||||
use alloy_core::{
|
||||
primitives::{Address, U256, Bytes, Signature, TxKind},
|
||||
primitives::{Address, U256, Bytes, PrimitiveSignature, TxKind},
|
||||
hex::FromHex,
|
||||
};
|
||||
use alloy_consensus::{SignableTransaction, TxLegacy, Signed};
|
||||
|
@ -46,7 +46,7 @@ pub async fn publish_tx(
|
|||
|
||||
let (tx, sig, _) = tx.into_parts();
|
||||
let mut bytes = vec![];
|
||||
tx.encode_with_signature_fields(&sig, &mut bytes);
|
||||
tx.into_signed(sig).eip2718_encode(&mut bytes);
|
||||
let pending_tx = provider.send_raw_transaction(&bytes).await.unwrap();
|
||||
pending_tx.get_receipt().await.unwrap()
|
||||
}
|
||||
|
@ -111,7 +111,7 @@ pub async fn send(
|
|||
);
|
||||
|
||||
let mut bytes = vec![];
|
||||
tx.encode_with_signature_fields(&Signature::from(sig), &mut bytes);
|
||||
tx.into_signed(PrimitiveSignature::from(sig)).eip2718_encode(&mut bytes);
|
||||
let pending_tx = provider.send_raw_transaction(&bytes).await.unwrap();
|
||||
pending_tx.get_receipt().await.unwrap()
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue