mirror of
https://github.com/serai-dex/serai.git
synced 2025-04-16 11:11:56 +00:00
Add ERC20 InInstruction test
This commit is contained in:
parent
a63a86ba79
commit
3d44766eff
6 changed files with 194 additions and 21 deletions
|
@ -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 {
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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> {
|
||||
|
|
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 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]
|
||||
|
|
Loading…
Reference in a new issue