mirror of
https://github.com/serai-dex/serai.git
synced 2025-12-08 12:19:24 +00:00
Add ERC20 InInstruction test
This commit is contained in:
@@ -52,7 +52,7 @@ impl Deployer {
|
|||||||
/// funded for this transaction to be submitted. This account has no known private key to anyone
|
/// 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.
|
/// so ETH sent can be neither misappropriated nor returned.
|
||||||
pub fn deployment_tx() -> Signed<TxLegacy> {
|
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
|
// Legacy transactions are used to ensure the widest possible degree of support across EVMs
|
||||||
let tx = TxLegacy {
|
let tx = TxLegacy {
|
||||||
|
|||||||
@@ -41,6 +41,9 @@ fn main() {
|
|||||||
"contracts/IRouter.sol",
|
"contracts/IRouter.sol",
|
||||||
"contracts/Router.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();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,17 +17,11 @@ contract TestERC20 {
|
|||||||
return 18;
|
return 18;
|
||||||
}
|
}
|
||||||
|
|
||||||
function totalSupply() public pure returns (uint256) {
|
uint256 public totalSupply;
|
||||||
return 1_000_000 * 10e18;
|
|
||||||
}
|
|
||||||
|
|
||||||
mapping(address => uint256) balances;
|
mapping(address => uint256) balances;
|
||||||
mapping(address => mapping(address => uint256)) allowances;
|
mapping(address => mapping(address => uint256)) allowances;
|
||||||
|
|
||||||
constructor() {
|
|
||||||
balances[msg.sender] = totalSupply();
|
|
||||||
}
|
|
||||||
|
|
||||||
function balanceOf(address owner) public view returns (uint256) {
|
function balanceOf(address owner) public view returns (uint256) {
|
||||||
return balances[owner];
|
return balances[owner];
|
||||||
}
|
}
|
||||||
@@ -35,6 +29,7 @@ contract TestERC20 {
|
|||||||
function transfer(address to, uint256 value) public returns (bool) {
|
function transfer(address to, uint256 value) public returns (bool) {
|
||||||
balances[msg.sender] -= value;
|
balances[msg.sender] -= value;
|
||||||
balances[to] += value;
|
balances[to] += value;
|
||||||
|
emit Transfer(msg.sender, to, value);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -42,15 +37,28 @@ contract TestERC20 {
|
|||||||
allowances[from][msg.sender] -= value;
|
allowances[from][msg.sender] -= value;
|
||||||
balances[from] -= value;
|
balances[from] -= value;
|
||||||
balances[to] += value;
|
balances[to] += value;
|
||||||
|
emit Transfer(from, to, value);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
function approve(address spender, uint256 value) public returns (bool) {
|
function approve(address spender, uint256 value) public returns (bool) {
|
||||||
allowances[msg.sender][spender] = value;
|
allowances[msg.sender][spender] = value;
|
||||||
|
emit Approval(msg.sender, spender, value);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
function allowance(address owner, address spender) public view returns (uint256) {
|
function allowance(address owner, address spender) public view returns (uint256) {
|
||||||
return allowances[owner][spender];
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -11,10 +11,7 @@ use borsh::{BorshSerialize, BorshDeserialize};
|
|||||||
|
|
||||||
use group::ff::PrimeField;
|
use group::ff::PrimeField;
|
||||||
|
|
||||||
use alloy_core::primitives::{
|
use alloy_core::primitives::{hex, Address, U256, TxKind};
|
||||||
hex::{self, FromHex},
|
|
||||||
Address, U256, Bytes, TxKind,
|
|
||||||
};
|
|
||||||
use alloy_sol_types::{SolValue, SolConstructor, SolCall, SolEvent};
|
use alloy_sol_types::{SolValue, SolConstructor, SolCall, SolEvent};
|
||||||
|
|
||||||
use alloy_consensus::TxLegacy;
|
use alloy_consensus::TxLegacy;
|
||||||
@@ -257,9 +254,18 @@ impl Router {
|
|||||||
const ESCAPE_HATCH_GAS: u64 = 61_238;
|
const ESCAPE_HATCH_GAS: u64 = 61_238;
|
||||||
|
|
||||||
fn code() -> Vec<u8> {
|
fn code() -> Vec<u8> {
|
||||||
const BYTECODE: &[u8] =
|
const BYTECODE: &[u8] = {
|
||||||
include_bytes!(concat!(env!("OUT_DIR"), "/serai-processor-ethereum-router/Router.bin"));
|
const BYTECODE_HEX: &[u8] =
|
||||||
Bytes::from_hex(BYTECODE).expect("compiled-in Router bytecode wasn't valid hex").to_vec()
|
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> {
|
fn init_code(key: &PublicKey) -> Vec<u8> {
|
||||||
|
|||||||
83
processor/ethereum/router/src/tests/erc20.rs
Normal file
83
processor/ethereum/router/src/tests/erc20.rs
Normal 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());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -38,6 +38,8 @@ use crate::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
mod constants;
|
mod constants;
|
||||||
|
mod erc20;
|
||||||
|
use erc20::Erc20;
|
||||||
|
|
||||||
pub(crate) fn test_key() -> (Scalar, PublicKey) {
|
pub(crate) fn test_key() -> (Scalar, PublicKey) {
|
||||||
loop {
|
loop {
|
||||||
@@ -241,13 +243,17 @@ impl Test {
|
|||||||
self.verify_state().await;
|
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) {
|
fn eth_in_instruction_tx(&self) -> (Coin, U256, Shorthand, TxLegacy) {
|
||||||
let coin = Coin::Ether;
|
let coin = Coin::Ether;
|
||||||
let amount = U256::from(1);
|
let amount = U256::from(1);
|
||||||
let shorthand = Shorthand::Raw(RefundableInInstruction {
|
let shorthand = Self::in_instruction();
|
||||||
origin: None,
|
|
||||||
instruction: SeraiInInstruction::Transfer(SeraiAddress([0xff; 32])),
|
|
||||||
});
|
|
||||||
|
|
||||||
let mut tx = self.router.in_instruction(coin, amount, &shorthand);
|
let mut tx = self.router.in_instruction(coin, amount, &shorthand);
|
||||||
tx.gas_limit = 1_000_000;
|
tx.gas_limit = 1_000_000;
|
||||||
@@ -363,7 +369,74 @@ async fn test_eth_in_instruction() {
|
|||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_erc20_in_instruction() {
|
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]
|
#[tokio::test]
|
||||||
|
|||||||
Reference in New Issue
Block a user