mirror of
https://github.com/serai-dex/serai.git
synced 2025-12-08 20:29:23 +00:00
Test ETH address/code OutInstructions
This commit is contained in:
@@ -68,14 +68,6 @@ use abi::{
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests;
|
mod tests;
|
||||||
|
|
||||||
// As per Dencun, used for estimating gas for determining relayer fees
|
|
||||||
const NON_ZERO_BYTE_GAS_COST: u64 = 16;
|
|
||||||
const MEMORY_EXPANSION_COST: u64 = 3; // Does not model the quadratic cost
|
|
||||||
const COLD_COST: u64 = 2_600;
|
|
||||||
const WARM_COST: u64 = 100;
|
|
||||||
const POSITIVE_VALUE_COST: u64 = 9_000;
|
|
||||||
const EMPTY_ACCOUNT_COST: u64 = 25_000;
|
|
||||||
|
|
||||||
impl From<&Signature> for abi::Signature {
|
impl From<&Signature> for abi::Signature {
|
||||||
fn from(signature: &Signature) -> Self {
|
fn from(signature: &Signature) -> Self {
|
||||||
Self {
|
Self {
|
||||||
@@ -247,6 +239,7 @@ pub struct Router {
|
|||||||
}
|
}
|
||||||
impl Router {
|
impl Router {
|
||||||
// Gas allocated for ERC20 calls
|
// Gas allocated for ERC20 calls
|
||||||
|
#[cfg(test)]
|
||||||
const GAS_FOR_ERC20_CALL: u64 = 100_000;
|
const GAS_FOR_ERC20_CALL: u64 = 100_000;
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -262,7 +255,12 @@ impl Router {
|
|||||||
*/
|
*/
|
||||||
const CONFIRM_NEXT_SERAI_KEY_GAS: u64 = 57_736;
|
const CONFIRM_NEXT_SERAI_KEY_GAS: u64 = 57_736;
|
||||||
const UPDATE_SERAI_KEY_GAS: u64 = 60_045;
|
const UPDATE_SERAI_KEY_GAS: u64 = 60_045;
|
||||||
const EXECUTE_BASE_GAS: u64 = 51_131;
|
const EXECUTE_ETH_BASE_GAS: u64 = 51_131;
|
||||||
|
const EXECUTE_ERC20_BASE_GAS: u64 = 149_831;
|
||||||
|
const EXECUTE_ETH_ADDRESS_OUT_INSTRUCTION_GAS: u64 = 41_453;
|
||||||
|
const EXECUTE_ETH_CODE_OUT_INSTRUCTION_GAS: u64 = 51_723;
|
||||||
|
const EXECUTE_ERC20_ADDRESS_OUT_INSTRUCTION_GAS: u64 = 0; // TODO
|
||||||
|
const EXECUTE_ERC20_CODE_OUT_INSTRUCTION_GAS: u64 = 0; // TODO
|
||||||
const ESCAPE_HATCH_GAS: u64 = 61_238;
|
const ESCAPE_HATCH_GAS: u64 = 61_238;
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -432,51 +430,39 @@ impl Router {
|
|||||||
coin: Coin,
|
coin: Coin,
|
||||||
instruction: &abi::OutInstruction,
|
instruction: &abi::OutInstruction,
|
||||||
) -> u64 {
|
) -> u64 {
|
||||||
// The assigned cost for performing an additional iteration of the loop
|
// As per Dencun, used for estimating gas for determining relayer fees
|
||||||
const ITERATION_COST: u64 = 5_000;
|
const NON_ZERO_BYTE_GAS_COST: u64 = 16;
|
||||||
// The additional cost for a `DestinationType.Code`, as an additional buffer for its complexity
|
const MEMORY_EXPANSION_COST: u64 = 3; // Does not model the quadratic cost
|
||||||
const CODE_COST: u64 = 10_000;
|
|
||||||
|
|
||||||
let size = u64::try_from(instruction.abi_encoded_size()).unwrap();
|
let size = u64::try_from(instruction.abi_encoded_size()).unwrap();
|
||||||
let calldata_memory_cost =
|
let calldata_memory_cost =
|
||||||
(NON_ZERO_BYTE_GAS_COST * size) + (MEMORY_EXPANSION_COST * size.div_ceil(32));
|
(size * NON_ZERO_BYTE_GAS_COST) + (size.div_ceil(32) * MEMORY_EXPANSION_COST);
|
||||||
|
|
||||||
ITERATION_COST +
|
match coin {
|
||||||
(match coin {
|
Coin::Ether => match instruction.destinationType {
|
||||||
Coin::Ether => match instruction.destinationType {
|
// The calldata and memory cost is already part of this
|
||||||
// We assume we're tranferring a positive value to a cold, empty account
|
abi::DestinationType::Address => Self::EXECUTE_ETH_ADDRESS_OUT_INSTRUCTION_GAS,
|
||||||
abi::DestinationType::Address => {
|
abi::DestinationType::Code => {
|
||||||
calldata_memory_cost + COLD_COST + POSITIVE_VALUE_COST + EMPTY_ACCOUNT_COST
|
// OutInstructions can't be encoded/decoded and doesn't have pub internals, enabling it
|
||||||
}
|
// to be correct by construction
|
||||||
abi::DestinationType::Code => {
|
let code = abi::CodeDestination::abi_decode(&instruction.destination, true).unwrap();
|
||||||
// OutInstructions can't be encoded/decoded and doesn't have pub internals, enabling it
|
Self::EXECUTE_ETH_CODE_OUT_INSTRUCTION_GAS +
|
||||||
// to be correct by construction
|
|
||||||
let code = abi::CodeDestination::abi_decode(&instruction.destination, true).unwrap();
|
|
||||||
// This performs a call to self with the value, incurring the positive-value cost before
|
|
||||||
// CREATE's
|
|
||||||
calldata_memory_cost +
|
calldata_memory_cost +
|
||||||
CODE_COST +
|
u64::from(code.gasLimit)
|
||||||
(WARM_COST + POSITIVE_VALUE_COST + u64::from(code.gasLimit))
|
|
||||||
}
|
|
||||||
abi::DestinationType::__Invalid => unreachable!(),
|
|
||||||
},
|
|
||||||
Coin::Erc20(_) => {
|
|
||||||
// The ERC20 is warmed by the fee payment to the relayer
|
|
||||||
let erc20_call_gas = WARM_COST + Self::GAS_FOR_ERC20_CALL;
|
|
||||||
match instruction.destinationType {
|
|
||||||
abi::DestinationType::Address => calldata_memory_cost + erc20_call_gas,
|
|
||||||
abi::DestinationType::Code => {
|
|
||||||
let code = abi::CodeDestination::abi_decode(&instruction.destination, true).unwrap();
|
|
||||||
calldata_memory_cost +
|
|
||||||
CODE_COST +
|
|
||||||
erc20_call_gas +
|
|
||||||
// Call to self to deploy the contract
|
|
||||||
(WARM_COST + u64::from(code.gasLimit))
|
|
||||||
}
|
|
||||||
abi::DestinationType::__Invalid => unreachable!(),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
})
|
abi::DestinationType::__Invalid => unreachable!(),
|
||||||
|
},
|
||||||
|
Coin::Erc20(_) => match instruction.destinationType {
|
||||||
|
abi::DestinationType::Address => Self::EXECUTE_ERC20_ADDRESS_OUT_INSTRUCTION_GAS,
|
||||||
|
abi::DestinationType::Code => {
|
||||||
|
let code = abi::CodeDestination::abi_decode(&instruction.destination, true).unwrap();
|
||||||
|
Self::EXECUTE_ERC20_CODE_OUT_INSTRUCTION_GAS +
|
||||||
|
calldata_memory_cost +
|
||||||
|
u64::from(code.gasLimit)
|
||||||
|
}
|
||||||
|
abi::DestinationType::__Invalid => unreachable!(),
|
||||||
|
},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The estimated gas cost for this OutInstruction.
|
/// The estimated gas cost for this OutInstruction.
|
||||||
@@ -495,18 +481,16 @@ impl Router {
|
|||||||
/// This is not guaranteed to be correct or even sufficient. It is a hint and a hint alone used
|
/// This is not guaranteed to be correct or even sufficient. It is a hint and a hint alone used
|
||||||
/// for determining relayer fees.
|
/// for determining relayer fees.
|
||||||
pub fn execute_gas_estimate(coin: Coin, outs: &OutInstructions) -> u64 {
|
pub fn execute_gas_estimate(coin: Coin, outs: &OutInstructions) -> u64 {
|
||||||
Self::EXECUTE_BASE_GAS +
|
(match coin {
|
||||||
(match coin {
|
// This is warm as it's the message sender who is called with the fee payment
|
||||||
// This is warm as it's the message sender who is called with the fee payment
|
Coin::Ether => Self::EXECUTE_ETH_BASE_GAS,
|
||||||
Coin::Ether => WARM_COST + POSITIVE_VALUE_COST,
|
// This is cold as we say the fee payment is the one warming the ERC20
|
||||||
// This is cold as we say the fee payment is the one warming the ERC20
|
Coin::Erc20(_) => Self::EXECUTE_ERC20_BASE_GAS,
|
||||||
Coin::Erc20(_) => COLD_COST + Self::GAS_FOR_ERC20_CALL,
|
}) + outs
|
||||||
}) +
|
.0
|
||||||
outs
|
.iter()
|
||||||
.0
|
.map(|out| Self::execute_out_instruction_gas_estimate_internal(coin, out))
|
||||||
.iter()
|
.sum::<u64>()
|
||||||
.map(|out| Self::execute_out_instruction_gas_estimate_internal(coin, out))
|
|
||||||
.sum::<u64>()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Construct a transaction to execute a batch of `OutInstruction`s.
|
/// Construct a transaction to execute a batch of `OutInstruction`s.
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ use alloy_node_bindings::{Anvil, AnvilInstance};
|
|||||||
|
|
||||||
use scale::Encode;
|
use scale::Encode;
|
||||||
use serai_client::{
|
use serai_client::{
|
||||||
networks::ethereum::Address as SeraiEthereumAddress,
|
networks::ethereum::{ContractDeployment, Address as SeraiEthereumAddress},
|
||||||
primitives::SeraiAddress,
|
primitives::SeraiAddress,
|
||||||
in_instructions::primitives::{
|
in_instructions::primitives::{
|
||||||
InInstruction as SeraiInInstruction, RefundableInInstruction, Shorthand,
|
InInstruction as SeraiInInstruction, RefundableInInstruction, Shorthand,
|
||||||
@@ -41,6 +41,8 @@ mod constants;
|
|||||||
mod erc20;
|
mod erc20;
|
||||||
use erc20::Erc20;
|
use erc20::Erc20;
|
||||||
|
|
||||||
|
const CALL_GAS_STIPEND: u64 = 2_300;
|
||||||
|
|
||||||
pub(crate) fn test_key() -> (Scalar, PublicKey) {
|
pub(crate) fn test_key() -> (Scalar, PublicKey) {
|
||||||
loop {
|
loop {
|
||||||
let key = Scalar::random(&mut OsRng);
|
let key = Scalar::random(&mut OsRng);
|
||||||
@@ -348,7 +350,7 @@ impl Test {
|
|||||||
fee: U256,
|
fee: U256,
|
||||||
out_instructions: &[(SeraiEthereumAddress, U256)],
|
out_instructions: &[(SeraiEthereumAddress, U256)],
|
||||||
results: Vec<bool>,
|
results: Vec<bool>,
|
||||||
) -> u64 {
|
) -> (Signed<TxLegacy>, u64, u64) {
|
||||||
let (message_hash, mut tx) = self.execute_tx(coin, fee, out_instructions);
|
let (message_hash, mut tx) = self.execute_tx(coin, fee, out_instructions);
|
||||||
tx.gas_price = 100_000_000_000;
|
tx.gas_price = 100_000_000_000;
|
||||||
let tx = ethereum_primitives::deterministically_sign(tx);
|
let tx = ethereum_primitives::deterministically_sign(tx);
|
||||||
@@ -371,7 +373,7 @@ impl Test {
|
|||||||
self.verify_state().await;
|
self.verify_state().await;
|
||||||
|
|
||||||
// We do return the gas used in case a caller can benefit from it
|
// We do return the gas used in case a caller can benefit from it
|
||||||
CalldataAgnosticGas::calculate(tx.tx(), receipt.gas_used)
|
(tx.clone(), receipt.gas_used, CalldataAgnosticGas::calculate(tx.tx(), receipt.gas_used))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn escape_hatch_tx(&self, escape_to: Address) -> TxLegacy {
|
fn escape_hatch_tx(&self, escape_to: Address) -> TxLegacy {
|
||||||
@@ -645,28 +647,118 @@ async fn test_empty_execute() {
|
|||||||
test.confirm_next_serai_key().await;
|
test.confirm_next_serai_key().await;
|
||||||
let () =
|
let () =
|
||||||
test.provider.raw_request("anvil_setBalance".into(), (test.router.address(), 1)).await.unwrap();
|
test.provider.raw_request("anvil_setBalance".into(), (test.router.address(), 1)).await.unwrap();
|
||||||
let gas_used = test.execute(Coin::Ether, U256::from(1), &[], vec![]).await;
|
|
||||||
|
|
||||||
// For the empty ETH case, we do compare this cost to the base cost
|
{
|
||||||
const CALL_GAS_STIPEND: u64 = 2_300;
|
let (tx, raw_gas_used, gas_used) = test.execute(Coin::Ether, U256::from(1), &[], vec![]).await;
|
||||||
// We don't use the call gas stipend here
|
// We don't use the call gas stipend here
|
||||||
const UNUSED_GAS: u64 = CALL_GAS_STIPEND;
|
const UNUSED_GAS: u64 = CALL_GAS_STIPEND;
|
||||||
assert_eq!(gas_used + UNUSED_GAS, Router::EXECUTE_BASE_GAS);
|
assert_eq!(gas_used + UNUSED_GAS, Router::EXECUTE_ETH_BASE_GAS);
|
||||||
|
|
||||||
|
assert_eq!(test.provider.get_balance(test.router.address()).await.unwrap(), U256::from(0));
|
||||||
|
let minted_to_sender = u128::from(tx.tx().gas_limit) * tx.tx().gas_price;
|
||||||
|
let spent_by_sender = u128::from(raw_gas_used) * tx.tx().gas_price;
|
||||||
|
assert_eq!(
|
||||||
|
test.provider.get_balance(tx.recover_signer().unwrap()).await.unwrap() -
|
||||||
|
U256::from(minted_to_sender - spent_by_sender),
|
||||||
|
U256::from(1)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
// This uses a token of Address(0) as it'll be interpreted as a non-standard ERC20 which uses 0
|
||||||
|
// gas, letting us safely evaluate the EXECUTE_ERC20_BASE_GAS constant
|
||||||
|
let (_tx, _raw_gas_used, gas_used) =
|
||||||
|
test.execute(Coin::Erc20(Address::ZERO), U256::from(1), &[], vec![]).await;
|
||||||
|
// Add an extra 1000 gas for decoding the return value which would exist if a compliant ERC20
|
||||||
|
const UNUSED_GAS: u64 = Router::GAS_FOR_ERC20_CALL + 1000;
|
||||||
|
assert_eq!(gas_used + UNUSED_GAS, Router::EXECUTE_ERC20_BASE_GAS);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_eth_address_out_instruction() {
|
async fn test_eth_address_out_instruction() {
|
||||||
todo!("TODO")
|
let mut test = Test::new().await;
|
||||||
|
test.confirm_next_serai_key().await;
|
||||||
|
let () =
|
||||||
|
test.provider.raw_request("anvil_setBalance".into(), (test.router.address(), 3)).await.unwrap();
|
||||||
|
|
||||||
|
let mut rand_address = [0xff; 20];
|
||||||
|
OsRng.fill_bytes(&mut rand_address);
|
||||||
|
let (tx, raw_gas_used, gas_used) = test
|
||||||
|
.execute(
|
||||||
|
Coin::Ether,
|
||||||
|
U256::from(1),
|
||||||
|
&[(SeraiEthereumAddress::Address(rand_address), U256::from(2))],
|
||||||
|
vec![true],
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
// We don't use the call gas stipend here
|
||||||
|
const UNUSED_GAS: u64 = CALL_GAS_STIPEND;
|
||||||
|
// This doesn't model the quadratic memory costs
|
||||||
|
let gas_for_eth_address_out_instruction = gas_used + UNUSED_GAS - Router::EXECUTE_ETH_BASE_GAS;
|
||||||
|
// 2000 gas as a surplus for the quadratic memory cost and any inaccuracies
|
||||||
|
assert_eq!(
|
||||||
|
gas_for_eth_address_out_instruction + 2000,
|
||||||
|
Router::EXECUTE_ETH_ADDRESS_OUT_INSTRUCTION_GAS
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(test.provider.get_balance(test.router.address()).await.unwrap(), U256::from(0));
|
||||||
|
let minted_to_sender = u128::from(tx.tx().gas_limit) * tx.tx().gas_price;
|
||||||
|
let spent_by_sender = u128::from(raw_gas_used) * tx.tx().gas_price;
|
||||||
|
assert_eq!(
|
||||||
|
test.provider.get_balance(tx.recover_signer().unwrap()).await.unwrap() -
|
||||||
|
U256::from(minted_to_sender - spent_by_sender),
|
||||||
|
U256::from(1)
|
||||||
|
);
|
||||||
|
assert_eq!(test.provider.get_balance(rand_address.into()).await.unwrap(), U256::from(2));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_erc20_address_out_instruction() {
|
async fn test_erc20_address_out_instruction() {
|
||||||
todo!("TODO")
|
todo!("TODO")
|
||||||
|
/*
|
||||||
|
assert_eq!(erc20.balance_of(&test, test.router.address()).await, U256::from(0));
|
||||||
|
assert_eq!(erc20.balance_of(&test, test.state.escaped_to.unwrap()).await, amount);
|
||||||
|
*/
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_eth_code_out_instruction() {
|
async fn test_eth_code_out_instruction() {
|
||||||
todo!("TODO")
|
let mut test = Test::new().await;
|
||||||
|
test.confirm_next_serai_key().await;
|
||||||
|
let () =
|
||||||
|
test.provider.raw_request("anvil_setBalance".into(), (test.router.address(), 3)).await.unwrap();
|
||||||
|
|
||||||
|
let mut rand_address = [0xff; 20];
|
||||||
|
OsRng.fill_bytes(&mut rand_address);
|
||||||
|
let (tx, raw_gas_used, gas_used) = test
|
||||||
|
.execute(
|
||||||
|
Coin::Ether,
|
||||||
|
U256::from(1),
|
||||||
|
&[(
|
||||||
|
SeraiEthereumAddress::Contract(ContractDeployment::new(100_000, vec![]).unwrap()),
|
||||||
|
U256::from(2),
|
||||||
|
)],
|
||||||
|
vec![true],
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
// This doesn't model the quadratic memory costs
|
||||||
|
let gas_for_eth_code_out_instruction = gas_used - Router::EXECUTE_ETH_BASE_GAS;
|
||||||
|
// 2000 gas as a surplus for the quadratic memory cost and any inaccuracies
|
||||||
|
assert_eq!(gas_for_eth_code_out_instruction + 2000, Router::EXECUTE_ETH_CODE_OUT_INSTRUCTION_GAS);
|
||||||
|
|
||||||
|
assert_eq!(test.provider.get_balance(test.router.address()).await.unwrap(), U256::from(0));
|
||||||
|
let minted_to_sender = u128::from(tx.tx().gas_limit) * tx.tx().gas_price;
|
||||||
|
let spent_by_sender = u128::from(raw_gas_used) * tx.tx().gas_price;
|
||||||
|
assert_eq!(
|
||||||
|
test.provider.get_balance(tx.recover_signer().unwrap()).await.unwrap() -
|
||||||
|
U256::from(minted_to_sender - spent_by_sender),
|
||||||
|
U256::from(1)
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
test.provider.get_balance(test.router.address().create(1)).await.unwrap(),
|
||||||
|
U256::from(2)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
@@ -854,7 +946,7 @@ async fn test_eth_address_out_instruction() {
|
|||||||
let instructions = OutInstructions::from([].as_slice());
|
let instructions = OutInstructions::from([].as_slice());
|
||||||
let receipt = publish_outs(&provider, &router, key, 2, Coin::Ether, fee, instructions).await;
|
let receipt = publish_outs(&provider, &router, key, 2, Coin::Ether, fee, instructions).await;
|
||||||
assert!(receipt.status());
|
assert!(receipt.status());
|
||||||
assert_eq!(Router::EXECUTE_BASE_GAS, ((receipt.gas_used + 1000) / 1000) * 1000);
|
assert_eq!(Router::EXECUTE_ETH_BASE_GAS, ((receipt.gas_used + 1000) / 1000) * 1000);
|
||||||
|
|
||||||
assert_eq!(router.next_nonce(receipt.block_hash.unwrap().into()).await.unwrap(), 3);
|
assert_eq!(router.next_nonce(receipt.block_hash.unwrap().into()).await.unwrap(), 3);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user