Skip to content

Keep ERC-20 balances and approval slots non-zero #4947

Open
@CodeSandwich

Description

@CodeSandwich

🧐 Motivation

The ERC-20 implementation uses a whole storage slot for the account balance and a whole slot for each account-spender approval. It's simple and elegant, but in some cases it's gas inefficient. Whenever a balance is fully spent or an approval fully used, the storage slot is zeroed, which triggers a gas refund. The refunds is much lower than the cost of making the slot non-zero when the balance goes up again or a new approval is set up, and the refunds may even be completely removed in the future, for example on Ethereum. This is especially wasteful if you want to approve a 3rd party to transfer an exact amount, or when a smart contract is transferring its entire balance to another contract or to the user.

📝 Details

The storage slots for the balances and approvals may be kept non-zero by always setting the highest bit to 1 and ignoring it when reading the value. For example:

    uint256 private constant _MASK = type(uint256).max >> 1; 

    function balanceOf(address account) public view virtual returns (uint256) {
        return _balances[account]  & _MASK;
    }

and

    function _update(address from, address to, uint256 value) internal virtual {
(...)
        _balances[from] = fromBalance - value | ~_MASK;

The nice part is that this is fully encapsulated because the balances and the approvals storages are both private. It's also backward compatible for the most part, it doesn't matter if the mask is already applied on the values in the storage, the code will always work the same.

The ugly part is that the slots become limited to storing values of up to 2^255-1, and the ERC-20 standard doesn't support such limitation. In the real world there doesn't seem to be any widely used token needing all 256 bits to keep the balances, but some checks would need to be done in the minting function. The approvals would become even more detached from the standard because now setting any approval above 2^255 would be interpreted as setting it to 2^256-1, which means an infinite approval. This too probably would have limited real-world implications.

An alternative

A non-backward-compatible alternative would be to apply on the stored values not a mask, but an offset of +1. Values 0 and 1 would be interpreted as 0, then 2 as 1, 3 as 2 and so on. This would broaden the maximum balances and approvals to 2^256-2.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions