gakonst / ethers-rs

Complete Ethereum & Celo library and wallet implementation in Rust. https://docs.rs/ethers
Apache License 2.0
2.5k stars 795 forks source link

Unexpected TX failure with ethers-rs and geth node #824

Open sveitser opened 2 years ago

sveitser commented 2 years ago

Version

├── ethers v0.6.0 (https://github.com/gakonst/ethers-rs#f9fadf06)
│   ├── ethers-addressbook v0.1.0 (https://github.com/gakonst/ethers-rs#f9fadf06)
│   │   ├── ethers-core v0.6.0 (https://github.com/gakonst/ethers-rs#f9fadf06)
│   ├── ethers-contract v0.6.0 (https://github.com/gakonst/ethers-rs#f9fadf06)
│   │   ├── ethers-contract-abigen v0.6.0 (https://github.com/gakonst/ethers-rs#f9fadf06)
│   │   │   ├── ethers-core v0.6.0 (https://github.com/gakonst/ethers-rs#f9fadf06) (*)
│   │   ├── ethers-contract-derive v0.6.0 (proc-macro) (https://github.com/gakonst/ethers-rs#f9fadf06)
│   │   │   ├── ethers-contract-abigen v0.6.0 (https://github.com/gakonst/ethers-rs#f9fadf06)
│   │   │   │   ├── ethers-core v0.6.0 (https://github.com/gakonst/ethers-rs#f9fadf06)
│   │   │   ├── ethers-core v0.6.0 (https://github.com/gakonst/ethers-rs#f9fadf06) (*)
│   │   ├── ethers-core v0.6.0 (https://github.com/gakonst/ethers-rs#f9fadf06) (*)
│   │   ├── ethers-providers v0.6.0 (https://github.com/gakonst/ethers-rs#f9fadf06)
│   │   │   ├── ethers-core v0.6.0 (https://github.com/gakonst/ethers-rs#f9fadf06) (*)
│   ├── ethers-core v0.6.0 (https://github.com/gakonst/ethers-rs#f9fadf06) (*)
│   ├── ethers-etherscan v0.2.0 (https://github.com/gakonst/ethers-rs#f9fadf06)
│   │   ├── ethers-core v0.6.0 (https://github.com/gakonst/ethers-rs#f9fadf06) (*)
│   ├── ethers-middleware v0.6.0 (https://github.com/gakonst/ethers-rs#f9fadf06)
│   │   ├── ethers-contract v0.6.0 (https://github.com/gakonst/ethers-rs#f9fadf06) (*)
│   │   ├── ethers-core v0.6.0 (https://github.com/gakonst/ethers-rs#f9fadf06) (*)
│   │   ├── ethers-etherscan v0.2.0 (https://github.com/gakonst/ethers-rs#f9fadf06) (*)
│   │   ├── ethers-providers v0.6.0 (https://github.com/gakonst/ethers-rs#f9fadf06) (*)
│   │   ├── ethers-signers v0.6.0 (https://github.com/gakonst/ethers-rs#f9fadf06)
│   │   │   ├── ethers-core v0.6.0 (https://github.com/gakonst/ethers-rs#f9fadf06) (*)
│   ├── ethers-providers v0.6.0 (https://github.com/gakonst/ethers-rs#f9fadf06) (*)
│   ├── ethers-signers v0.6.0 (https://github.com/gakonst/ethers-rs#f9fadf06) (*)
│   └── ethers-solc v0.1.0 (https://github.com/gakonst/ethers-rs#f9fadf06)
│       ├── ethers-core v0.6.0 (https://github.com/gakonst/ethers-rs#f9fadf06) (*)
geth: Version: 1.10.15-stable

Platform Darwin air 21.1.0 Darwin Kernel Version 21.1.0: Wed Oct 13 17:33:01 PDT 2021; root:xnu-8019.41.5~1/RELEASE_ARM64_T6000 arm64 arm Darwin

Description I have a weird issue that seems specific to ethers-rs with geth where a transaction fails that I think should be working. It works if I

It’s a bit convoluted because it involves ERC20 tokens and the transferFrom method. Here's a repo with all the code https://github.com/sveitser/geth-transfer-from-repro

I expected to see this happen: The desposit tx succeeds if the contract address is approved to move the tokens.

Instead this happened: It fails.

The contract in question is

//SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "hardhat/console.sol";
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";

contract Deposit {

    uint256 dummy;

    function deposit(address asset, uint256 amount) public {
        ERC20 token = ERC20(asset);
        token.transferFrom(msg.sender, address(this), amount);
        // dummy = 1; // the tx only goes through on geth if this is uncommented
    }
}

The rust code to trigger the error

use anyhow::Result;
use ethers::abi::{Abi, Tokenize};
use ethers::contract::Contract;
use ethers::prelude::artifacts::BytecodeObject;
use ethers::prelude::*;
use std::fs;
use std::path::Path;
use std::sync::Arc;
use std::time::Duration;
use std::{convert::TryFrom, env};

abigen!(
    Deposit,
    "./abi/contracts/Deposit.sol/Deposit/abi.json",
    event_derives(serde::Deserialize, serde::Serialize);

    SimpleToken,
    "./abi/contracts/SimpleToken.sol/SimpleToken/abi.json",
    event_derives(serde::Deserialize, serde::Serialize);
);

async fn load_contract(path: &Path) -> Result<(Abi, BytecodeObject)> {
    let abi_path = path.join("abi.json");
    let bin_path = path.join("bin.txt");

    let abi = ethers::abi::Contract::load(match fs::File::open(&abi_path) {
        Ok(v) => v,
        Err(_) => panic!("Unable to open path {:?}", abi_path),
    })?;

    let bytecode_str = match fs::read_to_string(&bin_path) {
        Ok(v) => v,
        Err(_) => panic!("Unable to read from path {:?}", bin_path),
    };
    let trimmed = bytecode_str.trim().trim_start_matches("0x");
    let bytecode: BytecodeObject = serde_json::from_value(serde_json::json!(trimmed)).unwrap();

    Ok((abi, bytecode))
}

pub async fn deploy<M: 'static + Middleware, T: Tokenize>(
    client: Arc<M>,
    path: &Path,
    constructor_args: T,
) -> Result<Contract<M>> {
    let (abi, bytecode) = load_contract(path).await?;
    let factory = ContractFactory::new(abi.clone(), bytecode.into_bytes().unwrap(), client.clone());
    let contract = factory.deploy(constructor_args)?.send().await?;
    Ok(contract)
}

#[tokio::main]
async fn main() -> Result<()> {
    let rpc_url = match env::var("RPC_URL") {
        Ok(val) => val,
        Err(_) => "http://localhost:8545".to_string(),
    };

    let provider = Provider::<Http>::try_from(rpc_url.clone())
        .expect("could not instantiate HTTP Provider")
        .interval(Duration::from_millis(100u64));

    let accounts = provider.get_accounts().await.unwrap();

    let chain_id = provider.get_chainid().await.unwrap().as_u64();

    let deployer_wallet = LocalWallet::new(&mut rand::thread_rng()).with_chain_id(chain_id);
    let deployer = Arc::new(SignerMiddleware::new(
        provider.clone(),
        deployer_wallet.clone(),
    ));

    let alice = Arc::new(SignerMiddleware::new(
        provider.clone(),
        LocalWallet::new(&mut rand::thread_rng()).with_chain_id(chain_id),
    ));

    for address in [deployer_wallet.address(), alice.address()] {
        let tx = TransactionRequest::new()
            .to(address)
            .value(ethers::utils::parse_ether(U256::from(1))?)
            .from(accounts[0]);

        let tx = provider.send_transaction(tx, None).await?.await?;
        println!("Sent funding tx to {} {:?}", address, tx.unwrap().status);
    }

    let token = deploy(
        deployer.clone(),
        Path::new("./abi/contracts/SimpleToken.sol/SimpleToken"),
        (),
    )
    .await
    .unwrap();
    let token = SimpleToken::new(token.address(), deployer.clone());
    println!("deployed token");

    let deposit = deploy(
        deployer.clone(),
        Path::new("./abi/contracts/Deposit.sol/Deposit"),
        (),
    )
    .await
    .unwrap();
    let deposit = Deposit::new(deposit.address(), deployer.clone());
    println!("deployed deposit");

    let alice_token = SimpleToken::new(token.address(), alice.clone());
    let alice_deposit = Deposit::new(deposit.address(), alice.clone());

    let amount = U256::from(1000);

    token
        .transfer(alice.address(), amount)
        .send()
        .await?
        .await?;

    let tx = alice_token
        .approve(deposit.address(), amount)
        .send()
        .await?
        .await?;

    println!("approve tx status {:?}", tx.unwrap().status);

    let tx = alice_deposit
        .deposit(token.address(), amount)
        .send()
        .await?
        .await?;

    // try to avoid estimate gas?
    println!("deposit tx status {:?}", tx.unwrap().status);

    // Check the tokens have been transferred
    let balance_contract = token.balance_of(deposit.address()).call().await?;

    println!("balance contract {}", balance_contract);
    assert_eq!(balance_contract, amount);

    Ok(())
}
sveitser commented 2 years ago

Haven't had time to debug in detail but this probably has something to do with gas estimations being too low and the transaction running out of gas. This may (or not) in turn have something to do with how refunds are handled as described here https://ethereum.stackexchange.com/a/25896