Skip to content

Commit d1523b5

Browse files
Merge pull request #121 from ethscriptions-protocol/fix_erc20
Enable address(0) erc20 balances to match ethscription behavior
2 parents 9a41741 + 9d82ffe commit d1523b5

10 files changed

+284
-47
lines changed

contracts/src/CollectionsManager.sol

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,10 @@ pragma solidity 0.8.24;
33

44
import "@openzeppelin/contracts/proxy/Clones.sol";
55
import {LibString} from "solady/utils/LibString.sol";
6-
import "./protocols/EthscriptionERC721.sol";
6+
import "./EthscriptionERC721.sol";
77
import "./Ethscriptions.sol";
88
import "./libraries/Predeploys.sol";
9-
import "./protocols/IProtocolHandler.sol";
9+
import "./interfaces/IProtocolHandler.sol";
1010

1111
contract CollectionsManager is IProtocolHandler {
1212
using Clones for address;
Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
// SPDX-License-Identifier: MIT
2+
pragma solidity 0.8.24;
3+
4+
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
5+
import {IERC20Metadata} from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol";
6+
import {ContextUpgradeable} from "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol";
7+
import {IERC20Errors} from "@openzeppelin/contracts/interfaces/draft-IERC6093.sol";
8+
import {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
9+
10+
/// @title ERC20NullOwnerCappedUpgradeable
11+
/// @notice ERC20 (Upgradeable) + Cap adapted to treat address(0) as a valid holder; single storage struct
12+
abstract contract ERC20NullOwnerCappedUpgradeable is Initializable, ContextUpgradeable, IERC20, IERC20Metadata, IERC20Errors {
13+
/// @custom:storage-location erc7201:ethscriptions.storage.ERC20NullOwnerCapped
14+
struct TokenStorage {
15+
mapping(address account => uint256) balances;
16+
mapping(address account => mapping(address spender => uint256)) allowances;
17+
uint256 totalSupply;
18+
string name;
19+
string symbol;
20+
uint256 cap;
21+
}
22+
23+
// Unique storage slot for this combined ERC20 + Cap storage
24+
// keccak256(abi.encode(uint256(keccak256("ethscriptions.storage.ERC20NullOwnerCapped")) - 1)) & ~bytes32(uint256(0xff))
25+
bytes32 private constant STORAGE_LOCATION = 0x4d6f413771b260e6694ffc0cfc1fc0bdb079c880580315446b9e26b778417b00;
26+
27+
function _getS() private pure returns (TokenStorage storage $) {
28+
assembly {
29+
$.slot := STORAGE_LOCATION
30+
}
31+
}
32+
33+
// Errors copied from OZ
34+
error ERC20ExceededCap(uint256 increasedSupply, uint256 cap);
35+
error ERC20InvalidCap(uint256 cap);
36+
37+
// Initializers
38+
function __ERC20_init(string memory name_, string memory symbol_) internal onlyInitializing {
39+
__ERC20_init_unchained(name_, symbol_);
40+
}
41+
42+
function __ERC20_init_unchained(string memory name_, string memory symbol_) internal onlyInitializing {
43+
TokenStorage storage $ = _getS();
44+
$.name = name_;
45+
$.symbol = symbol_;
46+
}
47+
48+
function __ERC20Capped_init(uint256 cap_) internal onlyInitializing {
49+
__ERC20Capped_init_unchained(cap_);
50+
}
51+
52+
function __ERC20Capped_init_unchained(uint256 cap_) internal onlyInitializing {
53+
TokenStorage storage $ = _getS();
54+
if (cap_ == 0) {
55+
revert ERC20InvalidCap(0);
56+
}
57+
$.cap = cap_;
58+
}
59+
60+
// Views
61+
function name() public view virtual returns (string memory) {
62+
TokenStorage storage $ = _getS();
63+
return $.name;
64+
}
65+
66+
function symbol() public view virtual returns (string memory) {
67+
TokenStorage storage $ = _getS();
68+
return $.symbol;
69+
}
70+
71+
function decimals() public view virtual returns (uint8) {
72+
return 18;
73+
}
74+
75+
function totalSupply() public view virtual returns (uint256) {
76+
TokenStorage storage $ = _getS();
77+
return $.totalSupply;
78+
}
79+
80+
function balanceOf(address account) public view virtual returns (uint256) {
81+
TokenStorage storage $ = _getS();
82+
return $.balances[account];
83+
}
84+
85+
function allowance(address owner, address spender) public view virtual returns (uint256) {
86+
TokenStorage storage $ = _getS();
87+
return $.allowances[owner][spender];
88+
}
89+
90+
// External ERC-20 (can be overridden to restrict usage in child)
91+
function transfer(address to, uint256 value) public virtual returns (bool) {
92+
address owner = _msgSender();
93+
_transfer(owner, to, value);
94+
return true;
95+
}
96+
97+
function approve(address spender, uint256 value) public virtual returns (bool) {
98+
address owner = _msgSender();
99+
_approve(owner, spender, value);
100+
return true;
101+
}
102+
103+
function transferFrom(address from, address to, uint256 value) public virtual returns (bool) {
104+
address spender = _msgSender();
105+
_spendAllowance(from, spender, value);
106+
_transfer(from, to, value);
107+
return true;
108+
}
109+
110+
// Internal core
111+
function _transfer(address from, address to, uint256 value) internal {
112+
if (from == address(0)) {
113+
revert ERC20InvalidSender(address(0));
114+
}
115+
// Allow `to == address(0)` to support null-owner semantics
116+
_update(from, to, value);
117+
}
118+
119+
// Update balances without affecting total supply (supports from/to == address(0))
120+
function _update(address from, address to, uint256 value) internal virtual {
121+
TokenStorage storage $ = _getS();
122+
// Debit from
123+
uint256 fromBalance = $.balances[from];
124+
if (fromBalance < value) {
125+
revert ERC20InsufficientBalance(from, fromBalance, value);
126+
}
127+
unchecked {
128+
$.balances[from] = fromBalance - value;
129+
}
130+
// Credit to
131+
unchecked {
132+
$.balances[to] += value;
133+
}
134+
emit Transfer(from, to, value);
135+
}
136+
137+
// Mint (null-owner aware): increases totalSupply and credits recipient (can be address(0))
138+
function _mint(address account, uint256 value) internal {
139+
TokenStorage storage $ = _getS();
140+
141+
uint256 newSupply = $.totalSupply + value;
142+
if (newSupply > $.cap) {
143+
revert ERC20ExceededCap(newSupply, $.cap);
144+
}
145+
146+
$.totalSupply = newSupply;
147+
148+
unchecked { $.balances[account] += value; }
149+
150+
emit Transfer(address(0), account, value);
151+
}
152+
153+
// Approvals
154+
function _approve(address owner, address spender, uint256 value) internal {
155+
_approve(owner, spender, value, true);
156+
}
157+
158+
function _approve(address owner, address spender, uint256 value, bool emitEvent) internal virtual {
159+
TokenStorage storage $ = _getS();
160+
if (owner == address(0)) {
161+
revert ERC20InvalidApprover(address(0));
162+
}
163+
if (spender == address(0)) {
164+
revert ERC20InvalidSpender(address(0));
165+
}
166+
$.allowances[owner][spender] = value;
167+
if (emitEvent) {
168+
emit Approval(owner, spender, value);
169+
}
170+
}
171+
172+
function _spendAllowance(address owner, address spender, uint256 value) internal virtual {
173+
uint256 currentAllowance = allowance(owner, spender);
174+
if (currentAllowance < type(uint256).max) {
175+
if (currentAllowance < value) {
176+
revert ERC20InsufficientAllowance(spender, currentAllowance, value);
177+
}
178+
unchecked {
179+
_approve(owner, spender, currentAllowance - value, false);
180+
}
181+
}
182+
}
183+
184+
// Cap view
185+
function maxSupply() public view virtual returns (uint256) {
186+
TokenStorage storage $ = _getS();
187+
return $.cap;
188+
}
189+
}

contracts/src/protocols/EthscriptionERC721.sol renamed to contracts/src/EthscriptionERC721.sol

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,13 @@
22
pragma solidity 0.8.24;
33

44
// import "@openzeppelin/contracts-upgradeable/token/ERC721/ERC721Upgradeable.sol";
5-
import "../ERC721EthscriptionsUpgradeable.sol";
5+
import "./ERC721EthscriptionsUpgradeable.sol";
66
// import "@openzeppelin/contracts-upgradeable/token/ERC721/extensions/ERC721EnumerableUpgradeable.sol";
77
// import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
8-
import "../Ethscriptions.sol";
8+
import "./Ethscriptions.sol";
99
import {LibString} from "solady/utils/LibString.sol";
1010
import {Base64} from "solady/utils/Base64.sol";
11-
import "../CollectionsManager.sol";
11+
import "./CollectionsManager.sol";
1212

1313
/// @title EthscriptionERC721
1414
/// @notice ERC-721 contract for an Ethscription collection

contracts/src/Ethscriptions.sol

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import {LibString} from "solady/utils/LibString.sol";
88
import "./EthscriptionsProver.sol";
99
import "./libraries/Predeploys.sol";
1010
import "./L2/L1Block.sol";
11-
import "./protocols/IProtocolHandler.sol";
11+
import "./interfaces/IProtocolHandler.sol";
1212

1313
/// @title Ethscriptions ERC-721 Contract
1414
/// @notice Mints Ethscriptions as ERC-721 tokens based on L1 transaction data
Lines changed: 16 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
11
// SPDX-License-Identifier: MIT
22
pragma solidity 0.8.24;
33

4-
import "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol";
5-
import "@openzeppelin/contracts-upgradeable/token/ERC20/extensions/ERC20CappedUpgradeable.sol";
4+
import "./ERC20NullOwnerCappedUpgradeable.sol";
65
import "./libraries/Predeploys.sol";
76

8-
contract EthscriptionsERC20 is ERC20Upgradeable, ERC20CappedUpgradeable {
7+
/// @title EthscriptionsERC20
8+
/// @notice ERC20 with cap that supports null address ownership; only TokenManager can mint/transfer
9+
contract EthscriptionsERC20 is ERC20NullOwnerCappedUpgradeable {
910
address public constant tokenManager = Predeploys.TOKEN_MANAGER;
10-
1111
bytes32 public deployTxHash; // The ethscription hash that deployed this token
12-
12+
1313
function initialize(
1414
string memory name_,
1515
string memory symbol_,
@@ -20,52 +20,32 @@ contract EthscriptionsERC20 is ERC20Upgradeable, ERC20CappedUpgradeable {
2020
__ERC20Capped_init(cap_);
2121
deployTxHash = deployTxHash_;
2222
}
23-
23+
2424
modifier onlyTokenManager() {
2525
require(msg.sender == tokenManager, "Only TokenManager");
2626
_;
2727
}
28-
28+
29+
// TokenManager-only mint that allows to == address(0)
2930
function mint(address to, uint256 amount) external onlyTokenManager {
3031
_mint(to, amount);
3132
}
32-
33+
34+
// TokenManager-only transfer that allows to/from == address(0)
3335
function forceTransfer(address from, address to, uint256 amount) external onlyTokenManager {
34-
// This is used by TokenManager to shadow NFT transfers
35-
// It bypasses approval checks since it's a system-level transfer
36-
_transfer(from, to, amount);
36+
_update(from, to, amount);
3737
}
38-
39-
// Override transfer functions to prevent user-initiated transfers
40-
// Only the TokenManager can move tokens via forceTransfer
38+
39+
// Disable user-initiated ERC20 flows
4140
function transfer(address, uint256) public pure override returns (bool) {
4241
revert("Transfers only allowed via Ethscriptions NFT");
4342
}
44-
43+
4544
function transferFrom(address, address, uint256) public pure override returns (bool) {
4645
revert("Transfers only allowed via Ethscriptions NFT");
4746
}
48-
47+
4948
function approve(address, uint256) public pure override returns (bool) {
5049
revert("Approvals not allowed");
5150
}
52-
53-
function increaseAllowance(address, uint256) public pure returns (bool) {
54-
revert("Approvals not allowed");
55-
}
56-
57-
function decreaseAllowance(address, uint256) public pure returns (bool) {
58-
revert("Approvals not allowed");
59-
}
60-
61-
// Required overrides for multiple inheritance
62-
function _update(address from, address to, uint256 value)
63-
internal
64-
override(ERC20Upgradeable, ERC20CappedUpgradeable)
65-
{
66-
super._update(from, to, value);
67-
68-
// Token balance proving has been removed in favor of ethscription-only proving
69-
// Token balances can be derived from ethscription ownership and transfer history
70-
}
71-
}
51+
}

contracts/src/TokenManager.sol

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import {LibString} from "solady/utils/LibString.sol";
66
import "./EthscriptionsERC20.sol";
77
import "./Ethscriptions.sol";
88
import "./libraries/Predeploys.sol";
9-
import "./protocols/IProtocolHandler.sol";
9+
import "./interfaces/IProtocolHandler.sol";
1010

1111
contract TokenManager is IProtocolHandler {
1212
using Clones for address;
File renamed without changes.

contracts/test/CollectionsManager.t.sol

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ pragma solidity ^0.8.24;
33

44
import "./TestSetup.sol";
55
import "../src/CollectionsManager.sol";
6-
import "../src/protocols/EthscriptionERC721.sol";
6+
import "../src/EthscriptionERC721.sol";
77
import {LibString} from "solady/utils/LibString.sol";
88

99
contract CollectionsManagerTest is TestSetup {

0 commit comments

Comments
 (0)