-
Notifications
You must be signed in to change notification settings - Fork 25
Enable address(0) erc20 balances to match ethscription behavior #121
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,193 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // SPDX-License-Identifier: MIT | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| pragma solidity 0.8.24; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import {IERC20Metadata} from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import {ContextUpgradeable} from "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import {IERC20Errors} from "@openzeppelin/contracts/interfaces/draft-IERC6093.sol"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// @title ERC20NullOwnerCappedUpgradeable | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// @notice ERC20 (Upgradeable) + Cap adapted to treat address(0) as a valid holder; single storage struct | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| abstract contract ERC20NullOwnerCappedUpgradeable is Initializable, ContextUpgradeable, IERC20, IERC20Metadata, IERC20Errors { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// @custom:storage-location erc7201:ethscriptions.storage.ERC20NullOwnerCapped | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| struct TokenStorage { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| mapping(address account => uint256) balances; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| mapping(address account => mapping(address spender => uint256)) allowances; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| uint256 totalSupply; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| string name; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| string symbol; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| uint256 cap; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Unique storage slot for this combined ERC20 + Cap storage | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // keccak256(abi.encode(uint256(keccak256("ethscriptions.storage.ERC20NullOwnerCapped")) - 1)) & ~bytes32(uint256(0xff)) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| bytes32 private constant STORAGE_LOCATION = 0x8f4f7bb0f9a741a04db8c5a3930ef1872dc1b0c6f996f78adc3f57e5f8b78400; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| function _getS() private pure returns (TokenStorage storage $) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| assembly { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| $.slot := STORAGE_LOCATION | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Errors copied from OZ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| error ERC20ExceededCap(uint256 increasedSupply, uint256 cap); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| error ERC20InvalidCap(uint256 cap); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Initializers | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| function __ERC20_init(string memory name_, string memory symbol_) internal onlyInitializing { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| __ERC20_init_unchained(name_, symbol_); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| function __ERC20_init_unchained(string memory name_, string memory symbol_) internal onlyInitializing { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| TokenStorage storage $ = _getS(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| $.name = name_; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| $.symbol = symbol_; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| function __ERC20Capped_init(uint256 cap_) internal onlyInitializing { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| __ERC20Capped_init_unchained(cap_); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| function __ERC20Capped_init_unchained(uint256 cap_) internal onlyInitializing { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| TokenStorage storage $ = _getS(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (cap_ == 0) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| revert ERC20InvalidCap(0); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| $.cap = cap_; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Views | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| function name() public view virtual returns (string memory) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| TokenStorage storage $ = _getS(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return $.name; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| function symbol() public view virtual returns (string memory) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| TokenStorage storage $ = _getS(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return $.symbol; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| function decimals() public view virtual returns (uint8) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return 18; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| function totalSupply() public view virtual returns (uint256) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| TokenStorage storage $ = _getS(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return $.totalSupply; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| function balanceOf(address account) public view virtual returns (uint256) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| TokenStorage storage $ = _getS(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return $.balances[account]; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| function allowance(address owner, address spender) public view virtual returns (uint256) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| TokenStorage storage $ = _getS(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return $.allowances[owner][spender]; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // External ERC-20 (can be overridden to restrict usage in child) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| function transfer(address to, uint256 value) public virtual returns (bool) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| address owner = _msgSender(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| _transfer(owner, to, value); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return true; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| function approve(address spender, uint256 value) public virtual returns (bool) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| address owner = _msgSender(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| _approve(owner, spender, value); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return true; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| function transferFrom(address from, address to, uint256 value) public virtual returns (bool) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| address spender = _msgSender(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| _spendAllowance(from, spender, value); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| _transfer(from, to, value); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return true; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Internal core | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| function _transfer(address from, address to, uint256 value) internal { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (from == address(0)) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| revert ERC20InvalidSender(address(0)); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Allow `to == address(0)` to support null-owner semantics | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| _update(from, to, value); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Modified from OZ: do NOT burn on to == address(0); always credit recipient (including zero address). | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| function _update(address from, address to, uint256 value) internal virtual { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| TokenStorage storage $ = _getS(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (from == address(0)) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Mint path | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| $.totalSupply += value; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| uint256 fromBalance = $.balances[from]; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (fromBalance < value) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| revert ERC20InsufficientBalance(from, fromBalance, value); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| unchecked { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| $.balances[from] = fromBalance - value; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // No burning: credit even address(0) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| unchecked { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| $.balances[to] += value; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+122
to
+133
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // 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; | |
| } | |
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Copilot
AI
Oct 16, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Copilot
AI
Oct 16, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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); | |
| } |
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -1,15 +1,15 @@ | ||||||
| // SPDX-License-Identifier: MIT | ||||||
| pragma solidity 0.8.24; | ||||||
|
|
||||||
| import "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol"; | ||||||
| import "@openzeppelin/contracts-upgradeable/token/ERC20/extensions/ERC20CappedUpgradeable.sol"; | ||||||
| import "./ERC20NullOwnerCappedUpgradeable.sol"; | ||||||
| import "./libraries/Predeploys.sol"; | ||||||
|
|
||||||
| contract EthscriptionsERC20 is ERC20Upgradeable, ERC20CappedUpgradeable { | ||||||
| /// @title EthscriptionsERC20 | ||||||
| /// @notice ERC20 with cap that supports null address ownership; only TokenManager can mint/transfer | ||||||
| contract EthscriptionsERC20 is ERC20NullOwnerCappedUpgradeable { | ||||||
| address public constant tokenManager = Predeploys.TOKEN_MANAGER; | ||||||
|
|
||||||
| bytes32 public deployTxHash; // The ethscription hash that deployed this token | ||||||
|
|
||||||
| function initialize( | ||||||
| string memory name_, | ||||||
| string memory symbol_, | ||||||
|
|
@@ -20,52 +20,32 @@ contract EthscriptionsERC20 is ERC20Upgradeable, ERC20CappedUpgradeable { | |||||
| __ERC20Capped_init(cap_); | ||||||
| deployTxHash = deployTxHash_; | ||||||
| } | ||||||
|
|
||||||
| modifier onlyTokenManager() { | ||||||
| require(msg.sender == tokenManager, "Only TokenManager"); | ||||||
| _; | ||||||
| } | ||||||
|
|
||||||
|
|
||||||
| // TokenManager-only mint that allows to == address(0) | ||||||
| function mint(address to, uint256 amount) external onlyTokenManager { | ||||||
| _mint(to, amount); | ||||||
| _update(address(0), to, amount); | ||||||
|
||||||
| _update(address(0), to, amount); | |
| _mint(to, amount); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Bug: Minting Error in Token Transfer
The forceTransfer function directly calls _update. When from is address(0), _update interprets this as a mint operation, incorrectly increasing totalSupply instead of transferring existing tokens. This leads to token inflation and breaks ERC20 balance invariants.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.