mirror of
https://github.com/serai-dex/serai.git
synced 2025-12-15 07:29:25 +00:00
Compare commits
5 Commits
e742a6b0ec
...
f8c3acae7b
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f8c3acae7b | ||
|
|
0957460f27 | ||
|
|
ea00ba9ff8 | ||
|
|
a9625364df | ||
|
|
75c6427d7c |
@@ -32,6 +32,17 @@ fn main() {
|
|||||||
&artifacts_path,
|
&artifacts_path,
|
||||||
)
|
)
|
||||||
.unwrap();
|
.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
|
// This cannot be handled with the sol! macro. The Router requires an import
|
||||||
// https://github.com/alloy-rs/core/issues/602
|
// https://github.com/alloy-rs/core/issues/602
|
||||||
@@ -44,6 +55,16 @@ fn main() {
|
|||||||
&(artifacts_path.clone() + "/router.rs"),
|
&(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 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
|
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
|
functions also expect relayers, yet do not explicitly pay fees. Those calls are expected to be
|
||||||
justified via the backpressure of transactions with fees.
|
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
|
// 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
|
* @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
|
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
|
* @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;
|
uint256 private _nextNonce;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @dev The next public key for Serai's Ethereum validator set, in the form the Schnorr library
|
* @dev The next public key for Serai's Ethereum validator set, in the form the Schnorr library
|
||||||
* expects
|
* expects
|
||||||
*/
|
*/
|
||||||
bytes32 private _nextSeraiKey;
|
bytes32 private _nextSeraiKey;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @dev The current public key for Serai's Ethereum validator set, in the form the Schnorr library
|
* @dev The current public key for Serai's Ethereum validator set, in the form the Schnorr library
|
||||||
* expects
|
* expects
|
||||||
*/
|
*/
|
||||||
bytes32 private _seraiKey;
|
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
|
// Clear the re-entrancy guard to allow multiple transactions to non-re-entrant functions within
|
||||||
// a transaction
|
// a transaction
|
||||||
|
// slither-disable-next-line assembly
|
||||||
assembly {
|
assembly {
|
||||||
tstore(reentrancyGuardSlot, 0)
|
tstore(reentrancyGuardSlot, 0)
|
||||||
}
|
}
|
||||||
@@ -121,10 +131,9 @@ contract Router is IRouterWithoutCollisions {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @dev
|
* @dev Verify a signature of the calldata, placed immediately after the function selector. The
|
||||||
* 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
|
||||||
* calldata should be signed with the nonce taking the place of the signature's commitment to
|
* its nonce, and the signature solution zeroed.
|
||||||
* its nonce, and the signature solution zeroed.
|
|
||||||
*/
|
*/
|
||||||
function verifySignature(bytes32 key)
|
function verifySignature(bytes32 key)
|
||||||
private
|
private
|
||||||
@@ -163,8 +172,8 @@ contract Router is IRouterWithoutCollisions {
|
|||||||
bytes32 signatureC;
|
bytes32 signatureC;
|
||||||
bytes32 signatureS;
|
bytes32 signatureS;
|
||||||
|
|
||||||
// slither-disable-next-line assembly
|
|
||||||
uint256 chainID = block.chainid;
|
uint256 chainID = block.chainid;
|
||||||
|
// slither-disable-next-line assembly
|
||||||
assembly {
|
assembly {
|
||||||
// Read the signature (placed after the function signature)
|
// Read the signature (placed after the function signature)
|
||||||
signatureC := mload(add(message, 36))
|
signatureC := mload(add(message, 36))
|
||||||
@@ -227,9 +236,9 @@ contract Router is IRouterWithoutCollisions {
|
|||||||
/// @notice Start updating the key representing Serai's Ethereum validators
|
/// @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
|
* @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
|
* 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
|
* (proving signatures can be made by the key in question and verified via our Schnorr
|
||||||
* contract).
|
* contract).
|
||||||
*
|
*
|
||||||
* The hex bytes are to cause a collision with `IRouter.updateSeraiKey`.
|
* 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 amount The amount to transfer in (msg.value if Ether)
|
||||||
/**
|
/**
|
||||||
* @param instruction The Shorthand-encoded InInstruction for Serai to associate with this
|
* @param instruction The Shorthand-encoded InInstruction for Serai to associate with this
|
||||||
* transfer in
|
* transfer in
|
||||||
*/
|
*/
|
||||||
// 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
|
||||||
@@ -326,7 +335,7 @@ contract Router is IRouterWithoutCollisions {
|
|||||||
/// @param amount The amount of the coin to transfer
|
/// @param amount The amount of the coin to transfer
|
||||||
/**
|
/**
|
||||||
* @return success If the coins were successfully transferred out. This is defined as if the
|
* @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)
|
// execute has this annotation yet this still flags (even when it doesn't have its own loop)
|
||||||
// slither-disable-next-line calls-loop
|
// slither-disable-next-line calls-loop
|
||||||
@@ -376,7 +385,7 @@ contract Router is IRouterWithoutCollisions {
|
|||||||
/// @param amount The amount of the coin to transfer
|
/// @param amount The amount of the coin to transfer
|
||||||
/**
|
/**
|
||||||
* @return success If the coins were successfully transferred out. For Ethereum, this is if the
|
* @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) {
|
function transferOut(address to, address coin, uint256 amount) private returns (bool success) {
|
||||||
if (coin == address(0)) {
|
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
|
/// @notice Execute some arbitrary code within a secure sandbox
|
||||||
/**
|
/**
|
||||||
* @dev This performs sandboxing by deploying this code with `CREATE`. This is an external
|
* @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 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 with `CALL` (which we can meter). This does forward `msg.value` to the newly
|
||||||
* deployed contract.
|
* deployed contract.
|
||||||
*/
|
*/
|
||||||
/// @param code The code to execute
|
/// @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
|
* 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
|
* completion for the execution of batches (despite their in-order start of execution) which
|
||||||
* isn't a headache worth dealing with.
|
* 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 signature The signature by the current key for Serai's Ethereum validators
|
||||||
// @param coin The coin all of these `OutInstruction`s are for
|
// @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
|
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
|
before deployment. This avoids needing to deploy the contract, then call transfer, then
|
||||||
call the contract again
|
call the contract again.
|
||||||
*/
|
|
||||||
address nextAddress = address(
|
|
||||||
uint160(uint256(keccak256(abi.encodePacked(address(this), _smartContractNonce))))
|
|
||||||
);
|
|
||||||
|
|
||||||
|
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);
|
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>;
|
pub(crate) type GasEstimator = Evm<'static, (), InMemoryDB>;
|
||||||
|
|
||||||
impl Router {
|
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 NONCE_STORAGE_SLOT: U256 = U256::from_be_slice(&[1]);
|
||||||
const SERAI_KEY_STORAGE_SLOT: U256 = U256::from_be_slice(&[3]);
|
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
|
// 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
|
// 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();
|
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 constants;
|
||||||
|
|
||||||
|
mod create_address;
|
||||||
|
|
||||||
mod erc20;
|
mod erc20;
|
||||||
use erc20::Erc20;
|
use erc20::Erc20;
|
||||||
|
|
||||||
@@ -465,6 +468,8 @@ impl Test {
|
|||||||
self.provider.debug_trace_transaction(*tx.hash(), Default::default()).await.unwrap();
|
self.provider.debug_trace_transaction(*tx.hash(), Default::default()).await.unwrap();
|
||||||
let refund =
|
let refund =
|
||||||
trace.try_into_default_frame().unwrap().struct_logs.last().unwrap().refund_counter;
|
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)
|
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;
|
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]
|
#[tokio::test]
|
||||||
async fn test_empty_execute() {
|
async fn test_empty_execute() {
|
||||||
let mut test = Test::new().await;
|
let mut test = Test::new().await;
|
||||||
test.confirm_next_serai_key().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
|
let () = test
|
||||||
.provider
|
.provider
|
||||||
.raw_request("anvil_setBalance".into(), (test.router.address(), 100_000))
|
.raw_request("anvil_setBalance".into(), (test.router.address(), fee))
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.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;
|
let (tx, gas_used) = test.execute(Coin::Ether, fee, [].as_slice().into(), vec![]).await;
|
||||||
// We don't use the call gas stipend here
|
// We don't use the call gas stipend here
|
||||||
const UNUSED_GAS: u64 = revm::interpreter::gas::CALL_STIPEND;
|
const UNUSED_GAS: u64 = revm::interpreter::gas::CALL_STIPEND;
|
||||||
assert_eq!(gas_used + UNUSED_GAS, gas);
|
assert_eq!(gas_used + UNUSED_GAS, gas);
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(test.provider.get_balance(test.router.address()).await.unwrap(), U256::from(0));
|
||||||
test.provider.get_balance(test.router.address()).await.unwrap(),
|
|
||||||
U256::from(100_000 - gas)
|
|
||||||
);
|
|
||||||
let minted_to_sender = u128::from(tx.tx().gas_limit) * tx.tx().gas_price;
|
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;
|
let spent_by_sender = u128::from(gas_used) * tx.tx().gas_price;
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
test.provider.get_balance(tx.recover_signer().unwrap()).await.unwrap() -
|
test.provider.get_balance(tx.recover_signer().unwrap()).await.unwrap() -
|
||||||
U256::from(minted_to_sender - spent_by_sender),
|
U256::from(minted_to_sender - spent_by_sender),
|
||||||
U256::from(gas)
|
U256::from(fee)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
let token = Address::from([0xff; 20]);
|
let token = Address::from([0xff; 20]);
|
||||||
{
|
{
|
||||||
#[rustfmt::skip]
|
let code = return_true_code();
|
||||||
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
|
|
||||||
];
|
|
||||||
// Deploy our 'token'
|
// Deploy our 'token'
|
||||||
let () = test.provider.raw_request("anvil_setCode".into(), (token, code)).await.unwrap();
|
let () = test.provider.raw_request("anvil_setCode".into(), (token, code)).await.unwrap();
|
||||||
let call =
|
let call =
|
||||||
@@ -754,7 +762,7 @@ async fn test_empty_execute() {
|
|||||||
test.provider.call(&call).await.unwrap().as_ref(),
|
test.provider.call(&call).await.unwrap().as_ref(),
|
||||||
U256::from(1).abi_encode().as_slice()
|
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);
|
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() {
|
async fn test_eth_address_out_instruction() {
|
||||||
let mut test = Test::new().await;
|
let mut test = Test::new().await;
|
||||||
test.confirm_next_serai_key().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];
|
let mut rand_address = [0xff; 20];
|
||||||
OsRng.fill_bytes(&mut rand_address);
|
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 gas = test.router.execute_gas(Coin::Ether, U256::from(1), &out_instructions);
|
||||||
let fee = U256::from(gas);
|
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;
|
let (tx, gas_used) = test.execute(Coin::Ether, fee, out_instructions, vec![true]).await;
|
||||||
const UNUSED_GAS: u64 = 2 * revm::interpreter::gas::CALL_STIPEND;
|
const UNUSED_GAS: u64 = 2 * revm::interpreter::gas::CALL_STIPEND;
|
||||||
assert_eq!(gas_used + UNUSED_GAS, gas);
|
assert_eq!(gas_used + UNUSED_GAS, gas);
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(test.provider.get_balance(test.router.address()).await.unwrap(), U256::from(0));
|
||||||
test.provider.get_balance(test.router.address()).await.unwrap(),
|
|
||||||
U256::from(100_000) - amount_out - fee
|
|
||||||
);
|
|
||||||
let minted_to_sender = u128::from(tx.tx().gas_limit) * tx.tx().gas_price;
|
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;
|
let spent_by_sender = u128::from(gas_used) * tx.tx().gas_price;
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
@@ -845,12 +852,11 @@ async fn test_eth_code_out_instruction() {
|
|||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let mut rand_address = [0xff; 20];
|
let code = return_true_code();
|
||||||
OsRng.fill_bytes(&mut rand_address);
|
|
||||||
let amount_out = U256::from(2);
|
let amount_out = U256::from(2);
|
||||||
let out_instructions = OutInstructions::from(
|
let out_instructions = OutInstructions::from(
|
||||||
[(
|
[(
|
||||||
SeraiEthereumAddress::Contract(ContractDeployment::new(50_000, vec![]).unwrap()),
|
SeraiEthereumAddress::Contract(ContractDeployment::new(50_000, code.clone()).unwrap()),
|
||||||
amount_out,
|
amount_out,
|
||||||
)]
|
)]
|
||||||
.as_slice(),
|
.as_slice(),
|
||||||
@@ -876,12 +882,43 @@ async fn test_eth_code_out_instruction() {
|
|||||||
U256::from(minted_to_sender - spent_by_sender),
|
U256::from(minted_to_sender - spent_by_sender),
|
||||||
U256::from(fee)
|
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]
|
#[tokio::test]
|
||||||
async fn test_erc20_code_out_instruction() {
|
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]
|
#[tokio::test]
|
||||||
@@ -1006,68 +1043,5 @@ async fn test_escape_hatch() {
|
|||||||
error Reentered();
|
error Reentered();
|
||||||
error EscapeFailed();
|
error EscapeFailed();
|
||||||
function executeArbitraryCode(bytes memory code) external payable;
|
function executeArbitraryCode(bytes memory code) external payable;
|
||||||
enum DestinationType {
|
function createAddress(uint256 nonce) private view returns (address);
|
||||||
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);
|
|
||||||
}
|
|
||||||
*/
|
*/
|
||||||
|
|||||||
Reference in New Issue
Block a user