mirror of
https://github.com/serai-dex/serai.git
synced 2025-12-12 22:19:26 +00:00
Compare commits
5 Commits
e742a6b0ec
...
f8c3acae7b
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f8c3acae7b | ||
|
|
0957460f27 | ||
|
|
ea00ba9ff8 | ||
|
|
a9625364df | ||
|
|
75c6427d7c |
@@ -32,6 +32,17 @@ fn main() {
|
||||
&artifacts_path,
|
||||
)
|
||||
.unwrap();
|
||||
// These are detected multiple times and distinguished, hence their renaming to canonical forms
|
||||
fs::rename(
|
||||
artifacts_path.clone() + "/Router_sol_Router.bin",
|
||||
artifacts_path.clone() + "/Router.bin",
|
||||
)
|
||||
.unwrap();
|
||||
fs::rename(
|
||||
artifacts_path.clone() + "/Router_sol_Router.bin-runtime",
|
||||
artifacts_path.clone() + "/Router.bin-runtime",
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// This cannot be handled with the sol! macro. The Router requires an import
|
||||
// https://github.com/alloy-rs/core/issues/602
|
||||
@@ -44,6 +55,16 @@ fn main() {
|
||||
&(artifacts_path.clone() + "/router.rs"),
|
||||
);
|
||||
|
||||
let test_artifacts_path = artifacts_path + "/tests";
|
||||
if !fs::exists(&test_artifacts_path).unwrap() {
|
||||
fs::create_dir(&test_artifacts_path).unwrap();
|
||||
}
|
||||
|
||||
// Build the test contracts
|
||||
build_solidity_contracts::build(&[], "contracts/tests", &(artifacts_path + "/tests")).unwrap();
|
||||
build_solidity_contracts::build(
|
||||
&["../../../networks/ethereum/schnorr/contracts", "../erc20/contracts", "contracts"],
|
||||
"contracts/tests",
|
||||
&test_artifacts_path,
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
@@ -22,6 +22,15 @@ import "IRouter.sol";
|
||||
The `execute` function pays a relayer, as expected for use in the account-abstraction model. Other
|
||||
functions also expect relayers, yet do not explicitly pay fees. Those calls are expected to be
|
||||
justified via the backpressure of transactions with fees.
|
||||
|
||||
We do transfer ERC20s to contracts before their successful deployment. The usage of CREATE should
|
||||
prevent deployment failures premised on address collisions, leaving failures to be failures with
|
||||
the user-provided code/gas limit. Those failures are deemed to be the user's fault. Alternative
|
||||
designs not only have increased overhead yet their own concerns around complexity (the Router
|
||||
calling itself via msg.sender), justifying this as acceptable.
|
||||
|
||||
Historically, the call-stack-depth limit would've made this design untenable. Due to EIP-150, even
|
||||
with 1 billion gas transactions, the call-stack-depth limit remains unreachable.
|
||||
*/
|
||||
// slither-disable-start low-level-calls,unchecked-lowlevel
|
||||
|
||||
@@ -37,7 +46,7 @@ contract Router is IRouterWithoutCollisions {
|
||||
|
||||
/**
|
||||
* @dev The next nonce used to determine the address of contracts deployed with CREATE. This is
|
||||
* used to predict the addresses of deployed contracts ahead of time.
|
||||
* used to predict the addresses of deployed contracts ahead of time.
|
||||
*/
|
||||
/*
|
||||
We don't expose a getter for this as it shouldn't be expected to have any specific value at a
|
||||
@@ -48,19 +57,19 @@ contract Router is IRouterWithoutCollisions {
|
||||
|
||||
/**
|
||||
* @dev The nonce to verify the next signature with, incremented upon an action to prevent
|
||||
* replays/out-of-order execution
|
||||
* replays/out-of-order execution
|
||||
*/
|
||||
uint256 private _nextNonce;
|
||||
|
||||
/**
|
||||
* @dev The next public key for Serai's Ethereum validator set, in the form the Schnorr library
|
||||
* expects
|
||||
* expects
|
||||
*/
|
||||
bytes32 private _nextSeraiKey;
|
||||
|
||||
/**
|
||||
* @dev The current public key for Serai's Ethereum validator set, in the form the Schnorr library
|
||||
* expects
|
||||
* expects
|
||||
*/
|
||||
bytes32 private _seraiKey;
|
||||
|
||||
@@ -84,6 +93,7 @@ contract Router is IRouterWithoutCollisions {
|
||||
|
||||
// Clear the re-entrancy guard to allow multiple transactions to non-re-entrant functions within
|
||||
// a transaction
|
||||
// slither-disable-next-line assembly
|
||||
assembly {
|
||||
tstore(reentrancyGuardSlot, 0)
|
||||
}
|
||||
@@ -121,10 +131,9 @@ contract Router is IRouterWithoutCollisions {
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev
|
||||
* Verify a signature of the calldata, placed immediately after the function selector. The
|
||||
* calldata should be signed with the nonce taking the place of the signature's commitment to
|
||||
* its nonce, and the signature solution zeroed.
|
||||
* @dev Verify a signature of the calldata, placed immediately after the function selector. The
|
||||
* calldata should be signed with the nonce taking the place of the signature's commitment to
|
||||
* its nonce, and the signature solution zeroed.
|
||||
*/
|
||||
function verifySignature(bytes32 key)
|
||||
private
|
||||
@@ -163,8 +172,8 @@ contract Router is IRouterWithoutCollisions {
|
||||
bytes32 signatureC;
|
||||
bytes32 signatureS;
|
||||
|
||||
// slither-disable-next-line assembly
|
||||
uint256 chainID = block.chainid;
|
||||
// slither-disable-next-line assembly
|
||||
assembly {
|
||||
// Read the signature (placed after the function signature)
|
||||
signatureC := mload(add(message, 36))
|
||||
@@ -227,9 +236,9 @@ contract Router is IRouterWithoutCollisions {
|
||||
/// @notice Start updating the key representing Serai's Ethereum validators
|
||||
/**
|
||||
* @dev This does not validate the passed-in key as much as possible. This is accepted as the key
|
||||
* won't actually be rotated to until it provides a signature confirming the update however
|
||||
* (proving signatures can be made by the key in question and verified via our Schnorr
|
||||
* contract).
|
||||
* won't actually be rotated to until it provides a signature confirming the update however
|
||||
* (proving signatures can be made by the key in question and verified via our Schnorr
|
||||
* contract).
|
||||
*
|
||||
* The hex bytes are to cause a collision with `IRouter.updateSeraiKey`.
|
||||
*/
|
||||
@@ -263,7 +272,7 @@ contract Router is IRouterWithoutCollisions {
|
||||
/// @param amount The amount to transfer in (msg.value if Ether)
|
||||
/**
|
||||
* @param instruction The Shorthand-encoded InInstruction for Serai to associate with this
|
||||
* transfer in
|
||||
* transfer in
|
||||
*/
|
||||
// Re-entrancy doesn't bork this function
|
||||
// slither-disable-next-line reentrancy-events
|
||||
@@ -326,7 +335,7 @@ contract Router is IRouterWithoutCollisions {
|
||||
/// @param amount The amount of the coin to transfer
|
||||
/**
|
||||
* @return success If the coins were successfully transferred out. This is defined as if the
|
||||
* call succeeded and returned true or nothing.
|
||||
* call succeeded and returned true or nothing.
|
||||
*/
|
||||
// execute has this annotation yet this still flags (even when it doesn't have its own loop)
|
||||
// slither-disable-next-line calls-loop
|
||||
@@ -376,7 +385,7 @@ contract Router is IRouterWithoutCollisions {
|
||||
/// @param amount The amount of the coin to transfer
|
||||
/**
|
||||
* @return success If the coins were successfully transferred out. For Ethereum, this is if the
|
||||
* call succeeded. For the ERC20, it's if the call succeeded and returned true or nothing.
|
||||
* call succeeded. For the ERC20, it's if the call succeeded and returned true or nothing.
|
||||
*/
|
||||
function transferOut(address to, address coin, uint256 amount) private returns (bool success) {
|
||||
if (coin == address(0)) {
|
||||
@@ -402,11 +411,82 @@ contract Router is IRouterWithoutCollisions {
|
||||
}
|
||||
}
|
||||
|
||||
/// @notice The header for an address, when encoded with RLP for the purposes of CREATE
|
||||
/// @dev 0x80 + 20, shifted left 30 bytes
|
||||
uint256 constant ADDRESS_HEADER = (0x80 + 20) << (30 * 8);
|
||||
|
||||
/// @notice Calculate the next address which will be deployed to by CREATE
|
||||
/**
|
||||
* @dev While CREATE2 is preferable inside smart contracts, CREATE2 is fundamentally vulnerable to
|
||||
* collisions. Our usage of CREATE forces an incremental nonce infeasible to brute force. While
|
||||
* addresses are still variable to the Router address, the Router address itself is the product
|
||||
* of an incremental nonce (the Deployer's). The Deployer's address is constant (generated via
|
||||
* NUMS methods), finally ensuring the security of this.
|
||||
*
|
||||
* This is written to be constant-gas, allowing state-independent gas prediction.
|
||||
*
|
||||
* This has undefined behavior when `nonce` is zero (EIP-161 makes this irrelevant).
|
||||
*/
|
||||
function createAddress(uint256 nonce) internal view returns (address) {
|
||||
unchecked {
|
||||
// The amount of bytes needed to represent the nonce
|
||||
uint256 bitsNeeded = 0;
|
||||
for (uint256 bits = 0; bits <= 64; bits += 8) {
|
||||
bool valueFits = nonce < (uint256(1) << bits);
|
||||
bool notPriorSet = bitsNeeded == 0;
|
||||
// If the value fits, and the bits weren't prior set, we should set the bits now
|
||||
uint256 shouldSet;
|
||||
// slither-disable-next-line assembly
|
||||
assembly {
|
||||
shouldSet := and(valueFits, notPriorSet)
|
||||
}
|
||||
// Carry the existing bitsNeeded value, set bits if should set
|
||||
bitsNeeded = bitsNeeded + (shouldSet * bits);
|
||||
}
|
||||
uint256 bytesNeeded = bitsNeeded / 8;
|
||||
|
||||
// if the nonce is an RLP string or not
|
||||
bool nonceIsNotStringBool = nonce <= 0x7f;
|
||||
uint256 nonceIsNotString;
|
||||
// slither-disable-next-line assembly
|
||||
assembly {
|
||||
nonceIsNotString := nonceIsNotStringBool
|
||||
}
|
||||
uint256 nonceIsString = nonceIsNotString ^ 1;
|
||||
|
||||
// Define the RLP length
|
||||
uint256 rlpEncodingLen = 23 + (nonceIsString * bytesNeeded);
|
||||
|
||||
uint256 rlpEncoding =
|
||||
// The header, which does not include itself in its length, shifted into the first byte
|
||||
((0xc0 + (rlpEncodingLen - 1)) << 248)
|
||||
// The address header, which is constant
|
||||
| ADDRESS_HEADER
|
||||
// Shift the address from bytes 12 .. 32 to 2 .. 22
|
||||
| (uint256(uint160(address(this))) << 80)
|
||||
// Shift the nonce (one byte) or the nonce's header from byte 31 to byte 22
|
||||
| (((nonceIsNotString * nonce) + (nonceIsString * (0x80 + bytesNeeded))) << 72)
|
||||
// Shift past the unnecessary bytes
|
||||
| (nonce * nonceIsString) << (72 - bitsNeeded);
|
||||
|
||||
// Store this to the scratch space
|
||||
bytes memory rlp;
|
||||
// slither-disable-next-line assembly
|
||||
assembly {
|
||||
mstore(0, rlpEncodingLen)
|
||||
mstore(32, rlpEncoding)
|
||||
rlp := 0
|
||||
}
|
||||
|
||||
return address(uint160(uint256(keccak256(rlp))));
|
||||
}
|
||||
}
|
||||
|
||||
/// @notice Execute some arbitrary code within a secure sandbox
|
||||
/**
|
||||
* @dev This performs sandboxing by deploying this code with `CREATE`. This is an external
|
||||
* function as we can't meter `CREATE`/internal functions. We work around this by calling this
|
||||
* function with `CALL` (which we can meter). This does forward `msg.value` to the newly
|
||||
* function as we can't meter `CREATE`/internal functions. We work around this by calling this
|
||||
* function with `CALL` (which we can meter). This does forward `msg.value` to the newly
|
||||
* deployed contract.
|
||||
*/
|
||||
/// @param code The code to execute
|
||||
@@ -435,6 +515,8 @@ contract Router is IRouterWithoutCollisions {
|
||||
* of CEI with `verifySignature` prevents replays, re-entrancy would allow out-of-order
|
||||
* completion for the execution of batches (despite their in-order start of execution) which
|
||||
* isn't a headache worth dealing with.
|
||||
*
|
||||
* Re-entrancy is also explicitly required due to how `_smartContractNonce` is handled.
|
||||
*/
|
||||
// @param signature The signature by the current key for Serai's Ethereum validators
|
||||
// @param coin The coin all of these `OutInstruction`s are for
|
||||
@@ -473,12 +555,12 @@ contract Router is IRouterWithoutCollisions {
|
||||
/*
|
||||
If it's an ERC20, we calculate the address of the will-be contract and transfer to it
|
||||
before deployment. This avoids needing to deploy the contract, then call transfer, then
|
||||
call the contract again
|
||||
*/
|
||||
address nextAddress = address(
|
||||
uint160(uint256(keccak256(abi.encodePacked(address(this), _smartContractNonce))))
|
||||
);
|
||||
call the contract again.
|
||||
|
||||
We use CREATE, not CREATE2, despite the difficulty in calculating the address
|
||||
in-contract, for reasons explained within `createAddress`'s documentation.
|
||||
*/
|
||||
address nextAddress = createAddress(_smartContractNonce);
|
||||
success = erc20TransferOut(nextAddress, coin, outs[i].amount);
|
||||
}
|
||||
|
||||
|
||||
13
processor/ethereum/router/contracts/tests/CreateAddress.sol
Normal file
13
processor/ethereum/router/contracts/tests/CreateAddress.sol
Normal file
@@ -0,0 +1,13 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
pragma solidity ^0.8.26;
|
||||
|
||||
import "Router.sol";
|
||||
|
||||
// Wrap the Router with a contract which exposes the address
|
||||
contract CreateAddress is Router {
|
||||
constructor() Router(bytes32(uint256(1))) { }
|
||||
|
||||
function createAddressForSelf(uint256 nonce) external returns (address) {
|
||||
return Router.createAddress(nonce);
|
||||
}
|
||||
}
|
||||
@@ -23,6 +23,7 @@ const CHAIN_ID: U256 = U256::from_be_slice(&[1]);
|
||||
pub(crate) type GasEstimator = Evm<'static, (), InMemoryDB>;
|
||||
|
||||
impl Router {
|
||||
const SMART_CONTRACT_NONCE_STORAGE_SLOT: U256 = U256::from_be_slice(&[0]);
|
||||
const NONCE_STORAGE_SLOT: U256 = U256::from_be_slice(&[1]);
|
||||
const SERAI_KEY_STORAGE_SLOT: U256 = U256::from_be_slice(&[3]);
|
||||
|
||||
@@ -89,6 +90,15 @@ impl Router {
|
||||
},
|
||||
);
|
||||
|
||||
// Insert the value for _smartContractNonce set in the constructor
|
||||
// All operations w.r.t. execute in constant-time, making the actual value irrelevant
|
||||
db.insert_account_storage(
|
||||
self.address,
|
||||
Self::SMART_CONTRACT_NONCE_STORAGE_SLOT,
|
||||
U256::from(1),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// Insert a non-zero nonce, as the zero nonce will update to the initial key and never be
|
||||
// used for any gas estimations of `execute`, the only function estimated
|
||||
db.insert_account_storage(self.address, Self::NONCE_STORAGE_SLOT, U256::from(1)).unwrap();
|
||||
|
||||
85
processor/ethereum/router/src/tests/create_address.rs
Normal file
85
processor/ethereum/router/src/tests/create_address.rs
Normal file
@@ -0,0 +1,85 @@
|
||||
use alloy_core::primitives::{hex, U256, Bytes, TxKind};
|
||||
use alloy_sol_types::SolCall;
|
||||
|
||||
use alloy_consensus::TxLegacy;
|
||||
|
||||
use alloy_rpc_types_eth::{TransactionInput, TransactionRequest};
|
||||
use alloy_provider::Provider;
|
||||
|
||||
use revm::{primitives::SpecId, interpreter::gas::calculate_initial_tx_gas};
|
||||
|
||||
use crate::tests::Test;
|
||||
|
||||
#[rustfmt::skip]
|
||||
#[expect(warnings)]
|
||||
#[expect(needless_pass_by_value)]
|
||||
#[expect(clippy::all)]
|
||||
#[expect(clippy::ignored_unit_patterns)]
|
||||
#[expect(clippy::redundant_closure_for_method_calls)]
|
||||
mod abi {
|
||||
alloy_sol_macro::sol!("contracts/tests/CreateAddress.sol");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_create_address() {
|
||||
let test = Test::new().await;
|
||||
|
||||
let address = {
|
||||
const BYTECODE: &[u8] = {
|
||||
const BYTECODE_HEX: &[u8] = include_bytes!(concat!(
|
||||
env!("OUT_DIR"),
|
||||
"/serai-processor-ethereum-router/tests/CreateAddress.bin"
|
||||
));
|
||||
const BYTECODE: [u8; BYTECODE_HEX.len() / 2] =
|
||||
match hex::const_decode_to_array::<{ BYTECODE_HEX.len() / 2 }>(BYTECODE_HEX) {
|
||||
Ok(bytecode) => bytecode,
|
||||
Err(_) => panic!("CreateAddress.bin did not contain valid hex"),
|
||||
};
|
||||
&BYTECODE
|
||||
};
|
||||
|
||||
let tx = TxLegacy {
|
||||
chain_id: None,
|
||||
nonce: 0,
|
||||
gas_price: 100_000_000_000u128,
|
||||
gas_limit: 1_100_000,
|
||||
to: TxKind::Create,
|
||||
value: U256::ZERO,
|
||||
input: Bytes::from_static(BYTECODE),
|
||||
};
|
||||
let tx = ethereum_primitives::deterministically_sign(tx);
|
||||
let receipt = ethereum_test_primitives::publish_tx(&test.provider, tx).await;
|
||||
receipt.contract_address.unwrap()
|
||||
};
|
||||
|
||||
// Check `createAddress` correctly encodes the nonce for every single meaningful bit pattern
|
||||
// The only meaningful patterns are < 0x80, == 0x80, and then each length greater > 0x80
|
||||
// The following covers all three
|
||||
let mut nonce = 1u64;
|
||||
let mut gas = None;
|
||||
while nonce.checked_add(nonce).is_some() {
|
||||
let input =
|
||||
(abi::CreateAddress::createAddressForSelfCall { nonce: U256::from(nonce) }).abi_encode();
|
||||
|
||||
// Make sure the function works as expected
|
||||
let call =
|
||||
TransactionRequest::default().to(address).input(TransactionInput::new(input.clone().into()));
|
||||
assert_eq!(
|
||||
&test.provider.call(&call).await.unwrap().as_ref()[12 ..],
|
||||
address.create(nonce).as_slice(),
|
||||
);
|
||||
|
||||
// Check the function is constant-gas
|
||||
let gas_used = test.provider.estimate_gas(&call).await.unwrap();
|
||||
let initial_gas = calculate_initial_tx_gas(SpecId::CANCUN, &input, false, &[], 0).initial_gas;
|
||||
let this_call = gas_used - initial_gas;
|
||||
if gas.is_none() {
|
||||
gas = Some(this_call);
|
||||
}
|
||||
assert_eq!(gas, Some(this_call));
|
||||
|
||||
nonce <<= 1;
|
||||
}
|
||||
|
||||
println!("createAddress gas: {}", gas.unwrap());
|
||||
}
|
||||
@@ -41,6 +41,9 @@ use crate::{
|
||||
};
|
||||
|
||||
mod constants;
|
||||
|
||||
mod create_address;
|
||||
|
||||
mod erc20;
|
||||
use erc20::Erc20;
|
||||
|
||||
@@ -465,6 +468,8 @@ impl Test {
|
||||
self.provider.debug_trace_transaction(*tx.hash(), Default::default()).await.unwrap();
|
||||
let refund =
|
||||
trace.try_into_default_frame().unwrap().struct_logs.last().unwrap().refund_counter;
|
||||
// This isn't capped to 1/5th of the TX's gas usage yet that's fine as none of our tests are
|
||||
// so refund intensive
|
||||
unused_gas += refund.unwrap_or(0)
|
||||
}
|
||||
|
||||
@@ -698,53 +703,56 @@ async fn test_erc20_top_level_transfer_in_instruction() {
|
||||
test.publish_in_instruction_tx(tx, coin, amount, &shorthand).await;
|
||||
}
|
||||
|
||||
// Code which returns true
|
||||
#[rustfmt::skip]
|
||||
fn return_true_code() -> Vec<u8> {
|
||||
vec![
|
||||
0x60, // push 1 byte | 3 gas
|
||||
0x01, // the value 1
|
||||
0x5f, // push 0 | 2 gas
|
||||
0x52, // mstore to offset 0 the value 1 | 3 gas
|
||||
0x60, // push 1 byte | 3 gas
|
||||
0x20, // the value 32
|
||||
0x5f, // push 0 | 2 gas
|
||||
0xf3, // return from offset 0 1 word | 0 gas
|
||||
// 13 gas for the execution plus a single word of memory for 16 gas total
|
||||
]
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_empty_execute() {
|
||||
let mut test = Test::new().await;
|
||||
test.confirm_next_serai_key().await;
|
||||
|
||||
{
|
||||
let gas = test.router.execute_gas(Coin::Ether, U256::from(1), &[].as_slice().into());
|
||||
let fee = U256::from(gas);
|
||||
|
||||
let () = test
|
||||
.provider
|
||||
.raw_request("anvil_setBalance".into(), (test.router.address(), 100_000))
|
||||
.raw_request("anvil_setBalance".into(), (test.router.address(), fee))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let gas = test.router.execute_gas(Coin::Ether, U256::from(1), &[].as_slice().into());
|
||||
let fee = U256::from(gas);
|
||||
let (tx, gas_used) = test.execute(Coin::Ether, fee, [].as_slice().into(), vec![]).await;
|
||||
// We don't use the call gas stipend here
|
||||
const UNUSED_GAS: u64 = revm::interpreter::gas::CALL_STIPEND;
|
||||
assert_eq!(gas_used + UNUSED_GAS, gas);
|
||||
|
||||
assert_eq!(
|
||||
test.provider.get_balance(test.router.address()).await.unwrap(),
|
||||
U256::from(100_000 - gas)
|
||||
);
|
||||
assert_eq!(test.provider.get_balance(test.router.address()).await.unwrap(), U256::from(0));
|
||||
let minted_to_sender = u128::from(tx.tx().gas_limit) * tx.tx().gas_price;
|
||||
let spent_by_sender = u128::from(gas_used) * tx.tx().gas_price;
|
||||
assert_eq!(
|
||||
test.provider.get_balance(tx.recover_signer().unwrap()).await.unwrap() -
|
||||
U256::from(minted_to_sender - spent_by_sender),
|
||||
U256::from(gas)
|
||||
U256::from(fee)
|
||||
);
|
||||
}
|
||||
|
||||
{
|
||||
let token = Address::from([0xff; 20]);
|
||||
{
|
||||
#[rustfmt::skip]
|
||||
let code = vec![
|
||||
0x60, // push 1 byte | 3 gas
|
||||
0x01, // the value 1
|
||||
0x5f, // push 0 | 2 gas
|
||||
0x52, // mstore to offset 0 the value 1 | 3 gas
|
||||
0x60, // push 1 byte | 3 gas
|
||||
0x20, // the value 32
|
||||
0x5f, // push 0 | 2 gas
|
||||
0xf3, // return from offset 0 1 word | 0 gas
|
||||
// 13 gas for the execution plus a single word of memory for 16 gas total
|
||||
];
|
||||
let code = return_true_code();
|
||||
// Deploy our 'token'
|
||||
let () = test.provider.raw_request("anvil_setCode".into(), (token, code)).await.unwrap();
|
||||
let call =
|
||||
@@ -754,7 +762,7 @@ async fn test_empty_execute() {
|
||||
test.provider.call(&call).await.unwrap().as_ref(),
|
||||
U256::from(1).abi_encode().as_slice()
|
||||
);
|
||||
// Check it has the expected gas cost
|
||||
// Check it has the expected gas cost (16 is documented in `return_true_code`)
|
||||
assert_eq!(test.provider.estimate_gas(&call).await.unwrap(), 21_000 + 16);
|
||||
}
|
||||
|
||||
@@ -773,11 +781,6 @@ async fn test_empty_execute() {
|
||||
async fn test_eth_address_out_instruction() {
|
||||
let mut test = Test::new().await;
|
||||
test.confirm_next_serai_key().await;
|
||||
let () = test
|
||||
.provider
|
||||
.raw_request("anvil_setBalance".into(), (test.router.address(), 100_000))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let mut rand_address = [0xff; 20];
|
||||
OsRng.fill_bytes(&mut rand_address);
|
||||
@@ -787,14 +790,18 @@ async fn test_eth_address_out_instruction() {
|
||||
|
||||
let gas = test.router.execute_gas(Coin::Ether, U256::from(1), &out_instructions);
|
||||
let fee = U256::from(gas);
|
||||
|
||||
let () = test
|
||||
.provider
|
||||
.raw_request("anvil_setBalance".into(), (test.router.address(), amount_out + fee))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let (tx, gas_used) = test.execute(Coin::Ether, fee, out_instructions, vec![true]).await;
|
||||
const UNUSED_GAS: u64 = 2 * revm::interpreter::gas::CALL_STIPEND;
|
||||
assert_eq!(gas_used + UNUSED_GAS, gas);
|
||||
|
||||
assert_eq!(
|
||||
test.provider.get_balance(test.router.address()).await.unwrap(),
|
||||
U256::from(100_000) - amount_out - fee
|
||||
);
|
||||
assert_eq!(test.provider.get_balance(test.router.address()).await.unwrap(), U256::from(0));
|
||||
let minted_to_sender = u128::from(tx.tx().gas_limit) * tx.tx().gas_price;
|
||||
let spent_by_sender = u128::from(gas_used) * tx.tx().gas_price;
|
||||
assert_eq!(
|
||||
@@ -845,12 +852,11 @@ async fn test_eth_code_out_instruction() {
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let mut rand_address = [0xff; 20];
|
||||
OsRng.fill_bytes(&mut rand_address);
|
||||
let code = return_true_code();
|
||||
let amount_out = U256::from(2);
|
||||
let out_instructions = OutInstructions::from(
|
||||
[(
|
||||
SeraiEthereumAddress::Contract(ContractDeployment::new(50_000, vec![]).unwrap()),
|
||||
SeraiEthereumAddress::Contract(ContractDeployment::new(50_000, code.clone()).unwrap()),
|
||||
amount_out,
|
||||
)]
|
||||
.as_slice(),
|
||||
@@ -876,12 +882,43 @@ async fn test_eth_code_out_instruction() {
|
||||
U256::from(minted_to_sender - spent_by_sender),
|
||||
U256::from(fee)
|
||||
);
|
||||
assert_eq!(test.provider.get_balance(test.router.address().create(1)).await.unwrap(), amount_out);
|
||||
let deployed = test.router.address().create(1);
|
||||
assert_eq!(test.provider.get_balance(deployed).await.unwrap(), amount_out);
|
||||
// The init code we use returns true, which will become the deployed contract's code
|
||||
assert_eq!(test.provider.get_code_at(deployed).await.unwrap().to_vec(), true.abi_encode());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_erc20_code_out_instruction() {
|
||||
todo!("TODO")
|
||||
let mut test = Test::new().await;
|
||||
test.confirm_next_serai_key().await;
|
||||
|
||||
let erc20 = Erc20::deploy(&test).await;
|
||||
let coin = Coin::Erc20(erc20.address());
|
||||
|
||||
let code = return_true_code();
|
||||
let amount_out = U256::from(2);
|
||||
let out_instructions = OutInstructions::from(
|
||||
[(SeraiEthereumAddress::Contract(ContractDeployment::new(50_000, code).unwrap()), amount_out)]
|
||||
.as_slice(),
|
||||
);
|
||||
|
||||
let gas = test.router.execute_gas(coin, U256::from(1), &out_instructions);
|
||||
let fee = U256::from(gas);
|
||||
|
||||
// Mint to the Router the necessary amount of the ERC20
|
||||
erc20.mint(&test, test.router.address(), amount_out + fee).await;
|
||||
|
||||
let (tx, gas_used) = test.execute(coin, fee, out_instructions, vec![true]).await;
|
||||
|
||||
let unused_gas = test.gas_unused_by_calls(&tx).await;
|
||||
assert_eq!(gas_used + unused_gas, gas);
|
||||
|
||||
assert_eq!(erc20.balance_of(&test, test.router.address()).await, U256::from(0));
|
||||
assert_eq!(erc20.balance_of(&test, tx.recover_signer().unwrap()).await, U256::from(fee));
|
||||
let deployed = test.router.address().create(1);
|
||||
assert_eq!(erc20.balance_of(&test, deployed).await, amount_out);
|
||||
assert_eq!(test.provider.get_code_at(deployed).await.unwrap().to_vec(), true.abi_encode());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -1006,68 +1043,5 @@ async fn test_escape_hatch() {
|
||||
error Reentered();
|
||||
error EscapeFailed();
|
||||
function executeArbitraryCode(bytes memory code) external payable;
|
||||
enum DestinationType {
|
||||
Address,
|
||||
Code
|
||||
}
|
||||
struct CodeDestination {
|
||||
uint32 gasLimit;
|
||||
bytes code;
|
||||
}
|
||||
struct OutInstruction {
|
||||
DestinationType destinationType;
|
||||
bytes destination;
|
||||
uint256 amount;
|
||||
}
|
||||
function execute(
|
||||
Signature calldata signature,
|
||||
address coin,
|
||||
uint256 fee,
|
||||
OutInstruction[] calldata outs
|
||||
) external;
|
||||
}
|
||||
|
||||
async fn publish_outs(
|
||||
provider: &RootProvider<SimpleRequest>,
|
||||
router: &Router,
|
||||
key: (Scalar, PublicKey),
|
||||
nonce: u64,
|
||||
coin: Coin,
|
||||
fee: U256,
|
||||
outs: OutInstructions,
|
||||
) -> TransactionReceipt {
|
||||
let msg = Router::execute_message(nonce, coin, fee, outs.clone());
|
||||
|
||||
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.execute(coin, fee, outs, &sig);
|
||||
tx.gas_price = 100_000_000_000;
|
||||
let tx = ethereum_primitives::deterministically_sign(tx);
|
||||
ethereum_test_primitives::publish_tx(provider, tx).await
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_eth_address_out_instruction() {
|
||||
let (_anvil, provider, router, key) = setup_test().await;
|
||||
confirm_next_serai_key(&provider, &router, 1, key).await;
|
||||
|
||||
let mut amount = U256::try_from(OsRng.next_u64()).unwrap();
|
||||
let mut fee = U256::try_from(OsRng.next_u64()).unwrap();
|
||||
if fee > amount {
|
||||
core::mem::swap(&mut amount, &mut fee);
|
||||
}
|
||||
assert!(amount >= fee);
|
||||
ethereum_test_primitives::fund_account(&provider, router.address(), amount).await;
|
||||
|
||||
let instructions = OutInstructions::from([].as_slice());
|
||||
let receipt = publish_outs(&provider, &router, key, 2, Coin::Ether, fee, instructions).await;
|
||||
assert!(receipt.status());
|
||||
assert_eq!(Router::EXECUTE_ETH_BASE_GAS, ((receipt.gas_used + 1000) / 1000) * 1000);
|
||||
|
||||
assert_eq!(router.next_nonce(receipt.block_hash.unwrap().into()).await.unwrap(), 3);
|
||||
}
|
||||
function createAddress(uint256 nonce) private view returns (address);
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user