|
1 | 1 | // SPDX-License-Identifier: MIT |
2 | 2 | pragma solidity 0.8.24; |
3 | 3 |
|
4 | | -import "./ERC20NullOwnerCappedUpgradeable.sol"; |
| 4 | +import "./ERC404NullOwnerCappedUpgradeable.sol"; |
5 | 5 | import "./libraries/Predeploys.sol"; |
| 6 | +import "./Ethscriptions.sol"; |
| 7 | +import "./ERC20FixedDenominationManager.sol"; |
| 8 | +import {LibString} from "solady/utils/LibString.sol"; |
| 9 | +import {Base64} from "solady/utils/Base64.sol"; |
6 | 10 |
|
7 | 11 | /// @title ERC20FixedDenomination |
8 | | -/// @notice ERC-20 proxy whose supply is managed in a fixed denomination by the manager contract. |
| 12 | +/// @notice Hybrid ERC-20/ERC-721 proxy whose supply is managed in fixed denominations by the manager contract. |
9 | 13 | /// @dev User-initiated transfers/approvals are disabled; only the manager can mutate balances. |
10 | | -contract ERC20FixedDenomination is ERC20NullOwnerCappedUpgradeable { |
| 14 | +/// Each NFT represents a fixed denomination amount (e.g., 1 NFT = mintAmount tokens). |
| 15 | +contract ERC20FixedDenomination is ERC404NullOwnerCappedUpgradeable { |
| 16 | + using LibString for *; |
11 | 17 |
|
12 | 18 | // ============================================================= |
13 | 19 | // CONSTANTS |
@@ -48,36 +54,153 @@ contract ERC20FixedDenomination is ERC20NullOwnerCappedUpgradeable { |
48 | 54 | string memory name_, |
49 | 55 | string memory symbol_, |
50 | 56 | uint256 cap_, |
| 57 | + uint256 mintAmount_, |
51 | 58 | bytes32 deployEthscriptionId_ |
52 | 59 | ) external initializer { |
53 | | - __ERC20_init(name_, symbol_); |
54 | | - __ERC20Capped_init(cap_); |
| 60 | + // cap_ is maxSupply * 10**18 |
| 61 | + // mintAmount_ is the denomination amount (e.g., 1000 for 1000 tokens per NFT) |
| 62 | + // units is mintAmount_ * 10**18 (amount of wei per NFT) |
| 63 | + |
| 64 | + uint256 units_ = mintAmount_ * (10 ** decimals()); |
| 65 | + |
| 66 | + __ERC404_init(name_, symbol_, cap_, units_); |
55 | 67 | deployEthscriptionId = deployEthscriptionId_; |
56 | 68 | } |
57 | 69 |
|
58 | | - /// @notice Mint tokens (manager only) |
59 | | - function mint(address to, uint256 amount) external onlyManager { |
60 | | - _mint(to, amount); |
| 70 | + /// @notice Historical accessor for the fixed denomination (whole tokens per NFT) |
| 71 | + function mintAmount() public view returns (uint256) { |
| 72 | + return denomination(); |
| 73 | + } |
| 74 | + |
| 75 | + /// @notice Mint one fixed-denomination note (manager only) |
| 76 | + /// @param to The recipient address |
| 77 | + /// @param nftId The specific NFT ID to mint (the mintId) |
| 78 | + function mint(address to, uint256 nftId) external onlyManager { |
| 79 | + // Mint the ERC20 tokens without triggering NFT creation |
| 80 | + _mintERC20WithoutNFT(to, units()); |
| 81 | + _mintERC721(to, nftId); |
61 | 82 | } |
62 | 83 |
|
63 | | - /// @notice Force transfer tokens (manager only) |
64 | | - function forceTransfer(address from, address to, uint256 amount) external onlyManager { |
65 | | - _update(from, to, amount); |
| 84 | + /// @notice Force transfer the fixed-denomination NFT and its synced ERC20 lot (manager only) |
| 85 | + /// @param from The sender address |
| 86 | + /// @param to The recipient address |
| 87 | + /// @param nftId The NFT ID to transfer (the mintId) |
| 88 | + function forceTransfer(address from, address to, uint256 nftId) external onlyManager { |
| 89 | + // Transfer the ERC20 tokens without triggering dynamic NFT logic |
| 90 | + _transferERC20(from, to, units()); |
| 91 | + |
| 92 | + // Transfer the specific NFT using the proper function |
| 93 | + uint256 id = ID_ENCODING_PREFIX + nftId; |
| 94 | + _transferERC721(from, to, id); |
66 | 95 | } |
67 | 96 |
|
68 | 97 | // ============================================================= |
69 | | - // DISABLED ERC20 FUNCTIONS |
| 98 | + // DISABLED ERC20/721 FUNCTIONS |
70 | 99 | // ============================================================= |
71 | 100 |
|
| 101 | + /// @notice Regular transfers are disabled - only manager can transfer |
72 | 102 | function transfer(address, uint256) public pure override returns (bool) { |
73 | 103 | revert TransfersOnlyViaEthscriptions(); |
74 | 104 | } |
75 | 105 |
|
| 106 | + /// @notice Regular transferFrom is disabled - only manager can transfer |
76 | 107 | function transferFrom(address, address, uint256) public pure override returns (bool) { |
77 | 108 | revert TransfersOnlyViaEthscriptions(); |
78 | 109 | } |
79 | 110 |
|
| 111 | + /// @notice Approvals are disabled |
80 | 112 | function approve(address, uint256) public pure override returns (bool) { |
81 | 113 | revert ApprovalsNotAllowed(); |
82 | 114 | } |
| 115 | + |
| 116 | + /// @notice ERC721 approvals are disabled |
| 117 | + function erc721Approve(address, uint256) public pure override { |
| 118 | + revert ApprovalsNotAllowed(); |
| 119 | + } |
| 120 | + |
| 121 | + /// @notice ERC20 approvals are disabled |
| 122 | + function erc20Approve(address, uint256) public pure override returns (bool) { |
| 123 | + revert ApprovalsNotAllowed(); |
| 124 | + } |
| 125 | + |
| 126 | + /// @notice SetApprovalForAll is disabled |
| 127 | + function setApprovalForAll(address, bool) public pure override { |
| 128 | + revert ApprovalsNotAllowed(); |
| 129 | + } |
| 130 | + |
| 131 | + /// @notice ERC721 transferFrom is disabled |
| 132 | + function erc721TransferFrom(address, address, uint256) public pure override { |
| 133 | + revert TransfersOnlyViaEthscriptions(); |
| 134 | + } |
| 135 | + |
| 136 | + /// @notice ERC20 transferFrom is disabled |
| 137 | + function erc20TransferFrom(address, address, uint256) public pure override returns (bool) { |
| 138 | + revert TransfersOnlyViaEthscriptions(); |
| 139 | + } |
| 140 | + |
| 141 | + /// @notice Safe transfers are disabled |
| 142 | + function safeTransferFrom(address, address, uint256) public pure override { |
| 143 | + revert TransfersOnlyViaEthscriptions(); |
| 144 | + } |
| 145 | + |
| 146 | + /// @notice Safe transfers with data are disabled |
| 147 | + function safeTransferFrom(address, address, uint256, bytes memory) public pure override { |
| 148 | + revert TransfersOnlyViaEthscriptions(); |
| 149 | + } |
| 150 | + |
| 151 | + // ============================================================= |
| 152 | + // TOKEN URI |
| 153 | + // ============================================================= |
| 154 | + |
| 155 | + /// @notice Returns metadata URI for NFT tokens |
| 156 | + /// @dev Returns a data URI with JSON metadata fetched from the main Ethscriptions contract |
| 157 | + function tokenURI(uint256 id_) public view virtual override returns (string memory) { |
| 158 | + // This will revert InvalidTokenId / NotFound on bad ids |
| 159 | + ownerOf(id_); |
| 160 | + |
| 161 | + uint256 mintId = id_ & ~ID_ENCODING_PREFIX; |
| 162 | + |
| 163 | + // Get the ethscriptionId for this mintId from the manager |
| 164 | + ERC20FixedDenominationManager mgr = ERC20FixedDenominationManager(manager); |
| 165 | + bytes32 ethscriptionId = mgr.getMintEthscriptionId(deployEthscriptionId, mintId); |
| 166 | + |
| 167 | + if (ethscriptionId == bytes32(0)) { |
| 168 | + // If no ethscription found, return minimal metadata |
| 169 | + return string(abi.encodePacked( |
| 170 | + "data:application/json;utf8,", |
| 171 | + '{"name":"', name(), ' Note #', mintId.toString(), '",', |
| 172 | + '"description":"Denomination note for ', mintAmount().toString(), ' tokens"}' |
| 173 | + )); |
| 174 | + } |
| 175 | + |
| 176 | + // Get the ethscription data from the main contract |
| 177 | + Ethscriptions ethscriptionsContract = Ethscriptions(Predeploys.ETHSCRIPTIONS); |
| 178 | + Ethscriptions.Ethscription memory ethscription = ethscriptionsContract.getEthscription(ethscriptionId, false); |
| 179 | + (string memory mediaType, string memory mediaUri) = ethscriptionsContract.getMediaUri(ethscriptionId); |
| 180 | + |
| 181 | + // Convert ethscriptionId to hex string (0x prefixed) |
| 182 | + string memory ethscriptionIdHex = uint256(ethscriptionId).toHexString(32); |
| 183 | + |
| 184 | + // Build the JSON metadata |
| 185 | + string memory jsonStart = string.concat( |
| 186 | + '{"name":"', name(), ' Note #', mintId.toString(), '"', |
| 187 | + ',"description":"Fixed denomination token for ', mintAmount().toString(), ' ', symbol(), ' tokens"' |
| 188 | + ); |
| 189 | + |
| 190 | + // Add ethscription ID and number |
| 191 | + string memory ethscriptionFields = string.concat( |
| 192 | + ',"ethscription_id":"', ethscriptionIdHex, '"', |
| 193 | + ',"ethscription_number":', ethscription.ethscriptionNumber.toString() |
| 194 | + ); |
| 195 | + |
| 196 | + // Add media field |
| 197 | + string memory mediaField = string.concat( |
| 198 | + ',"', mediaType, '":"', mediaUri, '"' |
| 199 | + ); |
| 200 | + |
| 201 | + string memory json = string.concat(jsonStart, ethscriptionFields, mediaField, '}'); |
| 202 | + |
| 203 | + return string.concat("data:application/json;base64,", Base64.encode(bytes(json))); |
| 204 | + } |
| 205 | + |
83 | 206 | } |
0 commit comments