DEV Community

Cover image for How to detect if an address is a smart contract using Vyper
Rafael Abuawad
Rafael Abuawad

Posted on Originally published at x.com

How to detect if an address is a smart contract using Vyper

1. Setup

For this guide, we'll use a minimal setup with uv as the dependency manager and Titanoboa to compile and test Vyper contracts.

If you're unfamiliar with Vyper, it's an EVM programming language with Python-like syntax. It's simple and direct, making it a great choice for developing efficient and secure smart contracts.

Create the project

Initialize a new project with uv:

uv init <project_name>
Enter fullscreen mode Exit fullscreen mode

Create a virtual environment:

uv venv
Enter fullscreen mode Exit fullscreen mode

Install Titanoboa:

uv add titanoboa
Enter fullscreen mode Exit fullscreen mode

Ready!

2. Vyper Built-in Features

Now that Titanoboa is installed, how do we detect whether an address is a smart contract?

The .is_contract Property

All address variables in Vyper have an .is_contract property.

.is_contract → bool
Enter fullscreen mode Exit fullscreen mode

Returns a boolean indicating whether a contract is deployed at the address.

It's that simple!

Create a file named Detector.vy inside a contracts folder:

contracts/Detector.vy

# pragma version ==0.4.3

@external
@pure
def is_target_a_contract(target: address) -> bool:
    return target.is_contract
Enter fullscreen mode Exit fullscreen mode

Let's Test It!

In the main.py file at the project root, write the following:

main.py

import boa

USER = "0xC14fF56E720f79d08413CE00533256433D2ED928"
USDC = "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48"
VITALIK_ETH = "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045"


def main():
    with boa.fork("https://ethereum-rpc.publicnode.com"):
        detector = boa.load("contracts/Detector.vy")

        assert detector.is_target_a_contract(USDC)
        assert not detector.is_target_a_contract(USER)
        assert not detector.is_target_a_contract(VITALIK_ETH)


if __name__ == "__main__":
    main()
Enter fullscreen mode Exit fullscreen mode

Run the script:

uv run main.py
Enter fullscreen mode Exit fullscreen mode

The Problem: EIP-7702

When performing an assertion on Vitalik's wallet, Vyper identifies it as a contract. Why?

This happens because the wallet uses EIP-7702 delegation. Technically, the account now has code, but how can we distinguish this case from a traditional smart contract?

3. Special Case: EIP-7702

To handle this, we need to inspect the code stored at the address.

If the code corresponds to EIP-7702 delegation, we'll consider the address not to be a traditional smart contract, even though .is_contract may return True.

The bytecode associated with EIP-7702 delegation starts with the same three bytes:

0xef0100
Enter fullscreen mode Exit fullscreen mode

This prefix is the delegation designator. It indicates that an account delegates execution to another address.

We can read the address's .code property to inspect these bytes.

.code → Bytes
Enter fullscreen mode Exit fullscreen mode

Returns the code associated with the address.

Updated Implementation

With this adjustment, we can distinguish between a traditional contract and an externally owned account (EOA) using EIP-7702 delegation.

contracts/Detector.vy

# pragma version ==0.4.3

@external
@view
def is_target_a_contract_and_not_an_eoa(_target: address) -> bool:
    if not _target.is_contract:
        return False

    # EIP-7702 delegation designator
    eip_7702: Bytes[3] = x"ef0100"

    # If the first three bytes match, treat it as a delegated EOA
    return slice(_target.code, 0, 3) != eip_7702
Enter fullscreen mode Exit fullscreen mode

Tips

  • Why use slice? In Vyper, slice extracts a portion of a byte sequence. Here, it reads only the first three bytes, which is sufficient to check the prefix.
  • EIP-7702 context: This EIP allows EOAs to delegate execution to smart contract code, complicating traditional contract detection methods based on code presence.

4. Other Detection Methods

Although .is_contract is a native Vyper property, you may encounter other approaches in older codebases or Solidity projects.

Method 1: Comparing msg.sender and tx.origin

This approach is sometimes used to check whether the caller is an EOA or another contract.

If msg.sender == tx.origin, the caller is often assumed to be a regular wallet. However, this only validates the current caller; it does not let you check an arbitrary address.

# pragma version ==0.4.3

@external
@view
def only_humans():
    # If they are equal, the caller is traditionally considered an EOA.
    assert msg.sender == tx.origin
Enter fullscreen mode Exit fullscreen mode

The relevant properties are:

  • msg.sender → address: The immediate caller of the current call.
  • tx.origin → address: The original account that initiated the transaction.

Caution: This check is not a reliable way to establish that a caller is human or that it cannot be a smart wallet. It can also break compatibility with account abstraction and smart-wallet systems.

Method 2: Checking Code Size with .codesize

A classic approach is to check whether the address has any code.

If the code size is greater than zero, the address is traditionally considered a contract.

# pragma version ==0.4.3

@external
@view
def is_target_a_contract(target: address) -> bool:
    return target.codesize > 0
Enter fullscreen mode Exit fullscreen mode

However, code-size checks share the same EIP-7702 limitations as other methods based on detecting whether code exists.

5. Which Method Should You Use?

For modern Vyper development:

  • .is_contract — The preferred option when you want a readable, direct check for whether an address has code associated with it.
  • msg.sender == tx.origin — Avoid using this as a general contract-detection or security mechanism. It is limited to the current caller and can break compatibility with smart wallets.
  • .codesize > 0 — A classic alternative, but it has the same EIP-7702 limitations as other code-size checks.
  • Inspecting .code for the EIP-7702 prefix — Useful when your application specifically needs to distinguish delegated EOAs from traditional contracts.

The key takeaway: Choose your detection method based on what you actually need to establish. An address having code, being an EOA, and being controlled by a human are three different properties.

Top comments (0)