Skip to content

Commit 124b55e

Browse files
Merge pull request #142 from ethscriptions-protocol/fix_collections
Enhance ERC721 Contracts with Base64 Metadata Encoding
2 parents c6d238e + 64230ae commit 124b55e

9 files changed

+660
-24
lines changed

app/models/erc721_ethscriptions_collection_parser.rb

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,21 @@ class Erc721EthscriptionsCollectionParser
5353
'item' => :single_item
5454
}
5555
},
56+
'transfer_ownership' => {
57+
keys: %w[collection_id new_owner],
58+
abi_type: '(bytes32,address)',
59+
validators: {
60+
'collection_id' => :bytes32,
61+
'new_owner' => :address
62+
}
63+
},
64+
'renounce_ownership' => {
65+
keys: %w[collection_id],
66+
abi_type: 'bytes32',
67+
validators: {
68+
'collection_id' => :bytes32
69+
}
70+
},
5671
'remove_items' => {
5772
keys: %w[collection_id ethscription_ids],
5873
abi_type: '(bytes32,bytes32[])',
@@ -399,6 +414,10 @@ def encode_operation(operation, data, schema, content_hash: nil)
399414
build_create_and_add_self_values(validated_data, content_hash: content_hash)
400415
when 'add_self_to_collection'
401416
build_add_self_to_collection_values(validated_data, content_hash: content_hash)
417+
when 'transfer_ownership'
418+
build_transfer_ownership_values(validated_data)
419+
when 'renounce_ownership'
420+
build_renounce_ownership_values(validated_data)
402421
when 'remove_items'
403422
build_remove_items_values(validated_data)
404423
when 'edit_collection'
@@ -488,6 +507,18 @@ def validate_bytes32(value, field_name)
488507
[value[2..]].pack('H*')
489508
end
490509

510+
def validate_address(value, field_name)
511+
unless value.is_a?(String) && value.match?(/\A0x[0-9a-f]{40}\z/)
512+
raise ValidationError, "Invalid address for #{field_name}: #{value}"
513+
end
514+
515+
if value == '0x0000000000000000000000000000000000000000'
516+
raise ValidationError, "Address cannot be zero for #{field_name}"
517+
end
518+
519+
value.downcase
520+
end
521+
491522
def validate_bytes32_array(value, field_name)
492523
unless value.is_a?(Array)
493524
raise ValidationError, "Expected array for #{field_name}"
@@ -675,6 +706,14 @@ def build_add_self_to_collection_values(data, content_hash:)
675706
[data['collection_id'], item_tuple]
676707
end
677708

709+
def build_transfer_ownership_values(data)
710+
[data['collection_id'], data['new_owner']]
711+
end
712+
713+
def build_renounce_ownership_values(data)
714+
data['collection_id']
715+
end
716+
678717
def build_remove_items_values(data)
679718
[data['collection_id'], data['ethscription_ids']]
680719
end

contracts/src/ERC721EthscriptionsCollection.sol

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,7 @@ contract ERC721EthscriptionsCollection is ERC721EthscriptionsEnumerableUpgradeab
124124
',"',
125125
mediaType,
126126
'":"',
127-
mediaUri.escapeJSON(),
127+
mediaUri,
128128
'"'
129129
);
130130

@@ -182,7 +182,7 @@ contract ERC721EthscriptionsCollection is ERC721EthscriptionsEnumerableUpgradeab
182182
'"}'
183183
);
184184

185-
return json;
185+
return string.concat("data:application/json;base64,", Base64.encode(bytes(json)));
186186
}
187187

188188
function safeTransferFrom(address, address, uint256, bytes memory)

contracts/src/ERC721EthscriptionsCollectionManager.sol

