Juan Antonio Tubío
Juan Antonio Tubío

Reputation: 1191

How to check if one wallet address have already called a method on an EVM contract (PYTHON)

Introduction

I have a list of wallets and I want to ensure that a contract function is called only once. To achieve this, I iterate through the wallets, check if each wallet has already called that function, and if not, I proceed to call it. (I'm testing on Linea and Base blockchains buy I'm looking for a universal solution for EVM blockchains.)

Explanation

I need to check if a specific contract function has been called by a wallet address after a given block number. Here are the data I have:

What I tried.

Using an API to Fetch Transactions:

I used the Lineascan/Basescan API to fetch transactions with the following code:

        url = (f"{self.scan_api_url}"
               f"?module=account"
               f"&action=txlist"
               f"&address={wallet_address if wallet_address is not None else self.wallet_address}"
               f"&startblock={self.quest.start_block}"
               f"&endblock=99999999"
               f"&page=1"
               f"&offset=10000"
               f"&sort=asc"
               f"&apikey={self.scan_api_key}")

And checking for contract_address on the list.

However, this approach often fails to return recent blocks due to delays in the API.

Iterating Through Blocks:

I tried iterating from the latest block back to startblock and checking for wallet_address.

block = self.w3.eth.get_block(block_num, full_transactions=True)

This method is very slow because it processes a large number of blocks.

Using get_logs():

I attempted to use the get_logs() method but faced limitations:

logs = self.w3.eth.get_logs()

The method has a limit of 1000 blocks and I couldn't figure out how to filter by wallet address and contract function.

What I'm Looking For:

Preferred Solution: A way to efficiently check if wallet_address has called contract_function_name on contract_address after start_block. Ideally, this solution would filter by both wallet address and contract function.

Alternative Solution: If filtering by both wallet address and contract function is not feasible, a solution that simply checks if wallet_address interacted with contract_address would be acceptable.

Any help or guidance on how to achieve this efficiently would be greatly appreciated!

Upvotes: 0

Views: 118

Answers (1)

Petr Hejda
Petr Hejda

Reputation: 43581

The most straightforward solution is to track the method executions onchain in Solidity - assuming that the contract is in development stage and that you can still redeploy it with the code changes.

pragma solidity ^0.8.26;

contract MyContract {
    mapping (address caller => mapping (bytes4 signature => bool called)) public wasCalled;

    modifier onlyOneCallPerAddress(address caller, bytes4 signature) {
        require(wasCalled[caller][signature] == false, "cannot call twice");
        wasCalled[caller][signature] = true;
        _;
    }

    function foo() external onlyOneCallPerAddress(msg.sender, msg.sig) {
    }

    function foo2(uint256 number) external onlyOneCallPerAddress(msg.sender, msg.sig) {
    }

    function notTracked() external {
        // this function does not use the `onlyOneCallPerAddress` modifier
        // so it can be called as many times as the caller wants
    }
}

This onlyOneCallPerAddress modifier accepts two arguments:

  1. Caller of the function (note that the caller can be EOA as well as another contract)

  2. And the function signature. It's the unique identifier of the called function and it is stored in the read-only global variable msg.sig.

    The function signature doesn't take argument values into account. So no matter what value you pass to the foo2() function, each caller can only invoke it once.


As for the offchain solution, I'm afraid you'll need to loop through all event logs emitted by the contract. And if any of the functions that you want to track doesn't emit event logs, you'll need to loop through all transactions.

Depending on your use case, you might not want to forget to run block traces to retrieve and validate against internal transactions as well. Otherwise, the caller could bypass your check by using proxy contracts.

  1. caller 1 -> proxy 1 -> your contract, target function foo()
  2. caller 1 -> proxy 2 -> your contract, target function foo()

Reading transaction receipts (without the internal transactions), this would simply show up as:

  1. from caller 1, to proxy 1
  2. from caller 1, to proxy 2

Unfortunately I don't know Python that well - but hopefully someone else can expand on this and post the code.

Upvotes: 0

Related Questions