✨ Adding ability to pack extra-data with balance in `ERC20`
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
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.
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 The 2612 logic is quite tightly bounded to the custom storage layout.