mirror of
https://github.com/serai-dex/serai.git
synced 2025-04-22 22:18:15 +00:00
Test Ether InInstructions
This commit is contained in:
parent
e922264ebf
commit
a63a86ba79
5 changed files with 121 additions and 7 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
@ -9483,6 +9483,7 @@ dependencies = [
|
|||
"futures-util",
|
||||
"group",
|
||||
"k256",
|
||||
"parity-scale-codec",
|
||||
"rand_core",
|
||||
"serai-client",
|
||||
"serai-ethereum-test-primitives",
|
||||
|
|
|
@ -21,6 +21,7 @@ use futures_util::stream::{StreamExt, FuturesUnordered};
|
|||
#[rustfmt::skip]
|
||||
#[expect(warnings)]
|
||||
#[expect(needless_pass_by_value)]
|
||||
#[expect(missing_docs)]
|
||||
#[expect(clippy::all)]
|
||||
#[expect(clippy::ignored_unit_patterns)]
|
||||
#[expect(clippy::redundant_closure_for_method_calls)]
|
||||
|
@ -28,11 +29,12 @@ mod abi {
|
|||
alloy_sol_macro::sol!("contracts/IERC20.sol");
|
||||
}
|
||||
use abi::IERC20::{IERC20Calls, transferCall, transferFromCall};
|
||||
use abi::SeraiIERC20::{
|
||||
SeraiIERC20Calls, transferWithInInstruction01BB244A8ACall as transferWithInInstructionCall,
|
||||
use abi::SeraiIERC20::SeraiIERC20Calls;
|
||||
pub use abi::IERC20::Transfer;
|
||||
pub use abi::SeraiIERC20::{
|
||||
transferWithInInstruction01BB244A8ACall as transferWithInInstructionCall,
|
||||
transferFromWithInInstruction00081948E0Call as transferFromWithInInstructionCall,
|
||||
};
|
||||
pub use abi::IERC20::Transfer;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
@ -156,6 +158,8 @@ impl Erc20 {
|
|||
) => Vec::from(inInstruction),
|
||||
}
|
||||
} 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![]
|
||||
};
|
||||
|
||||
|
|
|
@ -39,6 +39,7 @@ ethereum-primitives = { package = "serai-processor-ethereum-primitives", path =
|
|||
ethereum-deployer = { package = "serai-processor-ethereum-deployer", path = "../deployer", 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"] }
|
||||
|
||||
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_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_schnorr::{PublicKey, Signature};
|
||||
|
@ -309,6 +312,8 @@ impl Router {
|
|||
}
|
||||
|
||||
/// 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 {
|
||||
TxLegacy {
|
||||
to: TxKind::Call(self.address),
|
||||
|
@ -328,6 +333,8 @@ impl Router {
|
|||
}
|
||||
|
||||
/// 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 {
|
||||
TxLegacy {
|
||||
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.
|
||||
pub fn execute_message(
|
||||
chain_id: U256,
|
||||
|
@ -360,6 +398,8 @@ impl Router {
|
|||
}
|
||||
|
||||
/// 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 {
|
||||
// TODO
|
||||
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.
|
||||
///
|
||||
/// The gas price is not set and is left to the caller.
|
||||
pub fn escape_hatch(&self, escape_to: Address, sig: &Signature) -> TxLegacy {
|
||||
TxLegacy {
|
||||
to: TxKind::Call(self.address),
|
||||
|
@ -393,6 +435,8 @@ impl Router {
|
|||
}
|
||||
|
||||
/// 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 {
|
||||
TxLegacy {
|
||||
to: TxKind::Call(self.address),
|
||||
|
|
|
@ -18,6 +18,14 @@ use alloy_provider::{Provider, RootProvider};
|
|||
|
||||
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_schnorr::{PublicKey, Signature};
|
||||
use ethereum_deployer::Deployer;
|
||||
|
@ -26,7 +34,7 @@ use crate::{
|
|||
_irouter_abi::IRouterWithoutCollisions::{
|
||||
self as IRouter, IRouterWithoutCollisionsErrors as IRouterErrors,
|
||||
},
|
||||
Coin, OutInstructions, Router, Executed, Escape,
|
||||
Coin, InInstruction, OutInstructions, Router, Executed, Escape,
|
||||
};
|
||||
|
||||
mod constants;
|
||||
|
@ -165,6 +173,8 @@ impl Test {
|
|||
let tx = ethereum_primitives::deterministically_sign(tx);
|
||||
let receipt = ethereum_test_primitives::publish_tx(&self.provider, tx.clone()).await;
|
||||
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() {
|
||||
assert_eq!(
|
||||
CalldataAgnosticGas::calculate(tx.tx(), receipt.gas_used),
|
||||
|
@ -231,6 +241,21 @@ impl Test {
|
|||
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 {
|
||||
let msg = Router::escape_hatch_message(self.chain_id, self.state.next_nonce, escape_to);
|
||||
let sig = sign(self.state.key.unwrap(), &msg);
|
||||
|
@ -297,7 +322,43 @@ async fn test_update_serai_key() {
|
|||
|
||||
#[tokio::test]
|
||||
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]
|
||||
|
@ -379,7 +440,10 @@ async fn test_escape_hatch() {
|
|||
test.call_and_decode_err(test.confirm_next_serai_key_tx()).await,
|
||||
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
|
||||
// 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
|
||||
|
|
Loading…
Reference in a new issue