Skip to content

Commit 6b89550

Browse files
Merge pull request #146 from ethscriptions-protocol/fix_collections
Refactor ERC20FixedDenomination and Manager for Hybrid NFT Support
2 parents 9fa81fa + 8d4e965 commit 6b89550

File tree

7 files changed

+1426
-147
lines changed

7 files changed

+1426
-147
lines changed

contracts/src/ERC20FixedDenomination.sol

Lines changed: 135 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,19 @@
11
// SPDX-License-Identifier: MIT
22
pragma solidity 0.8.24;
33

4-
import "./ERC20NullOwnerCappedUpgradeable.sol";
4+
import "./ERC404NullOwnerCappedUpgradeable.sol";
55
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";
610

711
/// @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.
913
/// @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 *;
1117

1218
// =============================================================
1319
// CONSTANTS
@@ -48,36 +54,153 @@ contract ERC20FixedDenomination is ERC20NullOwnerCappedUpgradeable {
4854
string memory name_,
4955
string memory symbol_,
5056
uint256 cap_,
57+
uint256 mintAmount_,
5158
bytes32 deployEthscriptionId_
5259
) 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_);
5567
deployEthscriptionId = deployEthscriptionId_;
5668
}
5769

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);
6182
}
6283

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);
6695
}
6796

6897
// =============================================================
69-
// DISABLED ERC20 FUNCTIONS
98+
// DISABLED ERC20/721 FUNCTIONS
7099
// =============================================================
71100

101+
/// @notice Regular transfers are disabled - only manager can transfer
72102
function transfer(address, uint256) public pure override returns (bool) {
73103
revert TransfersOnlyViaEthscriptions();
74104
}
75105

106+
/// @notice Regular transferFrom is disabled - only manager can transfer
76107
function transferFrom(address, address, uint256) public pure override returns (bool) {
77108
revert TransfersOnlyViaEthscriptions();
78109
}
79110

111+
/// @notice Approvals are disabled
80112
function approve(address, uint256) public pure override returns (bool) {
81113
revert ApprovalsNotAllowed();
82114
}
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+
83206
}

0 commit comments

Comments
 (0)