mirror of
https://github.com/serai-dex/serai.git
synced 2025-12-08 12:19:24 +00:00
Test Ether InInstructions
This commit is contained in:
1
Cargo.lock
generated
1
Cargo.lock
generated
@@ -9483,6 +9483,7 @@ dependencies = [
|
|||||||
"futures-util",
|
"futures-util",
|
||||||
"group",
|
"group",
|
||||||
"k256",
|
"k256",
|
||||||
|
"parity-scale-codec",
|
||||||
"rand_core",
|
"rand_core",
|
||||||
"serai-client",
|
"serai-client",
|
||||||
"serai-ethereum-test-primitives",
|
"serai-ethereum-test-primitives",
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ use futures_util::stream::{StreamExt, FuturesUnordered};
|
|||||||
#[rustfmt::skip]
|
#[rustfmt::skip]
|
||||||
#[expect(warnings)]
|
#[expect(warnings)]
|
||||||
#[expect(needless_pass_by_value)]
|
#[expect(needless_pass_by_value)]
|
||||||
|
#[expect(missing_docs)]
|
||||||
#[expect(clippy::all)]
|
#[expect(clippy::all)]
|
||||||
#[expect(clippy::ignored_unit_patterns)]
|
#[expect(clippy::ignored_unit_patterns)]
|
||||||
#[expect(clippy::redundant_closure_for_method_calls)]
|
#[expect(clippy::redundant_closure_for_method_calls)]
|
||||||
@@ -28,11 +29,12 @@ mod abi {
|
|||||||
alloy_sol_macro::sol!("contracts/IERC20.sol");
|
alloy_sol_macro::sol!("contracts/IERC20.sol");
|
||||||
}
|
}
|
||||||
use abi::IERC20::{IERC20Calls, transferCall, transferFromCall};
|
use abi::IERC20::{IERC20Calls, transferCall, transferFromCall};
|
||||||
use abi::SeraiIERC20::{
|
use abi::SeraiIERC20::SeraiIERC20Calls;
|
||||||
SeraiIERC20Calls, transferWithInInstruction01BB244A8ACall as transferWithInInstructionCall,
|
pub use abi::IERC20::Transfer;
|
||||||
|
pub use abi::SeraiIERC20::{
|
||||||
|
transferWithInInstruction01BB244A8ACall as transferWithInInstructionCall,
|
||||||
transferFromWithInInstruction00081948E0Call as transferFromWithInInstructionCall,
|
transferFromWithInInstruction00081948E0Call as transferFromWithInInstructionCall,
|
||||||
};
|
};
|
||||||
pub use abi::IERC20::Transfer;
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests;
|
mod tests;
|
||||||
@@ -156,6 +158,8 @@ impl Erc20 {
|
|||||||
) => Vec::from(inInstruction),
|
) => Vec::from(inInstruction),
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
// We don't error here so this transfer is propagated up the stack, even without the
|
||||||
|
// InInstruction. In practice, Serai should acknowledge this and return it to the sender
|
||||||
vec![]
|
vec![]
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ ethereum-primitives = { package = "serai-processor-ethereum-primitives", path =
|
|||||||
ethereum-deployer = { package = "serai-processor-ethereum-deployer", path = "../deployer", default-features = false }
|
ethereum-deployer = { package = "serai-processor-ethereum-deployer", path = "../deployer", default-features = false }
|
||||||
erc20 = { package = "serai-processor-ethereum-erc20", path = "../erc20", default-features = false }
|
erc20 = { package = "serai-processor-ethereum-erc20", path = "../erc20", default-features = false }
|
||||||
|
|
||||||
|
scale = { package = "parity-scale-codec", version = "3", default-features = false, features = ["std"] }
|
||||||
serai-client = { path = "../../../substrate/client", default-features = false, features = ["ethereum"] }
|
serai-client = { path = "../../../substrate/client", default-features = false, features = ["ethereum"] }
|
||||||
|
|
||||||
futures-util = { version = "0.3", default-features = false, features = ["std"] }
|
futures-util = { version = "0.3", default-features = false, features = ["std"] }
|
||||||
|
|||||||
@@ -24,7 +24,10 @@ use alloy_transport::{TransportErrorKind, RpcError};
|
|||||||
use alloy_simple_request_transport::SimpleRequest;
|
use alloy_simple_request_transport::SimpleRequest;
|
||||||
use alloy_provider::{Provider, RootProvider};
|
use alloy_provider::{Provider, RootProvider};
|
||||||
|
|
||||||
use serai_client::networks::ethereum::Address as SeraiAddress;
|
use scale::Encode;
|
||||||
|
use serai_client::{
|
||||||
|
in_instructions::primitives::Shorthand, networks::ethereum::Address as SeraiAddress,
|
||||||
|
};
|
||||||
|
|
||||||
use ethereum_primitives::LogIndex;
|
use ethereum_primitives::LogIndex;
|
||||||
use ethereum_schnorr::{PublicKey, Signature};
|
use ethereum_schnorr::{PublicKey, Signature};
|
||||||
@@ -309,6 +312,8 @@ impl Router {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Construct a transaction to confirm the next key representing Serai.
|
/// Construct a transaction to confirm the next key representing Serai.
|
||||||
|
///
|
||||||
|
/// The gas price is not set and is left to the caller.
|
||||||
pub fn confirm_next_serai_key(&self, sig: &Signature) -> TxLegacy {
|
pub fn confirm_next_serai_key(&self, sig: &Signature) -> TxLegacy {
|
||||||
TxLegacy {
|
TxLegacy {
|
||||||
to: TxKind::Call(self.address),
|
to: TxKind::Call(self.address),
|
||||||
@@ -328,6 +333,8 @@ impl Router {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Construct a transaction to update the key representing Serai.
|
/// Construct a transaction to update the key representing Serai.
|
||||||
|
///
|
||||||
|
/// The gas price is not set and is left to the caller.
|
||||||
pub fn update_serai_key(&self, public_key: &PublicKey, sig: &Signature) -> TxLegacy {
|
pub fn update_serai_key(&self, public_key: &PublicKey, sig: &Signature) -> TxLegacy {
|
||||||
TxLegacy {
|
TxLegacy {
|
||||||
to: TxKind::Call(self.address),
|
to: TxKind::Call(self.address),
|
||||||
@@ -342,6 +349,37 @@ impl Router {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Construct a transaction to send coins with an InInstruction to Serai.
|
||||||
|
///
|
||||||
|
/// If coin is an ERC20, this will not create a transaction calling the Router but will create a
|
||||||
|
/// top-level transfer of the ERC20 to the Router. This avoids needing to call `approve` before
|
||||||
|
/// publishing the transaction calling the Router.
|
||||||
|
///
|
||||||
|
/// The gas limit and gas price are not set and are left to the caller.
|
||||||
|
pub fn in_instruction(&self, coin: Coin, amount: U256, in_instruction: &Shorthand) -> TxLegacy {
|
||||||
|
match coin {
|
||||||
|
Coin::Ether => TxLegacy {
|
||||||
|
to: self.address.into(),
|
||||||
|
input: abi::inInstructionCall::new((coin.into(), amount, in_instruction.encode().into()))
|
||||||
|
.abi_encode()
|
||||||
|
.into(),
|
||||||
|
value: amount,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
Coin::Erc20(erc20) => TxLegacy {
|
||||||
|
to: erc20.into(),
|
||||||
|
input: erc20::transferWithInInstructionCall::new((
|
||||||
|
self.address,
|
||||||
|
amount,
|
||||||
|
in_instruction.encode().into(),
|
||||||
|
))
|
||||||
|
.abi_encode()
|
||||||
|
.into(),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Get the message to be signed in order to execute a series of `OutInstruction`s.
|
/// Get the message to be signed in order to execute a series of `OutInstruction`s.
|
||||||
pub fn execute_message(
|
pub fn execute_message(
|
||||||
chain_id: U256,
|
chain_id: U256,
|
||||||
@@ -360,6 +398,8 @@ impl Router {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Construct a transaction to execute a batch of `OutInstruction`s.
|
/// Construct a transaction to execute a batch of `OutInstruction`s.
|
||||||
|
///
|
||||||
|
/// The gas limit and gas price are not set and are left to the caller.
|
||||||
pub fn execute(&self, coin: Coin, fee: U256, outs: OutInstructions, sig: &Signature) -> TxLegacy {
|
pub fn execute(&self, coin: Coin, fee: U256, outs: OutInstructions, sig: &Signature) -> TxLegacy {
|
||||||
// TODO
|
// TODO
|
||||||
let gas_limit = Self::EXECUTE_BASE_GAS + outs.0.iter().map(|_| 200_000 + 10_000).sum::<u64>();
|
let gas_limit = Self::EXECUTE_BASE_GAS + outs.0.iter().map(|_| 200_000 + 10_000).sum::<u64>();
|
||||||
@@ -383,6 +423,8 @@ impl Router {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Construct a transaction to trigger the escape hatch.
|
/// Construct a transaction to trigger the escape hatch.
|
||||||
|
///
|
||||||
|
/// The gas price is not set and is left to the caller.
|
||||||
pub fn escape_hatch(&self, escape_to: Address, sig: &Signature) -> TxLegacy {
|
pub fn escape_hatch(&self, escape_to: Address, sig: &Signature) -> TxLegacy {
|
||||||
TxLegacy {
|
TxLegacy {
|
||||||
to: TxKind::Call(self.address),
|
to: TxKind::Call(self.address),
|
||||||
@@ -393,6 +435,8 @@ impl Router {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Construct a transaction to escape coins via the escape hatch.
|
/// Construct a transaction to escape coins via the escape hatch.
|
||||||
|
///
|
||||||
|
/// The gas limit and gas price are not set and are left to the caller.
|
||||||
pub fn escape(&self, coin: Coin) -> TxLegacy {
|
pub fn escape(&self, coin: Coin) -> TxLegacy {
|
||||||
TxLegacy {
|
TxLegacy {
|
||||||
to: TxKind::Call(self.address),
|
to: TxKind::Call(self.address),
|
||||||
|
|||||||
@@ -18,6 +18,14 @@ use alloy_provider::{Provider, RootProvider};
|
|||||||
|
|
||||||
use alloy_node_bindings::{Anvil, AnvilInstance};
|
use alloy_node_bindings::{Anvil, AnvilInstance};
|
||||||
|
|
||||||
|
use scale::Encode;
|
||||||
|
use serai_client::{
|
||||||
|
primitives::SeraiAddress,
|
||||||
|
in_instructions::primitives::{
|
||||||
|
InInstruction as SeraiInInstruction, RefundableInInstruction, Shorthand,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
use ethereum_primitives::LogIndex;
|
use ethereum_primitives::LogIndex;
|
||||||
use ethereum_schnorr::{PublicKey, Signature};
|
use ethereum_schnorr::{PublicKey, Signature};
|
||||||
use ethereum_deployer::Deployer;
|
use ethereum_deployer::Deployer;
|
||||||
@@ -26,7 +34,7 @@ use crate::{
|
|||||||
_irouter_abi::IRouterWithoutCollisions::{
|
_irouter_abi::IRouterWithoutCollisions::{
|
||||||
self as IRouter, IRouterWithoutCollisionsErrors as IRouterErrors,
|
self as IRouter, IRouterWithoutCollisionsErrors as IRouterErrors,
|
||||||
},
|
},
|
||||||
Coin, OutInstructions, Router, Executed, Escape,
|
Coin, InInstruction, OutInstructions, Router, Executed, Escape,
|
||||||
};
|
};
|
||||||
|
|
||||||
mod constants;
|
mod constants;
|
||||||
@@ -165,6 +173,8 @@ impl Test {
|
|||||||
let tx = ethereum_primitives::deterministically_sign(tx);
|
let tx = ethereum_primitives::deterministically_sign(tx);
|
||||||
let receipt = ethereum_test_primitives::publish_tx(&self.provider, tx.clone()).await;
|
let receipt = ethereum_test_primitives::publish_tx(&self.provider, tx.clone()).await;
|
||||||
assert!(receipt.status());
|
assert!(receipt.status());
|
||||||
|
// Only check the gas is equal when writing to a previously unallocated storage slot, as this
|
||||||
|
// is the highest possible gas cost and what the constant is derived from
|
||||||
if self.state.key.is_none() {
|
if self.state.key.is_none() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
CalldataAgnosticGas::calculate(tx.tx(), receipt.gas_used),
|
CalldataAgnosticGas::calculate(tx.tx(), receipt.gas_used),
|
||||||
@@ -231,6 +241,21 @@ impl Test {
|
|||||||
self.verify_state().await;
|
self.verify_state().await;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn eth_in_instruction_tx(&self) -> (Coin, U256, Shorthand, TxLegacy) {
|
||||||
|
let coin = Coin::Ether;
|
||||||
|
let amount = U256::from(1);
|
||||||
|
let shorthand = Shorthand::Raw(RefundableInInstruction {
|
||||||
|
origin: None,
|
||||||
|
instruction: SeraiInInstruction::Transfer(SeraiAddress([0xff; 32])),
|
||||||
|
});
|
||||||
|
|
||||||
|
let mut tx = self.router.in_instruction(coin, amount, &shorthand);
|
||||||
|
tx.gas_limit = 1_000_000;
|
||||||
|
tx.gas_price = 100_000_000_000;
|
||||||
|
|
||||||
|
(coin, amount, shorthand, tx)
|
||||||
|
}
|
||||||
|
|
||||||
fn escape_hatch_tx(&self, escape_to: Address) -> TxLegacy {
|
fn escape_hatch_tx(&self, escape_to: Address) -> TxLegacy {
|
||||||
let msg = Router::escape_hatch_message(self.chain_id, self.state.next_nonce, escape_to);
|
let msg = Router::escape_hatch_message(self.chain_id, self.state.next_nonce, escape_to);
|
||||||
let sig = sign(self.state.key.unwrap(), &msg);
|
let sig = sign(self.state.key.unwrap(), &msg);
|
||||||
@@ -297,7 +322,43 @@ async fn test_update_serai_key() {
|
|||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_eth_in_instruction() {
|
async fn test_eth_in_instruction() {
|
||||||
todo!("TODO")
|
let mut test = Test::new().await;
|
||||||
|
test.confirm_next_serai_key().await;
|
||||||
|
|
||||||
|
let (coin, amount, shorthand, tx) = test.eth_in_instruction_tx();
|
||||||
|
|
||||||
|
// This should fail if the value mismatches the amount
|
||||||
|
{
|
||||||
|
let mut tx = tx.clone();
|
||||||
|
tx.value = U256::ZERO;
|
||||||
|
assert!(matches!(
|
||||||
|
test.call_and_decode_err(tx).await,
|
||||||
|
IRouterErrors::AmountMismatchesMsgValue(IRouter::AmountMismatchesMsgValue {})
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let tx = ethereum_primitives::deterministically_sign(tx);
|
||||||
|
let receipt = ethereum_test_primitives::publish_tx(&test.provider, tx.clone()).await;
|
||||||
|
assert!(receipt.status());
|
||||||
|
|
||||||
|
let block = receipt.block_number.unwrap();
|
||||||
|
let in_instructions =
|
||||||
|
test.router.in_instructions_unordered(block, block, &HashSet::new()).await.unwrap();
|
||||||
|
assert_eq!(in_instructions.len(), 1);
|
||||||
|
assert_eq!(
|
||||||
|
in_instructions[0],
|
||||||
|
InInstruction {
|
||||||
|
id: LogIndex {
|
||||||
|
block_hash: *receipt.block_hash.unwrap(),
|
||||||
|
index_within_block: receipt.inner.logs()[0].log_index.unwrap(),
|
||||||
|
},
|
||||||
|
transaction_hash: **tx.hash(),
|
||||||
|
from: tx.recover_signer().unwrap(),
|
||||||
|
coin,
|
||||||
|
amount,
|
||||||
|
data: shorthand.encode(),
|
||||||
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
@@ -379,7 +440,10 @@ async fn test_escape_hatch() {
|
|||||||
test.call_and_decode_err(test.confirm_next_serai_key_tx()).await,
|
test.call_and_decode_err(test.confirm_next_serai_key_tx()).await,
|
||||||
IRouterErrors::EscapeHatchInvoked(IRouter::EscapeHatchInvoked {})
|
IRouterErrors::EscapeHatchInvoked(IRouter::EscapeHatchInvoked {})
|
||||||
));
|
));
|
||||||
// TODO inInstruction
|
assert!(matches!(
|
||||||
|
test.call_and_decode_err(test.eth_in_instruction_tx().3).await,
|
||||||
|
IRouterErrors::EscapeHatchInvoked(IRouter::EscapeHatchInvoked {})
|
||||||
|
));
|
||||||
// TODO execute
|
// TODO execute
|
||||||
// We reject further attempts to update the escape hatch to prevent the last key from being
|
// We reject further attempts to update the escape hatch to prevent the last key from being
|
||||||
// able to switch from the honest escape hatch to siphoning via a malicious escape hatch (such
|
// able to switch from the honest escape hatch to siphoning via a malicious escape hatch (such
|
||||||
|
|||||||
Reference in New Issue
Block a user