-
Notifications
You must be signed in to change notification settings - Fork 25
Expand file tree
/
Copy pathERC404NullOwnerCappedUpgradeable.sol
More file actions
405 lines (327 loc) · 13.9 KB
/
Copy pathERC404NullOwnerCappedUpgradeable.sol
File metadata and controls
405 lines (327 loc) · 13.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
// 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";
import {IERC165} from "@openzeppelin/contracts/interfaces/IERC165.sol";
/// @title ERC404NullOwnerCappedUpgradeable
/// @notice Hybrid ERC20/ERC721 implementation with null owner support, supply cap, and upgradeability
/// @dev Combines ERC404 NFT functionality with null owner semantics and EIP-7201 namespaced storage
abstract contract ERC404NullOwnerCappedUpgradeable is
Initializable,
ContextUpgradeable,
IERC165,
IERC20,
IERC20Metadata,
IERC20Errors
{
struct TokenData {
address owner; // current owner (can be address(0) for null-owner)
uint88 index; // position in owned[owner] array
bool exists; // true if the token has been minted
}
// =============================================================
// STORAGE STRUCT
// =============================================================
/// @custom:storage-location erc7201:ethscriptions.storage.ERC404NullOwnerCapped
struct TokenStorage {
// === ERC20 State ===
mapping(address => uint256) balances;
mapping(address => mapping(address => uint256)) allowances;
uint256 totalSupply;
uint256 cap;
mapping(address => uint256[]) owned;
mapping(uint256 => TokenData) tokens;
mapping(uint256 => address) getApproved;
mapping(address => mapping(address => bool)) isApprovedForAll;
uint256 minted; // Number of NFTs minted
uint256 units; // Units for NFT minting (e.g., 1000 * 10^18)
// === Metadata ===
string name;
string symbol;
}
// =============================================================
// EVENTS
// =============================================================
// ERC20 Events are inherited from IERC20 (Transfer, Approval)
// ERC721 Events (using different names to avoid conflicts with ERC20)
event ERC721Transfer(address indexed from, address indexed to, uint256 indexed id);
// =============================================================
// CUSTOM ERRORS
// =============================================================
error UnsafeUpdate();
error ERC20ExceededCap(uint256 increasedSupply, uint256 cap);
error ERC20InvalidCap(uint256 cap);
error InvalidUnits(uint256 units);
error NotImplemented();
error NotFound();
error InvalidTokenId();
error AlreadyExists();
error InvalidRecipient();
error Unauthorized();
error OwnedIndexOverflow();
// =============================================================
// STORAGE ACCESSOR
// =============================================================
function _getS() internal pure returns (TokenStorage storage $) {
bytes32 slot = keccak256("ethscriptions.storage.ERC404NullOwnerCapped");
assembly {
$.slot := slot
}
}
// =============================================================
// INITIALIZERS
// =============================================================
function __ERC404_init(
string memory name_,
string memory symbol_,
uint256 cap_,
uint256 units_
) internal onlyInitializing {
__Context_init();
__ERC404_init_unchained(name_, symbol_, cap_, units_);
}
function __ERC404_init_unchained(
string memory name_,
string memory symbol_,
uint256 cap_,
uint256 units_
) internal onlyInitializing {
TokenStorage storage $ = _getS();
if (cap_ == 0) revert ERC20InvalidCap(cap_);
uint256 base = 10 ** decimals();
if (units_ == 0 || units_ % base != 0) revert InvalidUnits(units_);
$.name = name_;
$.symbol = symbol_;
$.cap = cap_;
$.units = units_;
}
// =============================================================
// ERC20 METADATA VIEWS
// =============================================================
function name() public view virtual override(IERC20Metadata) returns (string memory) {
TokenStorage storage $ = _getS();
return $.name;
}
function symbol() public view virtual override(IERC20Metadata) returns (string memory) {
TokenStorage storage $ = _getS();
return $.symbol;
}
function decimals() public pure override(IERC20Metadata) returns (uint8) {
return 18;
}
// =============================================================
// ERC20 VIEWS
// =============================================================
function totalSupply() public view virtual override returns (uint256) {
TokenStorage storage $ = _getS();
return $.totalSupply;
}
function balanceOf(address account) public view virtual override returns (uint256) {
TokenStorage storage $ = _getS();
return $.balances[account];
}
function balanceOf(address owner_, uint256 id_)
public
view
returns (uint256)
{
TokenStorage storage $ = _getS();
TokenData storage t = $.tokens[id_];
if (!t.exists) return 0;
return t.owner == owner_ ? 1 : 0;
}
function allowance(address owner, address spender) public view virtual override returns (uint256) {
TokenStorage storage $ = _getS();
return $.allowances[owner][spender];
}
function erc20TotalSupply() public view virtual returns (uint256) {
return totalSupply();
}
function erc20BalanceOf(address owner_) public view virtual returns (uint256) {
return balanceOf(owner_);
}
// =============================================================
// ERC721 VIEWS
// =============================================================
function erc721TotalSupply() public view virtual returns (uint256) {
TokenStorage storage $ = _getS();
return $.minted;
}
function erc721BalanceOf(address owner_) public view virtual returns (uint256) {
TokenStorage storage $ = _getS();
return $.owned[owner_].length;
}
function ownerOf(uint256 id_) public view virtual returns (address) {
_validateTokenId(id_);
TokenStorage storage $ = _getS();
TokenData storage t = $.tokens[id_];
if (!t.exists) revert NotFound();
return t.owner;
}
function owned(address owner_) public view virtual returns (uint256[] memory) {
TokenStorage storage $ = _getS();
return $.owned[owner_];
}
function getApproved(uint256 id_) public view virtual returns (address) {
_validateTokenId(id_);
TokenStorage storage $ = _getS();
if (!$.tokens[id_].exists) revert NotFound();
return $.getApproved[id_];
}
function isApprovedForAll(address owner_, address operator_) public view virtual returns (bool) {
TokenStorage storage $ = _getS();
return $.isApprovedForAll[owner_][operator_];
}
// =============================================================
// OTHER VIEWS
// =============================================================
function maxSupply() public view virtual returns (uint256) {
TokenStorage storage $ = _getS();
return $.cap;
}
function units() public view virtual returns (uint256) {
TokenStorage storage $ = _getS();
return $.units;
}
/// @notice Fixed denomination in whole-token units (e.g., 1000 if 1 NFT = 1000 tokens)
function denomination() public view virtual returns (uint256) {
return units() / (10 ** decimals());
}
/// @notice tokenURI must be implemented by child contract
function tokenURI(uint256 id_) public view virtual returns (string memory);
// =============================================================
// ERC20 OPERATIONS
// =============================================================
function transfer(address, uint256) public pure virtual override returns (bool) {
revert NotImplemented();
}
function approve(address, uint256) public pure virtual override returns (bool) {
revert NotImplemented();
}
function transferFrom(address, address, uint256) public pure virtual override returns (bool) {
revert NotImplemented();
}
function erc20Approve(address, uint256) public pure virtual returns (bool) {
revert NotImplemented();
}
function erc20TransferFrom(address, address, uint256) public pure virtual returns (bool) {
revert NotImplemented();
}
// =============================================================
// ERC721 OPERATIONS
// =============================================================
function erc721Approve(address, uint256) public pure virtual {
revert NotImplemented();
}
function erc721TransferFrom(address, address, uint256) public pure virtual {
revert NotImplemented();
}
function setApprovalForAll(address, bool) public pure virtual {
revert NotImplemented();
}
function safeTransferFrom(address, address, uint256) public pure virtual {
revert NotImplemented();
}
function safeTransferFrom(address, address, uint256, bytes memory) public pure virtual {
revert NotImplemented();
}
/// @notice Low-level ERC20 transfer
/// @dev Supports transfers to/from address(0) for null owner support
function _transferERC20(address from_, address to_, uint256 value_) internal virtual {
TokenStorage storage $ = _getS();
if (from_ == address(0)) {
// Minting with cap enforcement
uint256 newSupply = $.totalSupply + value_;
if (newSupply > $.cap) {
revert ERC20ExceededCap(newSupply, $.cap);
}
$.totalSupply = newSupply;
} else {
// Transfer
uint256 fromBalance = $.balances[from_];
if (fromBalance < value_) {
revert ERC20InsufficientBalance(from_, fromBalance, value_);
}
unchecked {
$.balances[from_] = fromBalance - value_;
}
}
unchecked {
$.balances[to_] += value_;
}
emit Transfer(from_, to_, value_);
}
/// @notice Transfer an ERC721 token
function _transferERC721(address from_, address to_, uint256 id_) internal virtual {
TokenStorage storage $ = _getS();
TokenData storage t = $.tokens[id_];
if (!t.exists) revert NotFound();
if (from_ != t.owner) revert Unauthorized();
if (from_ != address(0)) {
// Clear approval
delete $.getApproved[id_];
// Remove from sender's owned list
uint256 lastTokenId = $.owned[from_][$.owned[from_].length - 1];
if (lastTokenId != id_) {
uint256 updatedIndex = t.index;
$.owned[from_][updatedIndex] = lastTokenId;
$.tokens[lastTokenId].index = uint88(updatedIndex);
}
$.owned[from_].pop();
}
// Add to receiver's owned list (address(0) is a real owner in null-owner semantics)
uint256 newIndex = $.owned[to_].length;
if (newIndex > type(uint88).max) {
revert OwnedIndexOverflow();
}
t.owner = to_;
t.index = uint88(newIndex);
$.owned[to_].push(id_);
emit ERC721Transfer(from_, to_, id_);
}
/// @notice Mint ERC20 tokens without triggering NFT creation
/// @dev Used for fixed denomination tokens where NFTs are explicitly minted
function _mintERC20WithoutNFT(address to_, uint256 value_) internal virtual {
// Direct ERC20 mint without NFT logic (cap enforced in _transferERC20)
_transferERC20(address(0), to_, value_);
}
/// @notice Mint a specific NFT with a given ID
/// @dev Used for fixed denomination tokens to mint NFTs with specific mintIds
function _mintERC721(address to_, uint256 nftId_) internal virtual {
if (to_ == address(0)) {
revert InvalidRecipient();
}
_validateTokenId(nftId_);
TokenStorage storage $ = _getS();
TokenData storage t = $.tokens[nftId_];
// Check if this NFT already exists (including null-owner)
if (t.exists) {
revert AlreadyExists();
}
t.exists = true;
_transferERC721(address(0), to_, nftId_);
// Increment minted supply counter
$.minted++;
}
// =============================================================
// HELPER FUNCTIONS
// =============================================================
/// @dev Simple tokenId validation: nonzero and not max uint256.
function _validateTokenId(uint256 id_) internal pure {
if (id_ == 0 || id_ == type(uint256).max) {
revert InvalidTokenId();
}
}
// =============================================================
// ERC165 SUPPORT
// =============================================================
function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) {
return
interfaceId == type(IERC165).interfaceId ||
interfaceId == type(IERC20).interfaceId ||
interfaceId == type(IERC20Metadata).interfaceId;
}
}