-
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 all commits
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,189 @@ | ||
| // 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 = 0x4d6f413771b260e6694ffc0cfc1fc0bdb079c880580315446b9e26b778417b00; | ||
|
|
||
| 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); | ||
| } | ||
|
|
||
| // Update balances without affecting total supply (supports from/to == address(0)) | ||
| function _update(address from, address to, uint256 value) internal virtual { | ||
| TokenStorage storage $ = _getS(); | ||
| // 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; | ||
| } | ||
| emit Transfer(from, to, value); | ||
| } | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
|
|
||
| // Mint (null-owner aware): increases totalSupply and credits recipient (can be address(0)) | ||
| 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); | ||
| } | ||
|
Comment on lines
+138
to
+151
|
||
|
|
||
| // Approvals | ||
| function _approve(address owner, address spender, uint256 value) internal { | ||
| _approve(owner, spender, value, true); | ||
| } | ||
|
|
||
| function _approve(address owner, address spender, uint256 value, bool emitEvent) internal virtual { | ||
| TokenStorage storage $ = _getS(); | ||
| if (owner == address(0)) { | ||
| revert ERC20InvalidApprover(address(0)); | ||
| } | ||
| if (spender == address(0)) { | ||
| revert ERC20InvalidSpender(address(0)); | ||
| } | ||
| $.allowances[owner][spender] = value; | ||
| if (emitEvent) { | ||
| emit Approval(owner, spender, value); | ||
| } | ||
| } | ||
|
|
||
| function _spendAllowance(address owner, address spender, uint256 value) internal virtual { | ||
| uint256 currentAllowance = allowance(owner, spender); | ||
| if (currentAllowance < type(uint256).max) { | ||
| if (currentAllowance < value) { | ||
| revert ERC20InsufficientAllowance(spender, currentAllowance, value); | ||
| } | ||
| unchecked { | ||
| _approve(owner, spender, currentAllowance - value, false); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| // Cap view | ||
| function maxSupply() public view virtual returns (uint256) { | ||
| TokenStorage storage $ = _getS(); | ||
| return $.cap; | ||
| } | ||
| } | ||
| 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); | ||
| } | ||
|
|
||
|
|
||
| // TokenManager-only transfer that allows to/from == address(0) | ||
| function forceTransfer(address from, address to, uint256 amount) external onlyTokenManager { | ||
| // This is used by TokenManager to shadow NFT transfers | ||
| // It bypasses approval checks since it's a system-level transfer | ||
| _transfer(from, to, amount); | ||
| _update(from, to, amount); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Bug: Minting Error in Token TransferThe |
||
| } | ||
|
|
||
| // Override transfer functions to prevent user-initiated transfers | ||
| // Only the TokenManager can move tokens via forceTransfer | ||
|
|
||
| // Disable user-initiated ERC20 flows | ||
| function transfer(address, uint256) public pure override returns (bool) { | ||
| revert("Transfers only allowed via Ethscriptions NFT"); | ||
| } | ||
|
|
||
| function transferFrom(address, address, uint256) public pure override returns (bool) { | ||
| revert("Transfers only allowed via Ethscriptions NFT"); | ||
| } | ||
|
|
||
| function approve(address, uint256) public pure override returns (bool) { | ||
| revert("Approvals not allowed"); | ||
| } | ||
|
|
||
| function increaseAllowance(address, uint256) public pure returns (bool) { | ||
| revert("Approvals not allowed"); | ||
| } | ||
|
|
||
| function decreaseAllowance(address, uint256) public pure returns (bool) { | ||
| revert("Approvals not allowed"); | ||
| } | ||
|
|
||
| // Required overrides for multiple inheritance | ||
| function _update(address from, address to, uint256 value) | ||
| internal | ||
| override(ERC20Upgradeable, ERC20CappedUpgradeable) | ||
| { | ||
| super._update(from, to, value); | ||
|
|
||
| // Token balance proving has been removed in favor of ethscription-only proving | ||
| // Token balances can be derived from ethscription ownership and transfer history | ||
| } | ||
| } | ||
| } | ||
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
_updatefunction doesn't handle the case wherefromisaddress(0)(minting scenario). When minting,fromshould beaddress(0)and the balance check will fail sinceaddress(0)has no balance to debit. This function should handle minting by checking iffrom == address(0)and only updatingtotalSupplyand creditingtoin that case.