Nos contrats Splice couvrent deux cas d’utilisation majeurs. Premièrement, le contrat Splice lui-même est un NFT compatible ERC721 qui suit la propriété des épissures fabriquées. Deuxièmement, Splice’s modes sont gérés par un autre contrat (Splice Style NFT) qui contrôle la propriété des styles et stocke les emplacements fixes du code de style et de ses métadonnées. Chaque style NFT applique des règles telles que le nombre d’épissures d’un style pouvant être frappées, si la frappe est limitée à certaines collections d’origine, si un monnayeur a des droits de frappe exclusifs et, plus important encore, le prix d’un atelier.
Pour séparer les préoccupations, nous avons décidé de diviser l’architecture Splice en trois contrats : Splice, Style et PriceStrategy (Static). Nous connectons tous ces contrats pendant le déploiement / le processus de création de style en gardant les propriétés des membres saisies comme contrat composé.
contract Splice { SpliceStyleNFT public styleNFT; function setStyleNFT(SpliceStyleNFT _styleNFT) public onlyOwner {
styleNFT = _styleNFT;
} function quote(IERC721 nft, uint32 style_token_id)
public view
returns (uint256 fee) {
return styleNFT.quoteFee(nft, style_token_id);
}
}contract SpliceStyleNFT { address public spliceNFT; function setSplice(address _spliceNFT) external onlyOwner {
spliceNFT = _spliceNFT;
} function quoteFee(IERC721 nft, uint32 style_token_id)
external
view
returns (uint256 fee) {
fee = styleSettings[style_token_id].priceStrategy.quote(
this,
nft,
style_token_id,
styleSettings[style_token_id]
);
}
function mint(
...
ISplicePriceStrategy _priceStrategy,
bytes32 _priceStrategyParameters
) {
styleSettings[style_token_id] = StyleSettings({
priceStrategy: _priceStrategy,
priceParameters: _priceStrategyParameters
})
}}struct StyleSettings {
uint32 mintedOfStyle;
uint32 cap;
ISplicePriceStrategy priceStrategy;
bytes32 priceParameters;
bool salesIsActive;
bool collectionConstrained;
bool isFrozen;
string styleCID;
}contract SplicePriceStrategyStatic { function quote(
SpliceStyleNFT styleNFT,
IERC721 collection,
uint256 token_id,
StyleSettings memory styleSettings
) external pure override returns (uint256) {
return uint256(styleSettings.priceParameters);
}}
La séparation des préoccupations dans Solidity peut être un défi architectural car une fois les contrats déployés, ils ne peuvent généralement pas être modifiés. Notre priceStrategy semble inutile à première vue car il renvoie simplement les frais statiques qu’un monteur de style a fournis pendant le processus de frappe. La raison pour laquelle nous l’avons construit de cette façon est due à des hypothèses prospectives. La conception des contrats intelligents doit être suffisamment ouverte pour être prolongée dès le premier jour, car c’est un cauchemar de les changer une fois qu’ils portent l’état et sont utilisés dans la nature.
Notre Stratégie de prix l’abstraction permet d’ajouter des calculs de prix beaucoup plus sophistiqués à l’avenir, sans que nous ayons besoin de modifier quoi que ce soit sur les contrats existants. Ses implémentations reçoivent suffisamment d’informations pour permettre des stratégies de tarification complexes et arbitraires. Une simple extension pourrait être un algorithme de frais de frappe à liaison linéaire qui augmente légèrement les frais avec chaque atelier Splice, ou une vente aux enchères néerlandaise qui fait varier les frais en fonction du temps écoulé depuis la dernière frappe.
S’assurer que quelqu’un possède une origine est assez simple, vous transmettez simplement l’adresse de la collection d’origine à un IERC721 et vérifiez que son ownerOf méthode donne msg.sender pour l’ID de jeton entrant :
function mint(
IERC721[] memory origin_collections,
uint256[] memory origin_token_ids,
uint32 style_token_id,
bytes32[] memory allowlistProof,
bytes calldata input_params
) {
for (uint256 i = 0; i < origin_collections.length; i++) {
if (origin_collections[i].ownerOf(origin_token_ids[i]) != msg.sender) {
revert NotOwningOrigin();
}
}
Mais il y a plus que ça. Un artiste peut décider de créer un style qui ne fonctionne que pour, disons, trois collections d’origine dédiées. Si tel est le cas, les adresses de ces collections peuvent être fournies dans le collectionAllowed membre sur notre Style NFT. Un style avec une contrainte de collection rétablira Splice mints lorsque la ou les collections d’origine ne sont pas répertoriées dans la liste des contraintes. Par défaut, les styles ne sont pas limités à des collections particulières, de sorte que les épissures peuvent être créées à partir de n’importe quel NFT d’origine.
Enfin, nous avons décidé d’ajouter un allowList caractéristique qui est unique à chaque Style NFT. Si vous vouliez créer des listes d’autorisation naïvement, vous pouvez simplement ajouter un membre de type mapping(uint32 => mapping(address => bool)) mais devrait initialiser ce mappage lors du déploiement d’un nouveau style. Si un créateur de style souhaite avoir, disons, 200 adresses sur une liste d’autorisation, cela nécessiterait l’envoi d’une quantité importante de gaz tout au long de la transaction de frappe. Puisque nous voulions que cette fonctionnalité soit économe en gaz, nous avons dû trouver une meilleure solution.
Nous avons décidé d’utiliser les preuves d’arbre Merkle pour déterminer si une demande de menthe est émise par un utilisateur autorisé. Heureusement, OpenZeppelin nous a couvert avec leur implémentation de référence pour les preuves Merkle, donc l’implémentation de cette fonctionnalité ne prend qu’une seule ligne de code réel :
///SpliceStyleNFT.solfunction verifyAllowlistEntryProof(
uint32 style_token_id,
bytes32[] memory allowlistProof,
address requestor
) public view returns (bool) {
return MerkleProof.verify(
allowlistProof,
allowlists[style_token_id].merkleRoot,
keccak256(abi.encodePacked(requestor))
);
}///Splice.sol:mintif (
allowlistProof.length == 0 ||
!styleNFT.verifyAllowlistEntryProof(
style_token_id,
allowlistProof,
msg.sender
) {
revert NotAllowedToMint('no reservations left or proof failed');
}
)
L’inconvénient de cette approche est que les membres de la liste d’autorisation doivent être conservés dans un endroit sûr et que les preuves doivent être générées pour chaque utilisateur individuellement, mais comme cette fonctionnalité traite de toute façon des données autorisées, il n’est pas nécessaire de la résoudre de manière 100 % décentralisée. Le code client qui crée le tableau de preuves respectif ressemble à ceci :
import { MerkleTree } from 'merkletreejs';
import keccak256 from 'keccak256';function createMerkleProof(allowedAddresses: string[]): MerkleTree {
const leaves = allowedAddresses.map((x) => keccak256(x));
return new MerkleTree(leaves, keccak256, {
sort: true
});
}const allowedAddresses: string[] = [/* many 0xaddresses */]const merkleTree = createMerkleProof(allowedAddresses);
const leaf = utils.keccak256(_allowedAddresses[0]);// this grows linearly with the overall amount of allowed addresses
const proof = merkleTree.getHexProof(leaf);const verified = await styleNft.verifyAllowlistEntryProof(
1,
proof,
allowedAddresses[0]
);expect(verified).to.be.true;
Notre implémentation initiale de Splice était basée sur l’hypothèse que les utilisateurs ont le contrôle total de leurs résultats d’épissage : le code rend un résultat de Splice sur la machine de l’utilisateur, l’encode au format PNG, le publie sur IPFS (en utilisant nft.storage), puis appelle la méthode mint du contrat en utilisant son hachage de contenu IPFS.
Nous nous sommes alors rendu compte que cette implémentation pouvait facilement être trompée par les utilisateurs : ils pouvaient en fait alimenter n’importe quel hachage IPFS qu’ils aimaient dans la fonction mint, pouvant effectivement tout frapper. Ce n’est certainement pas ce que nous voulions autoriser, nous avons donc eu l’idée d’utiliser un oracle qui demande des backends de validation dédiés pour valider que les « demandes » mint de nos utilisateurs contiennent réellement des images Splice valides qui correspondent aux paramètres d’entrée que l’utilisateur avait utilisés sur leur machine (vous trouvez une implémentation pour cela dans l’historique de notre repo).
Avec l’introduction de Style NFT dédiés, nous avons déconseillé cette étape de validation. Pour l’instant, Splice ne stocke aucune métadonnée IPFS. La seule information que nous gardons pour chaque épissure sur chaîne est son « provenance » qui est calculé lors d’un Splice mint :
/// Splice.sol:mintbytes32 provenanceHash = keccak256(
abi.encodePacked(
origin_collections, origin_token_ids, style_token_id
)
);
La provenance (ou l’héritage comme nous l’appelions historiquement) est unique pour chaque entrée Splice. Les utilisateurs eux-mêmes ne fournissent aucune autre information que leurs entrées d’origine souhaitées et un identifiant de jeton de style qui détermine quel code de style est exécuté sur les entrées et qui est lui-même ancré sur IPFS. Ainsi, toute personne connaissant l’entrée qui a conduit à la provenance peut reproduire de manière déterministe l’image rendue.
Si vous regardez les identifiants de jeton de Splice, par exemple « 12884901894”, vous remarquerez qu’ils ne sont apparemment pas générés de manière incrémentielle. En gros, ils le sont, mais c’est difficile à voir. Splice combine deux ID incrémentiels en une valeur d’ID unique pour chaque jeton. Il combine l’ID de style choisi et l’ID de jeton d’épissure incrémentiel (par style) en un ID de jeton unique (uint64, stocké sous uint256 comme requis par ERC721) :
/// SpliceStyleNFTfunction incrementMintedPerStyle(uint32 style_token_id)
external
onlySplice
returns (uint32)
{
if (mintsLeft(style_token_id) == 0) {
revert StyleIsFullyMinted();
}styleSettings[style_token_id].mintedOfStyle += 1;
/// Splice.sol
return styleSettings[style_token_id].mintedOfStyle;
}
uint32 nextStyleMintId = styleNFT.incrementMintedPerStyle(style_token_id);token_id = BytesLib.toUint64(
abi.encodePacked(style_token_id, nextStyleMintId),
0
);
Cette approche vous permet de calculer l’héritage de style de chaque Splice NFT. Vous prenez ses 64 bits les moins significatifs et les divisez en deux valeurs uint32. Le premier désigne l’ID de style, le second l’ID d’épissure incrémentiel pour ce style :
/// Splice.tspublic static tokenIdToStyleAndToken(tokenId: BigNumber) {
const hxToken = ethers.utils.arrayify(
utils.zeroPad(tokenId.toHexString(), 8)
);
return {
style_token_id: BigNumber.from(hxToken.slice(0, 4),
token_id: BigNumber.from(hxToken.slice(4))
};
}
Un identifiant de jeton à lecture décimale 12884901894 se traduit par la chaîne hexadécimale 0x0300000006 (ou complété à 8 octets / uint64 : 0x0000000300000006), donc cela se traduit effectivement par « l’épissure n°6 du style n°3 ».