Lines changed: 29 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,7 @@ contract ERC721EthscriptionsCollectionManager is IProtocolHandler {
152152
event ItemsRemoved(bytes32 indexed collectionId, uint256 count, bytes32 updateTxHash);
153153
event CollectionEdited(bytes32 indexed collectionId);
154154
event CollectionLocked(bytes32 indexed collectionId);
155+
event OwnershipTransferred(bytes32 indexed collectionId, address indexed previousOwner, address indexed newOwner);
155156

156157
modifier onlyEthscriptions() {
157158
require(msg.sender == address(ethscriptions), "Only Ethscriptions contract");
@@ -184,6 +185,17 @@ contract ERC721EthscriptionsCollectionManager is IProtocolHandler {
184185
_addSingleItem(op.collectionId, ethscriptionId, op.item);
185186
}
186187

188+
function op_transfer_ownership(bytes32 ethscriptionId, bytes calldata data) external onlyEthscriptions {
189+
(bytes32 collectionId, address newOwner) = abi.decode(data, (bytes32, address));
190+
require(newOwner != address(0), "New owner cannot be zero address");
191+
_transferCollectionOwnership(ethscriptionId, collectionId, newOwner);
192+
}
193+
194+
function op_renounce_ownership(bytes32 ethscriptionId, bytes calldata data) external onlyEthscriptions {
195+
bytes32 collectionId = abi.decode(data, (bytes32));
196+
_transferCollectionOwnership(ethscriptionId, collectionId, address(0));
197+
}
198+
187199
function op_remove_items(bytes32 ethscriptionId, bytes calldata data) external onlyEthscriptions {
188200
RemoveItemsOperation memory removeOp = abi.decode(data, (RemoveItemsOperation));
189201
CollectionRecord storage collection = collectionStore[removeOp.collectionId];
@@ -266,12 +278,6 @@ contract ERC721EthscriptionsCollectionManager is IProtocolHandler {
266278
}
267279

268280
function onTransfer(bytes32 ethscriptionId, address from, address to) external override onlyEthscriptions {
269-
if (collectionExists(ethscriptionId)) {
270-
ERC721EthscriptionsCollection collection = ERC721EthscriptionsCollection(collectionStore[ethscriptionId].collectionContract);
271-
collection.factoryTransferOwnership(to);
272-
return;
273-
}
274-
275281
Membership storage membership = membershipOfEthscription[ethscriptionId];
276282

277283
if (!collectionExists(membership.collectionId)) {
@@ -454,6 +460,23 @@ contract ERC721EthscriptionsCollectionManager is IProtocolHandler {
454460
require(currentOwner == sender, errorMessage);
455461
}
456462

463+
function _transferCollectionOwnership(bytes32 ethscriptionId, bytes32 collectionId, address newOwner) private {
464+
CollectionRecord storage collection = collectionStore[collectionId];
465+
require(collection.collectionContract != address(0), "Collection does not exist");
466+
467+
address sender = _getEthscriptionCreator(ethscriptionId);
468+
ERC721EthscriptionsCollection collectionContract = ERC721EthscriptionsCollection(collection.collectionContract);
469+
address currentOwner = collectionContract.owner();
470+
require(currentOwner == sender, "Only collection owner can transfer");
471+
472+
if (newOwner == currentOwner) {
473+
revert("New owner must differ");
474+
}
475+
476+
collectionContract.factoryTransferOwnership(newOwner);
477+
emit OwnershipTransferred(collectionId, currentOwner, newOwner);
478+
}
479+
457480
function _verifyItemMerkleProof(ItemData memory item, bytes32 merkleRoot) private pure {
458481
require(merkleRoot != bytes32(0), "Merkle proof required");
459482

@@ -488,4 +511,3 @@ contract ERC721EthscriptionsCollectionManager is IProtocolHandler {
488511
return getCollection(collectionId);
489512
}
490513
}
491-

contracts/src/NameRegistry.sol

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

4-
import "@openzeppelin/contracts/utils/Strings.sol";
54
import {Base64} from "solady/utils/Base64.sol";
65
import {LibString} from "solady/utils/LibString.sol";
76
import "./ERC721EthscriptionsEnumerableUpgradeable.sol";
87
import "./interfaces/IProtocolHandler.sol";
98
import "./Ethscriptions.sol";
109
import "./libraries/Predeploys.sol";
10+
import {IERC721} from "@openzeppelin/contracts/token/ERC721/IERC721.sol";
1111

1212
/// @title NameRegistry
1313
/// @notice Handles legacy word-domain registrations and mirrors ownership as an ERC-721 collection.
1414
contract NameRegistry is ERC721EthscriptionsEnumerableUpgradeable, IProtocolHandler {
15-
using Strings for uint256;
15+
using LibString for *;
1616

1717
Ethscriptions public constant ethscriptions = Ethscriptions(Predeploys.ETHSCRIPTIONS);
1818

19-
string public constant PROTOCOL_NAME = "word-domains";
19+
string public constant protocolName = "word-domains";
2020

2121
struct DomainRecord {
2222
bytes32 packedName;
@@ -62,10 +62,6 @@ contract NameRegistry is ERC721EthscriptionsEnumerableUpgradeable, IProtocolHand
6262
return "NAME";
6363
}
6464

65-
function protocolName() external pure returns (string memory) {
66-
return PROTOCOL_NAME;
67-
}
68-
6965
// ============================
7066
// Protocol handler functions
7167
// ============================
@@ -192,27 +188,90 @@ contract NameRegistry is ERC721EthscriptionsEnumerableUpgradeable, IProtocolHand
192188
// Get the ethscription data to extract the ethscription number
193189
Ethscriptions.Ethscription memory ethscription = ethscriptions.getEthscription(record.ethscriptionId, false);
194190

191+
// Get the media URI from the ethscription
192+
(string memory mediaType, string memory mediaUri) = ethscriptions.getMediaUri(record.ethscriptionId);
193+
195194
// Convert ethscriptionId to hex string (0x prefixed)
196195
string memory ethscriptionIdHex = uint256(record.ethscriptionId).toHexString(32);
197196

198197
bytes memory json = abi.encodePacked(
199198
'{"name":"',
200-
name,
199+
name.escapeJSON(),
201200
'","description":"Dotless word domain"',
202201
',"ethscription_id":"',
203202
ethscriptionIdHex,
204203
'","ethscription_number":',
205204
ethscription.ethscriptionNumber.toString(),
206-
',"attributes":[',
205+
',"',
206+
mediaType,
207+
'":"',
208+
mediaUri,
209+
'","attributes":[',
207210
'{"trait_type":"Name","value":"',
208-
name,
211+
name.escapeJSON(),
209212
'"}',
210213
']}'
211214
);
212215

213216
return string(abi.encodePacked("data:application/json;base64,", Base64.encode(json)));
214217
}
215218

219+
/// @notice OpenSea collection-level metadata
220+
/// @return JSON string with collection metadata
221+
function contractURI() external pure returns (string memory) {
222+
return string(abi.encodePacked(
223+
'data:application/json;base64,',
224+
Base64.encode(bytes(
225+
'{"name":"Word Domains Registry",'
226+
'"description":"On-chain word domain name system for Ethscriptions. Register unique, dotless domain names as NFTs.",'
227+
'"image":"data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTAwIiBoZWlnaHQ9IjUwMCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cmVjdCB3aWR0aD0iNTAwIiBoZWlnaHQ9IjUwMCIgZmlsbD0iIzEwMTAxMCIvPjx0ZXh0IHg9IjI1MCIgeT0iMjUwIiBmb250LXNpemU9IjgwIiBmb250LWZhbWlseT0ibW9ub3NwYWNlIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmaWxsPSIjMDBmZjAwIj5bTkFNRVNdPC90ZXh0Pjwvc3ZnPg==",'
228+
'"external_link":"https://ethscriptions.com"}'
229+
))
230+
));
231+
}
232+
233+
// --- Transfer/approvals blocked externally ---------------------------------
234+
235+
function transferFrom(address, address, uint256)
236+
public
237+
pure
238+
override(ERC721EthscriptionsUpgradeable, IERC721)
239+
{
240+
revert TransfersDisabled();
241+
}
242+
243+
function safeTransferFrom(address, address, uint256)
244+
public
245+
pure
246+
override(ERC721EthscriptionsUpgradeable, IERC721)
247+
{
248+
revert TransfersDisabled();
249+
}
250+
251+
function safeTransferFrom(address, address, uint256, bytes memory)
252+
public
253+
pure
254+
override(ERC721EthscriptionsUpgradeable, IERC721)
255+
{
256+
revert TransfersDisabled();
257+
}
258+
259+
function approve(address, uint256)
260+
public
261+
pure
262+
override(ERC721EthscriptionsUpgradeable, IERC721)
263+
{
264+
revert TransfersDisabled();
265+
}
266+
267+
function setApprovalForAll(address, bool)
268+
public
269+
pure
270+
override(ERC721EthscriptionsUpgradeable, IERC721)
271+
{
272+
revert TransfersDisabled();
273+
}
274+
216275
function _update(address to, uint256 tokenId, address auth) internal override returns (address) {
217276
if (auth != address(0) && auth != address(this)) {
218277
revert TransfersDisabled();

contracts/test/CollectionURIResolution.t.sol

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

44
import "./TestSetup.sol";
55
import {LibString} from "solady/utils/LibString.sol";
6+
import {Base64} from "solady/utils/Base64.sol";
67

78
contract CollectionURIResolutionTest is TestSetup {
89
using LibString for *;
@@ -33,7 +34,18 @@ contract CollectionURIResolutionTest is TestSetup {
3334
string memory contractUri = collection.contractURI();
3435

3536
assertTrue(bytes(contractUri).length > 0, "Should have contractURI");
36-
assertTrue(contractUri.contains(regularUri), "Should contain original URI");
37+
38+
// contractURI returns base64-encoded JSON, decode it
39+
// Check it starts with data URI prefix
40+
string memory prefix = "data:application/json;base64,";
41+
assertTrue(contractUri.startsWith(prefix), "Should be a data URI");
42+
43+
// Extract and decode the base64 part
44+
string memory base64Part = contractUri.slice(bytes(prefix).length);
45+
bytes memory decodedBytes = Base64.decode(base64Part);
46+
string memory decodedJson = string(decodedBytes);
47+
48+
assertTrue(decodedJson.contains(regularUri), "Should contain original URI");
3749
}
3850

3951
function test_DataURIPassesThrough() public {

0 commit comments

Comments
 (0)