# EIP-4626 Compatibility Study

By [banteg](https://paragraph.com/@banteg) · 2023-07-25

---

Summary
=======

There are currently 566 contracts implementing [EIP-4626 Tokenized Vaults](https://eips.ethereum.org/EIPS/eip-4626) standard on Ethereum mainnet.

Of them, 69 contracts, including some notable ones like [Savings DAI](https://etherscan.io/address/0x83F20F44975D03b1b09e64809B757c47f942BEeA) and [Staked Yearn Ether](https://etherscan.io/address/0x8E5CBc6f470d064063341aceF7c45172A3EEf766), don't emit a `Transfer` event on `mint/deposit/burn/redeem`.

This could affect tokens being picked up by explorers and wallets, as well as require changes to data analytics pipelines to correctly account for all balance changes.

The results can be [found here (.csv)](https://gist.github.com/banteg/ee0893630099853cfc4c971f85f4bdbf).

How it happened
===============

The EIP-4626 standard defines `Deposit` and `Withdraw` events that contain all the needed info. The `Transfer` event comes from [ERC-20 Token Standard](https://eips.ethereum.org/EIPS/eip-20), which says:

> A token contract which creates new tokens SHOULD trigger a Transfer event with the `_from` address set to `0x0` when tokens are created.

It doesn’t contain a strict requirement (and [auditors agree](https://github.com/makerdao/sdai/blob/master/audits/ChainSecurity_Oazo_Apps_Limited_Savings_Dai_audit_1.pdf)) as it doesn’t say anything about burning, but every data analyst hates it when a token doesn’t follow an implicit convention of `Transfer(0x, receiver, amount)` on `mint` and `Transfer(owner, 0x, amount)` on `burn`.

This problem can be traced to [EIP-4626 example repo](https://github.com/fubuloubu/ERC4626) and most likely stems from it.

The problem doesn’t exist in other implementation like [Snekmate](https://github.com/pcaversaccio/snekmate/blob/690dc0710aa0c3fd80f707ebf6dd50674f3f7ff1/src/extensions/ERC4626.vy#L668) and [Solady](https://github.com/Vectorized/solady/blob/30558f5402f02351b96eeb6eaf32bcea29773841/src/tokens/ERC4626.sol#L454).

What can be done
================

If you operate an explorer or a wallet, you can do the following:

1.  For `Deposit(sender, owner, assets, shares)` event check if there is an adjacent `Transfer(0x0, owner, shares)` and if it’s missing, insert it.
    
2.  For `Withdraw(sender, receiver, owner, assets, shares)` check for `Transfer(owner, 0x0, shares)` and add it if it’s missing.
    

Methodology
===========

To find all EIP-4626 compatible tokens you can start from all the `Deposit` events. As often happens with events, there would be collisions with unrelated contracts that happen to share the same selector. The code here uses [Ape](https://github.com/apeworx/ape) and some portions could be shortened or omitted for brevity.

    sdai = Contract('0x83F20F44975D03b1b09e64809B757c47f942BEeA')
    log_filter = LogFilter.from_event(sdai.Deposit)
    deposit_logs = list(chain.provider.get_contract_logs(log_filter))
    

This would pull all `Deposit` events emitted by all addresses. Do the same for `Withdraw` events. Now we need to filter out the incompatible contracts. EIP-4626 doesn’t require implementing EIP-165 `supportsInterface` method, so let’s do this by comparing a contract interface against the reference ABI.

    abi = requests.get('https://raw.githubusercontent.com/fubuloubu/ERC4626/main/contracts/ERC4626.json').json()
    erc4626 = ContractType.parse_obj({'abi': abi})
    
    def is_erc4626(c: ContractType):
        events = all(e in c.events for e in erc4626.events)
        methods = all(m in c.methods for m in erc4626.methods)
        return events and methods
    

Next up we pull all the `Transfer` logs for found contracts.

    log_filter = LogFilter.from_event(sdai.Transfer, addresses=erc4626_compatible_addrs)
    transfer_logs = list(chain.provider.get_contract_logs(log_filter))
    

The only remaining thing is to find whether there was a matching `Transfer` event for each `Deposit` and `Withdraw` event and annotate the results.

    def check_compat(addr):
        transfer_tuples = {tuple(log.event_arguments.values()) for log in transfer_logs_by_addr[addr]}
    
        try:    
            for log in deposit_logs_by_addr[addr]:
                assert len(log.event_arguments) == 4
                transfer_args = ZERO_ADDRESS, log['owner'], log['shares']
                assert transfer_args in transfer_tuples
            
            for log in withdraw_logs_by_addr[addr]:
                assert len(log.event_arguments) == 5
                transfer_args = log['owner'], ZERO_ADDRESS, log['shares']
                assert transfer_args in transfer_tuples
        except AssertionError:
            return False
    
        return True

---

*Originally published on [banteg](https://paragraph.com/@banteg/eip-4626-compatibility-study)*
