Enable address(0) erc20 balances to match ethscription behavior#121
Conversation
There was a problem hiding this comment.
Pull Request Overview
This pull request enables ERC-20 tokens to have balances at the zero address (address(0)) to match Ethscription behavior, where tokens can be held by the null address.
Key changes:
- Creates a new ERC20 implementation that allows null address ownership
- Updates token interface from
cap()tomaxSupply()for consistency - Adds comprehensive test coverage for null address token operations
Reviewed Changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
| contracts/src/ERC20NullOwnerCappedUpgradeable.sol | New ERC20 implementation that supports null address ownership by overriding the _update function to credit rather than burn tokens sent to address(0) |
| contracts/src/EthscriptionsERC20.sol | Refactored to use the new null-owner-aware base contract and simplified token management functions |
| contracts/test/EthscriptionsToken.t.sol | Added tests for null address minting and transfers, plus updated existing test to use new maxSupply() method |
Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.
| // TokenManager-only mint that allows to == address(0) | ||
| function mint(address to, uint256 amount) external onlyTokenManager { | ||
| _mint(to, amount); | ||
| _update(address(0), to, amount); |
There was a problem hiding this comment.
The mint function bypasses the standard _mint helper function which handles minting logic. Consider using _mint(to, amount) instead of directly calling _update(address(0), to, amount) for consistency with ERC20 patterns and to ensure any future mint-specific logic is preserved.
| _update(address(0), to, amount); | |
| _mint(to, amount); |
| bytes32 private constant STORAGE_LOCATION = 0x8f4f7bb0f9a741a04db8c5a3930ef1872dc1b0c6f996f78adc3f57e5f8b78400; | ||
|
|
||
| function _getS() private pure returns (TokenStorage storage $) { | ||
| assembly { | ||
| $.slot := STORAGE_LOCATION |
There was a problem hiding this comment.
The storage location calculation comment should be verified. The actual hash calculation should match the provided constant value to ensure storage slot uniqueness and prevent collisions with other contracts.
| bytes32 private constant STORAGE_LOCATION = 0x8f4f7bb0f9a741a04db8c5a3930ef1872dc1b0c6f996f78adc3f57e5f8b78400; | |
| function _getS() private pure returns (TokenStorage storage $) { | |
| assembly { | |
| $.slot := STORAGE_LOCATION | |
| // Compute storage slot as described in the comment above | |
| function _storageLocation() internal pure returns (bytes32) { | |
| // keccak256(abi.encode(uint256(keccak256("ethscriptions.storage.ERC20NullOwnerCapped")) - 1)) & ~bytes32(uint256(0xff)) | |
| bytes32 typeHash = keccak256("ethscriptions.storage.ERC20NullOwnerCapped"); | |
| uint256 slot = uint256(keccak256(abi.encode(uint256(typeHash) - 1))) & ~uint256(0xff); | |
| return bytes32(slot); | |
| } | |
| function _getS() private pure returns (TokenStorage storage $) { | |
| bytes32 location = _storageLocation(); | |
| assembly { | |
| $.slot := location |
| // Mint (null-owner aware) | ||
| function _mint(address account, uint256 value) internal { | ||
| _update(address(0), account, value); | ||
| } | ||
|
|
There was a problem hiding this comment.
The _mint function is defined but not used in the codebase where mint directly calls _update. This creates inconsistency - either use _mint in the public mint function or remove this unused helper to avoid confusion.
| // Mint (null-owner aware) | |
| function _mint(address account, uint256 value) internal { | |
| _update(address(0), account, value); | |
| } |
There was a problem hiding this comment.
Pull Request Overview
Copilot reviewed 9 out of 10 changed files in this pull request and generated 2 comments.
Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.
| // Debit from | ||
| uint256 fromBalance = $.balances[from]; | ||
| if (fromBalance < value) { | ||
| revert ERC20InsufficientBalance(from, fromBalance, value); | ||
| } | ||
| unchecked { | ||
| $.balances[from] = fromBalance - value; | ||
| } | ||
| // Credit to | ||
| unchecked { | ||
| $.balances[to] += value; | ||
| } |
There was a problem hiding this comment.
The _update function doesn't handle the case where from is address(0) (minting scenario). When minting, from should be address(0) and the balance check will fail since address(0) has no balance to debit. This function should handle minting by checking if from == address(0) and only updating totalSupply and crediting to in that case.
| // Debit from | |
| uint256 fromBalance = $.balances[from]; | |
| if (fromBalance < value) { | |
| revert ERC20InsufficientBalance(from, fromBalance, value); | |
| } | |
| unchecked { | |
| $.balances[from] = fromBalance - value; | |
| } | |
| // Credit to | |
| unchecked { | |
| $.balances[to] += value; | |
| } | |
| if (from == address(0)) { | |
| // Minting: only credit 'to' | |
| unchecked { | |
| $.balances[to] += value; | |
| } | |
| } else { | |
| // Debit from | |
| uint256 fromBalance = $.balances[from]; | |
| if (fromBalance < value) { | |
| revert ERC20InsufficientBalance(from, fromBalance, value); | |
| } | |
| unchecked { | |
| $.balances[from] = fromBalance - value; | |
| } | |
| // Credit to | |
| unchecked { | |
| $.balances[to] += value; | |
| } | |
| } |
| function _mint(address account, uint256 value) internal { | ||
| TokenStorage storage $ = _getS(); | ||
|
|
||
| uint256 newSupply = $.totalSupply + value; | ||
| if (newSupply > $.cap) { | ||
| revert ERC20ExceededCap(newSupply, $.cap); | ||
| } | ||
|
|
||
| $.totalSupply = newSupply; | ||
|
|
||
| unchecked { $.balances[account] += value; } | ||
|
|
||
| emit Transfer(address(0), account, value); | ||
| } |
There was a problem hiding this comment.
The _mint function bypasses the _update function and directly manipulates balances and totalSupply. This creates inconsistency since _update is meant to be the single point for balance changes. The _mint function should call _update(address(0), account, value) after checking the cap, but _update needs to be fixed first to handle the minting case.
Note
Introduce an upgradeable ERC20 base that allows address(0) balances and migrate EthscriptionsERC20 to use it, enabling mint/transfer to the null address with updated tests and interface paths.
address(0)as a valid holder; includesmaxSupply().ERC20NullOwnerCappedUpgradeable; TokenManager-onlymintandforceTransfernow allowto/from == address(0)via_update; disables user transfers/approvals; exposesmaxSupply()instead ofcap().interfaces/IProtocolHandlerand local paths; align with null-owner ERC20 behavior.cap()assertions withmaxSupply(); add tests for minting to and transferring NFTs intoaddress(0)reflecting in ERC20 balances; adjust imports.Written by Cursor Bugbot for commit 9d82ffe. This will update automatically on new commits. Configure here.