Getting rid of WETH using EIP-3074

(Originally posted here.)

While randomly looking at WETH I was wondering,

Hmm, this was written such a long time ago, what would it look like with modern Solidity?

WETH9 was written for 0.4.18 (2017!) and we are at 0.8.10 today. (@MrChico, what does 9 stands for?)

So I started iteratively changing it, adding small improvements one-by-one:

  1. Turning those constants into actual constants
  2. Upgrading the syntax of the fallback function
  3. Adding the emit keyword to events
  4. Fine-tuning public into external where it makes sense
  5. Using custom errors (revert InsufficientBalance())
  6. Adding even more type casting, especially for that comparison with -1
  7. (And maybe renaming wad :grimacing:)

Cool. This took a few minutes. The code was pretty nice to begin with as it didn’t had any unneeded complexity. Now what?

I remembered those old attempts of trying to get rid of WETH as it is a nuisance. There were some proposals to enshrine it as a “precompile”. There was a proposal, WETH10, to introduce EIP-2612 permits and flash loans into it. And a rewrite into Yul+, WETH11, mostly motivated by saving gas (some explainer thread here).

Getting rid of WETH for good always intrigued me as a goal, therefore spent some time in the past thinking about those enshrining proposals. It just occurred to me that perhaps with EIP-3074 we can make this happen!

One would need to call authorize on this new contract, and from that point it gains access to the authorizer’s Ether. Transfers are still subject to the well known allowance system. (I assume by the time EIP-3074 goes live there will be nice ways to revoke authorizations.)

Here’s the rough code I put together in a few minutes:

Disclaimer: do not use this for anything.

pragma solidity ^0.8.0;

library EIP3074 {
    function transferEther(bytes32 commit, uint8 yParity, uint r, uint s, address sender, address recipient, uint amount) public {
        assembly {
            // NOTE: Verbatim actually isn't enabled in inline assembly yet
            function auth(a, b, c, d) -> e {
                e := verbatim_4i_1o(hex"f6", a, b, c, d)
            }
            function authcall(a, b, c, d, e, f, g, h) -> i {
                i := verbatim_8i_1o(hex"f7", a, b, c, d, e, f, g, h)
            }

            let authorized := auth(commit, yParity, r, s)
            if iszero(eq(authorized, sender)) { revert(0, 0) }

            let success := authcall(gas(), recipient, 0, amount, 0, 0, 0, 0)
            if iszero(success) { revert(0, 0) }
        }
    }
}

contract WETH3074 {
    string public constant name     = "Wrapped Ether";
    string public constant symbol   = "WETH";
    uint8  public constant decimals = 18;

    event  Approval(address indexed src, address indexed guy, uint wad);
    event  Transfer(address indexed src, address indexed dst, uint wad);

    mapping (address => mapping (address => uint)) public allowance;

    struct AuthParams {
        bytes32 commit;
        uint8 yParity;
        uint r;
        uint s;
    }
    mapping (address => AuthParams) private authParams;

    function totalSupply() external pure returns (uint) {
        // TODO: what to do with this?
        return uint(int(-1));
    }

    function balanceOf(address account) public view returns (uint) {
        return account.balance;
    }

    function approve(address guy, uint wad) external returns (bool) {
        allowance[msg.sender][guy] = wad;
        emit Approval(msg.sender, guy, wad);
        return true;
    }

    function transfer(address dst, uint wad) external returns (bool) {
        return transferFrom(msg.sender, dst, wad);
    }

    function transferFrom(address src, address dst, uint wad)
        public
        returns (bool)
    {
        require(balanceOf(src) >= wad); // TODO use custom error

        if (src != msg.sender && allowance[src][msg.sender] != uint(int(-1))) {
            require(allowance[src][msg.sender] >= wad); // TODO use custom error
            allowance[src][msg.sender] -= wad;
        }

        AuthParams memory params = authParams[src];
        EIP3074.transferEther(params.commit, params.yParity, params.r, params.s, src, dst, wad);

        emit Transfer(src, dst, wad);

        return true;
    }

    function authorize(bytes32 commit, uint8 yParity, uint r, uint s) external {
        authParams[msg.sender] = AuthParams(commit, yParity, r, s);
    }
}

It has at least the following problems:

  1. The totalSupply is not set properly – it is not possible to extract this information from within the chain, yet
  2. The Transfer event is only emitted if transfer is done via the token, but accounts can still natively do transfers
  3. EIP-3074 actually doesn’t yet allow valueExt, i.e. transfers of Ether of the authorizing account
  4. EIP-3074 is nowhere near to adoption yet
  5. The authorisation parameters could be stored more efficiently

Coincidentally this also solves the complaint of WETH9 not emitting a transfer on deposit, since there’s no need for depositing.

Thanks @hrkrshnn and @matt for the brief review.

P.S. There are security considerations about EIP-3074, which are not specific to this use case. It is probably better discussing them in the existing issues.

4 Likes

I am still intrigued by the possibility of making Ether ERC-20 compatible with such a simple “trick”.

One of currently not fully solved problems of EIP-3074 is that authorisation cannot be easily withdrawn (there are multiple proposals for it). It occurred to me that in the case of this contract, as long as it has no “bugs”, having an authorize and deauthorize function could be sufficient:

    /// Authorise for sender.
    function authorize(bytes32 commit, bool yParity, uint r, uint s) external {
        authParams[msg.sender] = AuthParams(commit, (yParity ? (1 << 255) : 0) | r, s);
    }

    /// Removes authorisation for sender account.
    function deauthorize() external {
        delete authParams[msg.sender];
    }

Created a repository for the fun of it, and to have a better track of iterations: GitHub - axic/weth3074

1 Like

WETH9 uses kelvin versioning, meaning that 9 is the first version, and subsequent versions use a smaller number. The intention is that as the version number approaches zero, developers are more and more aware that further changes are not recommended.

Obviously I had no idea about that when I coded WETH10 :man_shrugging:

1 Like

@edsonayllon suggested that the name should be different to WETH and to not use an EIP number to avoid confusion. The suggested name is “AETH” aka “Authorized Ether”. I think that may not be a bad name.

1 Like