diff --git a/processor/ethereum/router/contracts/Router.sol b/processor/ethereum/router/contracts/Router.sol index 5d8211da..12d4fa9c 100644 --- a/processor/ethereum/router/contracts/Router.sol +++ b/processor/ethereum/router/contracts/Router.sol @@ -243,6 +243,16 @@ contract Router is IRouterWithoutCollisions { // Re-entrancy doesn't bork this function // slither-disable-next-line reentrancy-events 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 if (coin == address(0)) { 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 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; diff --git a/processor/ethereum/router/src/lib.rs b/processor/ethereum/router/src/lib.rs index e28fb2f5..1531a5b9 100644 --- a/processor/ethereum/router/src/lib.rs +++ b/processor/ethereum/router/src/lib.rs @@ -70,16 +70,15 @@ pub enum Coin { /// Ether, the native coin of Ethereum. Ether, /// An ERC20 token. - Erc20([u8; 20]), + Erc20(Address), } impl Coin { fn address(&self) -> Address { - (match self { - Coin::Ether => [0; 20], + match self { + Coin::Ether => [0; 20].into(), Coin::Erc20(address) => *address, - }) - .into() + } } /// Read a `Coin`. @@ -91,7 +90,7 @@ impl Coin { 1 => { let mut address = [0; 20]; reader.read_exact(&mut address)?; - Coin::Erc20(address) + Coin::Erc20(address.into()) } _ => Err(io::Error::other("unrecognized Coin type"))?, }) @@ -103,7 +102,7 @@ impl Coin { Coin::Ether => writer.write_all(&[0]), Coin::Erc20(token) => { writer.write_all(&[1])?; - writer.write_all(token) + writer.write_all(token.as_ref()) } } } @@ -275,10 +274,12 @@ impl Executed { #[derive(Clone, Debug)] pub struct Router(Arc>, Address); 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 UPDATE_SERAI_KEY_GAS: u64 = 61_000; const EXECUTE_BASE_GAS: u64 = 48_000; + const ESCAPE_HATCH_GAS: u64 = 58_000; + const ESCAPE_GAS: u64 = 200_000; fn code() -> Vec { 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 { + 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. pub async fn in_instructions( &self, block: u64, - allowed_tokens: &HashSet<[u8; 20]>, + allowed_tokens: &HashSet
, ) -> Result, RpcError> { // The InInstruction events for this block 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] { Coin::Ether } else { - let token = *log.coin.0; + let token = log.coin; if !allowed_tokens.contains(&token) { continue; @@ -490,7 +520,7 @@ impl Router { } // 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; } // Check if this is a transfer log diff --git a/processor/ethereum/router/src/tests/mod.rs b/processor/ethereum/router/src/tests/mod.rs index 107723f8..e5f8f41e 100644 --- a/processor/ethereum/router/src/tests/mod.rs +++ b/processor/ethereum/router/src/tests/mod.rs @@ -177,7 +177,6 @@ async fn test_update_serai_key() { #[tokio::test] async fn test_eth_in_instruction() { 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(); @@ -291,7 +290,52 @@ async fn test_erc20_code_out_instruction() { todo!("TODO") } +async fn escape_hatch( + provider: &Arc>, + 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>, + 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] 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; } diff --git a/processor/ethereum/src/primitives/mod.rs b/processor/ethereum/src/primitives/mod.rs index 197acf8f..39f1eb94 100644 --- a/processor/ethereum/src/primitives/mod.rs +++ b/processor/ethereum/src/primitives/mod.rs @@ -1,3 +1,5 @@ +use alloy_core::primitives::{FixedBytes, Address}; + use serai_client::primitives::Amount; pub(crate) mod output; @@ -5,13 +7,14 @@ pub(crate) mod transaction; pub(crate) mod machine; 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") { Ok(res) => res, 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). #[allow(clippy::inconsistent_digit_grouping)] diff --git a/processor/ethereum/src/rpc.rs b/processor/ethereum/src/rpc.rs index 7f8a422b..610eb491 100644 --- a/processor/ethereum/src/rpc.rs +++ b/processor/ethereum/src/rpc.rs @@ -165,7 +165,7 @@ impl ScannerFeed for Rpc { let mut instructions = router.in_instructions(block.number, &HashSet::from(TOKENS)).await?; 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()) .await? {