mirror of
https://github.com/serai-dex/serai.git
synced 2025-03-24 08:08:51 +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
|
// 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;
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)]
|
||||||
|
|
|
@ -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?
|
||||||
{
|
{
|
||||||
|
|
Loading…
Reference in a new issue