Basic Ethereum escapeHatch test

This commit is contained in:
Luke Parker 2024-12-09 02:00:17 -05:00
parent 9ccfa8a9f5
commit 5b3c5ec02b
No known key found for this signature in database
5 changed files with 106 additions and 18 deletions

View file

@ -243,6 +243,16 @@ contract Router is IRouterWithoutCollisions {
// Re-entrancy doesn't bork this function // Re-entrancy doesn't bork this function
// slither-disable-next-line reentrancy-events // slither-disable-next-line reentrancy-events
function inInstruction(address coin, uint256 amount, bytes memory instruction) external payable { function inInstruction(address coin, uint256 amount, bytes memory instruction) external payable {
// Check there is an active key
if (_seraiKey == bytes32(0)) {
revert InvalidSeraiKey();
}
// Don't allow further InInstructions once the escape hatch has been invoked
if (_escapedTo != address(0)) {
revert EscapeHatchInvoked();
}
// Check the transfer // Check the transfer
if (coin == address(0)) { if (coin == address(0)) {
if (amount != msg.value) revert AmountMismatchesMsgValue(); if (amount != msg.value) revert AmountMismatchesMsgValue();
@ -313,7 +323,8 @@ contract Router is IRouterWithoutCollisions {
This should be in such excess of the gas requirements of integrated tokens we'll survive This should be in such excess of the gas requirements of integrated tokens we'll survive
repricing, so long as the repricing doesn't revolutionize EVM gas costs as we know it. In such repricing, so long as the repricing doesn't revolutionize EVM gas costs as we know it. In such
a case, Serai would have to migrate to a new smart contract using `escapeHatch`. a case, Serai would have to migrate to a new smart contract using `escapeHatch`. That also
covers all other potential exceptional cases.
*/ */
uint256 _gas = 100_000; uint256 _gas = 100_000;

View file

@ -70,16 +70,15 @@ pub enum Coin {
/// Ether, the native coin of Ethereum. /// Ether, the native coin of Ethereum.
Ether, Ether,
/// An ERC20 token. /// An ERC20 token.
Erc20([u8; 20]), Erc20(Address),
} }
impl Coin { impl Coin {
fn address(&self) -> Address { fn address(&self) -> Address {
(match self { match self {
Coin::Ether => [0; 20], Coin::Ether => [0; 20].into(),
Coin::Erc20(address) => *address, Coin::Erc20(address) => *address,
}) }
.into()
} }
/// Read a `Coin`. /// Read a `Coin`.
@ -91,7 +90,7 @@ impl Coin {
1 => { 1 => {
let mut address = [0; 20]; let mut address = [0; 20];
reader.read_exact(&mut address)?; reader.read_exact(&mut address)?;
Coin::Erc20(address) Coin::Erc20(address.into())
} }
_ => Err(io::Error::other("unrecognized Coin type"))?, _ => Err(io::Error::other("unrecognized Coin type"))?,
}) })
@ -103,7 +102,7 @@ impl Coin {
Coin::Ether => writer.write_all(&[0]), Coin::Ether => writer.write_all(&[0]),
Coin::Erc20(token) => { Coin::Erc20(token) => {
writer.write_all(&[1])?; writer.write_all(&[1])?;
writer.write_all(token) writer.write_all(token.as_ref())
} }
} }
} }
@ -275,10 +274,12 @@ impl Executed {
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct Router(Arc<RootProvider<SimpleRequest>>, Address); pub struct Router(Arc<RootProvider<SimpleRequest>>, Address);
impl Router { impl Router {
const DEPLOYMENT_GAS: u64 = 995_000; const DEPLOYMENT_GAS: u64 = 1_000_000;
const CONFIRM_NEXT_SERAI_KEY_GAS: u64 = 58_000; const CONFIRM_NEXT_SERAI_KEY_GAS: u64 = 58_000;
const UPDATE_SERAI_KEY_GAS: u64 = 61_000; const UPDATE_SERAI_KEY_GAS: u64 = 61_000;
const EXECUTE_BASE_GAS: u64 = 48_000; const EXECUTE_BASE_GAS: u64 = 48_000;
const ESCAPE_HATCH_GAS: u64 = 58_000;
const ESCAPE_GAS: u64 = 200_000;
fn code() -> Vec<u8> { fn code() -> Vec<u8> {
const BYTECODE: &[u8] = const BYTECODE: &[u8] =
@ -395,11 +396,40 @@ impl Router {
} }
} }
/// Get the message to be signed in order to trigger the escape hatch.
pub fn escape_hatch_message(nonce: u64, escape_to: Address) -> Vec<u8> {
abi::escapeHatchCall::new((
abi::Signature { c: U256::try_from(nonce).unwrap().into(), s: U256::ZERO.into() },
escape_to,
))
.abi_encode()
}
/// Construct a transaction to trigger the escape hatch.
pub fn escape_hatch(&self, escape_to: Address, sig: &Signature) -> TxLegacy {
TxLegacy {
to: TxKind::Call(self.1),
input: abi::escapeHatchCall::new((abi::Signature::from(sig), escape_to)).abi_encode().into(),
gas_limit: Self::ESCAPE_HATCH_GAS * 120 / 100,
..Default::default()
}
}
/// Construct a transaction to escape coins via the escape hatch.
pub fn escape(&self, coin: Address) -> TxLegacy {
TxLegacy {
to: TxKind::Call(self.1),
input: abi::escapeCall::new((coin,)).abi_encode().into(),
gas_limit: Self::ESCAPE_GAS,
..Default::default()
}
}
/// Fetch the `InInstruction`s emitted by the Router from this block. /// Fetch the `InInstruction`s emitted by the Router from this block.
pub async fn in_instructions( pub async fn in_instructions(
&self, &self,
block: u64, block: u64,
allowed_tokens: &HashSet<[u8; 20]>, allowed_tokens: &HashSet<Address>,
) -> Result<Vec<InInstruction>, RpcError<TransportErrorKind>> { ) -> Result<Vec<InInstruction>, RpcError<TransportErrorKind>> {
// The InInstruction events for this block // The InInstruction events for this block
let filter = Filter::new().from_block(block).to_block(block).address(self.1); let filter = Filter::new().from_block(block).to_block(block).address(self.1);
@ -451,7 +481,7 @@ impl Router {
let coin = if log.coin.0 == [0; 20] { let coin = if log.coin.0 == [0; 20] {
Coin::Ether Coin::Ether
} else { } else {
let token = *log.coin.0; let token = log.coin;
if !allowed_tokens.contains(&token) { if !allowed_tokens.contains(&token) {
continue; continue;
@ -490,7 +520,7 @@ impl Router {
} }
// Check if this log is from the token we expected to be transferred // Check if this log is from the token we expected to be transferred
if tx_log.address().0 != token { if tx_log.address() != token {
continue; continue;
} }
// Check if this is a transfer log // Check if this is a transfer log

View file

@ -177,7 +177,6 @@ async fn test_update_serai_key() {
#[tokio::test] #[tokio::test]
async fn test_eth_in_instruction() { 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; confirm_next_serai_key(&provider, &router, 1, key).await;
let amount = U256::try_from(OsRng.next_u64()).unwrap(); let amount = U256::try_from(OsRng.next_u64()).unwrap();
@ -291,7 +290,52 @@ async fn test_erc20_code_out_instruction() {
todo!("TODO") todo!("TODO")
} }
async fn escape_hatch(
provider: &Arc<RootProvider<SimpleRequest>>,
router: &Router,
nonce: u64,
key: (Scalar, PublicKey),
escape_to: Address,
) -> TransactionReceipt {
let msg = Router::escape_hatch_message(nonce, escape_to);
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.escape_hatch(escape_to, &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::ESCAPE_HATCH_GAS), ((receipt.gas_used + 1000) / 1000) * 1000);
receipt
}
async fn escape(
provider: &Arc<RootProvider<SimpleRequest>>,
router: &Router,
coin: Coin,
) -> TransactionReceipt {
let mut tx = router.escape(coin.address());
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());
receipt
}
#[tokio::test] #[tokio::test]
async fn test_escape_hatch() { async fn test_escape_hatch() {
todo!("TODO") let (_anvil, provider, router, key) = setup_test().await;
confirm_next_serai_key(&provider, &router, 1, key).await;
let escape_to: Address = {
let mut escape_to = [0; 20];
OsRng.fill_bytes(&mut escape_to);
escape_to.into()
};
escape_hatch(&provider, &router, 2, key, escape_to).await;
escape(&provider, &router, Coin::Ether).await;
} }

View file

@ -1,3 +1,5 @@
use alloy_core::primitives::{FixedBytes, Address};
use serai_client::primitives::Amount; use serai_client::primitives::Amount;
pub(crate) mod output; pub(crate) mod output;
@ -5,13 +7,14 @@ pub(crate) mod transaction;
pub(crate) mod machine; pub(crate) mod machine;
pub(crate) mod block; pub(crate) mod block;
pub(crate) const DAI: [u8; 20] = pub(crate) const DAI: Address = Address(FixedBytes(
match const_hex::const_decode_to_array(b"0x6B175474E89094C44Da98b954EedeAC495271d0F") { match const_hex::const_decode_to_array(b"0x6B175474E89094C44Da98b954EedeAC495271d0F") {
Ok(res) => res, Ok(res) => res,
Err(_) => panic!("invalid non-test DAI hex address"), Err(_) => panic!("invalid non-test DAI hex address"),
}; },
));
pub(crate) const TOKENS: [[u8; 20]; 1] = [DAI]; pub(crate) const TOKENS: [Address; 1] = [DAI];
// 8 decimals, so 1_000_000_00 would be 1 ETH. This is 0.0015 ETH (5 USD if Ether is ~3300 USD). // 8 decimals, so 1_000_000_00 would be 1 ETH. This is 0.0015 ETH (5 USD if Ether is ~3300 USD).
#[allow(clippy::inconsistent_digit_grouping)] #[allow(clippy::inconsistent_digit_grouping)]

View file

@ -165,7 +165,7 @@ impl<D: Db> ScannerFeed for Rpc<D> {
let mut instructions = router.in_instructions(block.number, &HashSet::from(TOKENS)).await?; let mut instructions = router.in_instructions(block.number, &HashSet::from(TOKENS)).await?;
for token in TOKENS { for token in TOKENS {
for TopLevelTransfer { id, from, amount, data } in Erc20::new(provider.clone(), token) for TopLevelTransfer { id, from, amount, data } in Erc20::new(provider.clone(), **token)
.top_level_transfers(block.number, router.address()) .top_level_transfers(block.number, router.address())
.await? .await?
{ {