(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:
- Turning those constants into actual constants
- Upgrading the syntax of the fallback function
- Adding the
emit
keyword to events - Fine-tuning
public
intoexternal
where it makes sense - Using custom errors (
revert InsufficientBalance()
) - Adding even more type casting, especially for that comparison with
-1
- (And maybe renaming
wad
)
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:
- The
totalSupply
is not set properly – it is not possible to extract this information from within the chain, yet - The
Transfer
event is only emitted if transfer is done via the token, but accounts can still natively do transfers - EIP-3074 actually doesn’t yet allow
valueExt
, i.e. transfers of Ether of the authorizing account - EIP-3074 is nowhere near to adoption yet
- 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.