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
// 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;

View file

@ -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<RootProvider<SimpleRequest>>, 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<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.
pub async fn in_instructions(
&self,
block: u64,
allowed_tokens: &HashSet<[u8; 20]>,
allowed_tokens: &HashSet<Address>,
) -> Result<Vec<InInstruction>, RpcError<TransportErrorKind>> {
// 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

View file

@ -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<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]
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;
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)]

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?;
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?
{