Vectorized / solady

Optimized Solidity snippets.
MIT License
2.57k stars 342 forks source link

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

Open Philogy opened 11 months ago

Philogy commented 11 months ago

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

Vectorized commented 11 months ago

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.

z0r0z commented 11 months ago

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.

Vectorized commented 11 months ago

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