mirror of
https://github.com/serai-dex/serai.git
synced 2024-12-22 19:49:22 +00:00
Basic Ethereum escapeHatch test
This commit is contained in:
parent
9ccfa8a9f5
commit
5b3c5ec02b
5 changed files with 106 additions and 18 deletions
|
@ -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;
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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)]
|
||||
|
|
|
@ -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?
|
||||
{
|
||||
|
|
Loading…
Reference in a new issue