Add ERC20 InInstruction test

This commit is contained in:
Luke Parker 2025-01-24 03:23:58 -05:00
parent a63a86ba79
commit 3d44766eff
No known key found for this signature in database
6 changed files with 194 additions and 21 deletions
processor/ethereum
deployer/src
router

View file

@ -52,7 +52,7 @@ impl Deployer {
/// funded for this transaction to be submitted. This account has no known private key to anyone
/// so ETH sent can be neither misappropriated nor returned.
pub fn deployment_tx() -> Signed<TxLegacy> {
let bytecode = Bytes::from(BYTECODE);
let bytecode = Bytes::from_static(BYTECODE);
// Legacy transactions are used to ensure the widest possible degree of support across EVMs
let tx = TxLegacy {

View file

@ -41,6 +41,9 @@ fn main() {
"contracts/IRouter.sol",
"contracts/Router.sol",
],
&(artifacts_path + "/router.rs"),
&(artifacts_path.clone() + "/router.rs"),
);
// Build the test contracts
build_solidity_contracts::build(&[], "contracts/tests", &(artifacts_path + "/tests")).unwrap();
}

View file

@ -17,17 +17,11 @@ contract TestERC20 {
return 18;
}
function totalSupply() public pure returns (uint256) {
return 1_000_000 * 10e18;
}
uint256 public totalSupply;
mapping(address => uint256) balances;
mapping(address => mapping(address => uint256)) allowances;
constructor() {
balances[msg.sender] = totalSupply();
}
function balanceOf(address owner) public view returns (uint256) {
return balances[owner];
}
@ -35,6 +29,7 @@ contract TestERC20 {
function transfer(address to, uint256 value) public returns (bool) {
balances[msg.sender] -= value;
balances[to] += value;
emit Transfer(msg.sender, to, value);
return true;
}
@ -42,15 +37,28 @@ contract TestERC20 {
allowances[from][msg.sender] -= value;
balances[from] -= value;
balances[to] += value;
emit Transfer(from, to, value);
return true;
}
function approve(address spender, uint256 value) public returns (bool) {
allowances[msg.sender][spender] = value;
emit Approval(msg.sender, spender, value);
return true;
}
function allowance(address owner, address spender) public view returns (uint256) {
return allowances[owner][spender];
}
function mint(address owner, uint256 value) external {
balances[owner] += value;
totalSupply += value;
emit Transfer(address(0), owner, value);
}
function magicApprove(address owner, address spender, uint256 value) external {
allowances[owner][spender] = value;
emit Approval(owner, spender, value);
}
}

View file

@ -11,10 +11,7 @@ use borsh::{BorshSerialize, BorshDeserialize};
use group::ff::PrimeField;
use alloy_core::primitives::{
hex::{self, FromHex},
Address, U256, Bytes, TxKind,
};
use alloy_core::primitives::{hex, Address, U256, TxKind};
use alloy_sol_types::{SolValue, SolConstructor, SolCall, SolEvent};
use alloy_consensus::TxLegacy;
@ -257,9 +254,18 @@ impl Router {
const ESCAPE_HATCH_GAS: u64 = 61_238;
fn code() -> Vec<u8> {
const BYTECODE: &[u8] =
include_bytes!(concat!(env!("OUT_DIR"), "/serai-processor-ethereum-router/Router.bin"));
Bytes::from_hex(BYTECODE).expect("compiled-in Router bytecode wasn't valid hex").to_vec()
const BYTECODE: &[u8] = {
const BYTECODE_HEX: &[u8] =
include_bytes!(concat!(env!("OUT_DIR"), "/serai-processor-ethereum-router/Router.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!("Router.bin did not contain valid hex"),
};
&BYTECODE
};
BYTECODE.to_vec()
}
fn init_code(key: &PublicKey) -> Vec<u8> {

View file

@ -0,0 +1,83 @@
use alloy_core::primitives::{hex, Address, U256, Bytes, TxKind, PrimitiveSignature};
use alloy_sol_types::SolCall;
use alloy_consensus::{TxLegacy, SignableTransaction, Signed};
use alloy_provider::Provider;
use ethereum_primitives::keccak256;
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/ERC20.sol");
}
pub struct Erc20(Address);
impl Erc20 {
pub(crate) async fn deploy(test: &Test) -> Self {
const BYTECODE: &[u8] = {
const BYTECODE_HEX: &[u8] =
include_bytes!(concat!(env!("OUT_DIR"), "/serai-processor-ethereum-router/TestERC20.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!("TestERC20.bin did not contain valid hex"),
};
&BYTECODE
};
let tx = TxLegacy {
chain_id: None,
nonce: 0,
gas_price: 100_000_000_000u128,
gas_limit: 1_000_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;
Self(receipt.contract_address.unwrap())
}
pub(crate) fn address(&self) -> Address {
self.0
}
pub(crate) async fn approve(&self, test: &Test, owner: Address, spender: Address, amount: U256) {
let tx = TxLegacy {
chain_id: None,
nonce: 0,
gas_price: 100_000_000_000u128,
gas_limit: 1_000_000,
to: self.0.into(),
value: U256::ZERO,
input: abi::TestERC20::magicApproveCall::new((owner, spender, amount)).abi_encode().into(),
};
let tx = ethereum_primitives::deterministically_sign(tx);
let receipt = ethereum_test_primitives::publish_tx(&test.provider, tx).await;
assert!(receipt.status());
}
pub(crate) async fn mint(&self, test: &Test, account: Address, amount: U256) {
let tx = TxLegacy {
chain_id: None,
nonce: 0,
gas_price: 100_000_000_000u128,
gas_limit: 1_000_000,
to: self.0.into(),
value: U256::ZERO,
input: abi::TestERC20::mintCall::new((account, amount)).abi_encode().into(),
};
let tx = ethereum_primitives::deterministically_sign(tx);
let receipt = ethereum_test_primitives::publish_tx(&test.provider, tx).await;
assert!(receipt.status());
}
}

View file

@ -38,6 +38,8 @@ use crate::{
};
mod constants;
mod erc20;
use erc20::Erc20;
pub(crate) fn test_key() -> (Scalar, PublicKey) {
loop {
@ -241,13 +243,17 @@ impl Test {
self.verify_state().await;
}
fn in_instruction() -> Shorthand {
Shorthand::Raw(RefundableInInstruction {
origin: None,
instruction: SeraiInInstruction::Transfer(SeraiAddress([0xff; 32])),
})
}
fn eth_in_instruction_tx(&self) -> (Coin, U256, Shorthand, TxLegacy) {
let coin = Coin::Ether;
let amount = U256::from(1);
let shorthand = Shorthand::Raw(RefundableInInstruction {
origin: None,
instruction: SeraiInInstruction::Transfer(SeraiAddress([0xff; 32])),
});
let shorthand = Self::in_instruction();
let mut tx = self.router.in_instruction(coin, amount, &shorthand);
tx.gas_limit = 1_000_000;
@ -363,7 +369,74 @@ async fn test_eth_in_instruction() {
#[tokio::test]
async fn test_erc20_in_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 amount = U256::from(1);
let shorthand = Test::in_instruction();
// The provided `in_instruction` function will use a top-level transfer for ERC20 InInstructions,
// so we have to manually write this call
let tx = TxLegacy {
chain_id: None,
nonce: 0,
gas_price: 100_000_000_000u128,
gas_limit: 1_000_000,
to: test.router.address().into(),
value: U256::ZERO,
input: crate::abi::inInstructionCall::new((coin.into(), amount, shorthand.encode().into()))
.abi_encode()
.into(),
};
// If no `approve` was granted, this should fail
assert!(matches!(
test.call_and_decode_err(tx.clone()).await,
IRouterErrors::TransferFromFailed(IRouter::TransferFromFailed {})
));
let tx = ethereum_primitives::deterministically_sign(tx);
{
let signer = tx.recover_signer().unwrap();
erc20.mint(&test, signer, amount).await;
erc20.approve(&test, signer, test.router.address(), amount).await;
}
let receipt = ethereum_test_primitives::publish_tx(&test.provider, tx.clone()).await;
assert!(receipt.status());
let block = receipt.block_number.unwrap();
// If we don't whitelist this token, we shouldn't be yielded an InInstruction
{
let in_instructions =
test.router.in_instructions_unordered(block, block, &HashSet::new()).await.unwrap();
assert!(in_instructions.is_empty());
}
let in_instructions = test
.router
.in_instructions_unordered(block, block, &HashSet::from([coin.into()]))
.await
.unwrap();
assert_eq!(in_instructions.len(), 1);
assert_eq!(
in_instructions[0],
InInstruction {
id: LogIndex {
block_hash: *receipt.block_hash.unwrap(),
// First is the Transfer log, then the InInstruction log
index_within_block: receipt.inner.logs()[1].log_index.unwrap(),
},
transaction_hash: **tx.hash(),
from: tx.recover_signer().unwrap(),
coin,
amount,
data: shorthand.encode(),
}
);
}
#[tokio::test]