solady icon indicating copy to clipboard operation
solady copied to clipboard

✨ Adding ability to pack extra-data with balance in `ERC20`

Open Philogy opened this issue 2 years ago • 3 comments

I wanted to open this issue to discuss potential designs for how ERC20 may efficiently be extended to allow for users to set a lower total supply cap than $2^{256}-1$ and use upper (or lower) bits to pack additional information. Having such a large supply cap is mostly unnecessary and developers may often want to pack other per-address data with balances to minimize cost of initial storage modifications e.g. the ERC2612 nonce.

More critically in cases where an application that has per-address information that is frequently accessed together with balances this can allow developers to save on entire cold storage accesses e.g. permission flags.

The main problems I see is that because of Solidity's lack of generics you cannot easily support an arbitrary extra-data bit size. Furthermore such packing introduces overhead that may not be desirable for applications that do not require it, however this overhead would be minimal:

If balance is stored in the upper-most bits, extra data in the lower bits the only added overhead is a simple bit-shift of the amount e.g. in _transfer:

             mstore(0x0c, or(from_, _BALANCE_SLOT_SEED))
             let fromBalanceSlot := keccak256(0x0c, 0x20)
             let fromBalance := sload(fromBalanceSlot)
+            let balChange := shl(EXTRA_DATA_BITS, amount)
             // Revert if insufficient balance.
-            if gt(amount, fromBalance) {
+            if gt(balChange, fromBalance) {
                 mstore(0x00, 0xf4d678b8) // `InsufficientBalance()`.
                 revert(0x1c, 0x04)
             }
             // Subtract and store the updated balance.
-            sstore(fromBalanceSlot, sub(fromBalance, amount))
+            sstore(fromBalanceSlot, sub(fromBalance, balChange))
             // Compute the balance slot of `to`.
             mstore(0x00, to)
             let toBalanceSlot := keccak256(0x0c, 0x20)
             // Add and store the updated balance of `to`.
             // Will not overflow because the sum of all user balances
             // cannot exceed the maximum uint256 value.
-            sstore(toBalanceSlot, add(sload(toBalanceSlot), amount))
+            sstore(toBalanceSlot, add(sload(toBalanceSlot), balChange))
             // Emit the {Transfer} event.
             mstore(0x20, amount)
             log3(0x20, 0x20, _TRANSFER_EVENT_SIGNATURE, shr(96, from_), shr(96, mload(0x0c)))

I think 96-bits is a good candidate for the space to be allocated to balances and the total supply. It allows for a total supply of 79.2B (with decimals at 18) which is arguably for most (serious) applications. Applications that go beyond this usually do so to deflate the per-unit price of a token to take advantage of people's unit bias. 96-bits for the balance would also allow you to store up to a full address as extra-data

Philogy avatar Nov 27 '23 16:11 Philogy

I think we can abuse function overrides to allow near zero-cost abstraction.

Code will be bloat tho.

The PITA is the 2612 function.

Another way is to have a ERC20P. Probably neater.

This sounds very enticing… but might let others have the fun, since ERC20 is already audited. 🤔

I was lazy to add packing in cuz memecoins that abuse unit bias is one of the primary use cases of Ethereum.

Vectorized avatar Nov 30 '23 05:11 Vectorized

any thoughts on moving 2612 out of ERC20 base? I think a more stripped down version would be easier to work with and allow for these more custom overrides more simply.

z0r0z avatar Dec 09 '23 05:12 z0r0z

@z0r0z The 2612 logic is quite tightly bounded to the custom storage layout.

Vectorized avatar Dec 11 '23 14:12 Vectorized