Overview of ProjectOpenSea/EthMoji Composable.sol contract
--
GitHub - ProjectOpenSea/ethmoji-contracts: Ethmoji smart contracts
Ethmoji smart contracts. Contribute to ProjectOpenSea/ethmoji-contracts development by creating an account on GitHub.
github.com
“Smart contracts for Ethmoji — composable art on the blockchain.”
This article provides a comprehensive technical breakdown of the Composable.sol contract from the ProjectOpenSea/ethmoji-contracts repository, explaining every function, inherited contract, and external library dependency, bringing a practical knowledge of OpenSea’s composable token design.
Quick Disclaimer, this repository was archived in September 3, 20222 and uses solidity version ^0.4.21 with OpenZeppelin v1.x . While some patterns might be outdated (compared to their modern standard now), the architectural concepts remain educational and historically significant.
Introduction
The ethmoji-contracts powers a digital art studio on the Ethereum blockchain. It lets creators mint individual NFT “layers” (like backgrounds, characters, or accessories) and then allows anyone to combine those layers into new, unique composite NFTs — think of it like digital LEGOs for art.
When you compose a new piece:
- You select existing layer-NFTs you own (or that others have made available)
- You pay a small ETH fee that automatically distributes royalties to each layer’s creator
- The contract mints a brand-new NFT representing your unique combination
- That new NFT can itself become a layer in future compositions (if enabled)
All of this happens trustlessly on-chain: no central server, no manual royalty tracking, and cryptographic guarantees that no two compositions are identical. The contract is the engine behind OpenSea’s early vision of user-generated, composable NFT art where value flows automatically to creators every time their work is reused.
Composable.sol
This contract starts with the usual: pragma solidity ^0.4.21; , specifying the Solidity compiler version. Directly after it are imports frmo the OpenZeppelins’ contracts: ERC721Token, SafeMath, Ownable, PullPayment, Pausable.
ERC721Token.sol: The foundation of all NFT functionality. This contract implements the ERC721 standard for non-fungible tokens. It tracks ownership of NFT tokensownerOf(uint256 tokenID), manages transfers withtransferFrom() and safeTransferFrom(), maintains token metadata (name and symbol), and providestotalSupply()to check the total minted tokens.Ownable.sol: Implements basic Access Control. It receives anaddress ownerin its constructor and provides theonlyOwnermodifier to functions, allowing only the specified owner to be able to call it. It also providestransferOwnership(), to set a new address as the owner of the contract.PullPayment.sol: Implements the pull payment pattern to prevent reentrancy attacks. Instead of directly sending ETH withtransfer(), payments are recorded in apaymentsmapping. Recipients can then callwithdraw()to claim their funds. TheasyncSend()internal function queues payments safely before sending. This pattern is essential for the royalty distribution mechanism incompose().Pausable.sol: Provides emergency stop functionality by addingpausedboolean state variable. It provideswhenNotPausedandwhenPausedmodifiers, allowing contract owner to halt critical functions during emergencies. It is applied tocompose()to prevent minting during maintenance.
Next comes the State Variables:
uint public constant MAX_LAYERS = 100;
uint256 public minCompositionFee;
mapping (uint256 => uint256) public tokenIdToCompositionPrice;
mapping (uint256 => uint256) public tokenIdToCompositionPriceChangeRate;
mapping (uint256 => bool) public tokenIdToCompPricePermission;
mapping (uint256 => uint256[]) public tokenIdToLayers;
mapping (bytes32 => bool) public compositions;
mapping (uint256 => uint256) public imageHashes;
mapping (uint256 => uint256) public tokenIdToImageHash;
bool public isCompositionOnlyWithBaseLayers;MAX_LAYERS:Caps composition complexity at 100 layers, preventing gas limit exhaustion attacks.tokenIdToLayers: Maps each token to its constituent layer IDs, enabling recursive composition trackingcompositions: Tracks unique layer combinations via keccak256 hash (prevents duplicate NFT minting)imageHashes: Maps image content hash to token ID. Ensures visual uniqueness.tokenIdToCompositionPrice: Dynamic pricing per token, giving room for creator monetizationisCompositionOnlyWithBaseLayers:Toggle for composition rules, controlling composability depth.
Public Functions
* mintTo() : This is the function to create Base Layer Tokens.
function mintTo(
address _to,
uint256 _compositionPrice,
uint256 _changeRate,
bool _changeableCompPrice,
uint256 _imageHash
) public onlyOwnerPurpose: Mints a new “base layer” token that can be used in compositions.
Step-by-Step Execution:
_getNextTokenId()calculates the next available token ID usingtotalSupply().add(1)_mint(_to, newTokenIndex)assigns ownership via ERC721's internal mint functiontokenIdToLayers[newTokenIndex] = [newTokenIndex]initializes the layer array with the token's own ID (base layers contain only themselves)_isUnique()verifies no existing composition uses this exact layer+image combination- Records uniqueness in
compositionsandimageHashesmappings - Emits
BaseTokenCreatedevent for off-chain indexing - Initializes pricing parameters via private helper functions.
* compose(): The Core Composition Engine
function compose(
uint256[] _tokenIds,
uint256 _imageHash
) public payable whenNotPausedPurpose: Creates a new NFT by combining existing tokens as layers, distributing royalties to layer owners.
Detailed Execution Flow
- Validation Phase (performs safety checks on input and data):
require(_tokenIds.length > 1); // Must combine at least 2 layers
uint256 price = getTotalCompositionPrice(_tokenIds);
require(msg.sender != address(0) && msg.value >= price); // Payment check
require(_tokenIds.length <= MAX_LAYERS); // Gas safety- Layer Flattening & Deduplication
uint256[] memory layers = new uint256; // Pre-allocate array
uint actualSize = 0;
for (uint i = 0; i < _tokenIds.length; i++) {
uint256 compositionLayerId = _tokenIds[i];
require(_tokenLayersExist(compositionLayerId)); // Token must exist
uint256[] memory inheritedLayers = tokenIdToLayers[compositionLayerId];
// If only base layers allowed, enforce single-layer tokens
if (isCompositionOnlyWithBaseLayers) {
require(inheritedLayers.length == 1);
}
require(inheritedLayers.length < MAX_LAYERS); // Prevent deep recursion
// Flatten inherited layers while avoiding duplicates
for (uint j = 0; j < inheritedLayers.length; j++) {
require(actualSize < MAX_LAYERS);
for (uint k = 0; k < layers.length; k++) {
require(layers[k] != inheritedLayers[j]); // No duplicates
if (layers[k] == 0) break; // Optimization: stop at empty slot
}
layers[actualSize] = inheritedLayers[j];
actualSize += 1;
}- Royalty Distribution (Pull Payment Pattern)
require(ownerOf(compositionLayerId) != address(0));
asyncSend(
ownerOf(compositionLayerId),
tokenIdToCompositionPrice[compositionLayerId]
);
emit RoyaltiesPaid(compositionLayerId, tokenIdToCompositionPrice[compositionLayerId], ownerOf(compositionLayerId));
// Dynamic price increase for reused layers
tokenIdToCompositionPrice[compositionLayerId] =
tokenIdToCompositionPrice[compositionLayerId].add(
tokenIdToCompositionPriceChangeRate[compositionLayerId]
);The asyncSend() function (from PullPayment) records the payment obligation but doesn't transfer ETH immediately. Layer owners must later call withdraw() to claim funds — this prevents reentrancy vulnerabilities that plagued early NFT contracts.
- Minting the New Composition
uint256 newTokenIndex = _getNextTokenId();
tokenIdToLayers[newTokenIndex] = _trim(layers, actualSize); // Remove empty slots
require(_isUnique(tokenIdToLayers[newTokenIndex], _imageHash)); // Uniqueness check
compositions[keccak256(tokenIdToLayers[newTokenIndex])] = true;
imageHashes[_imageHash] = newTokenIndex;
tokenIdToImageHash[newTokenIndex] = _imageHash;
_mint(msg.sender, newTokenIndex); // Assign to composer- Refunding and Pricing Logic
if (msg.value > price) {
uint256 purchaseExcess = SafeMath.sub(msg.value, price);
msg.sender.transfer(purchaseExcess); // Refund overpayment
}
if (!isCompositionOnlyWithBaseLayers) {
_setCompositionPrice(newTokenIndex, minCompositionFee); // Enable recursive composition
}
emit CompositionTokenCreated(newTokenIndex, tokenIdToLayers[newTokenIndex], msg.sender);That’s the entire logic of the compose() function. One thing to note here is that the nested loops for deduplication could be gas heavy. With MAX_LAYERS=100, this remains feasible but would require optimization for larger compositions in modern contracts.
View Functions to Query Composition Data
getTokenLayers(uint256 _tokenId): Returns the flattened array of base layer IDs that constitute a token. Essential for frontends to render composite images.isValidComposition(uint256[] _tokenIds, uint256 _imageHash): Allows off-chain validation before submitting expensivecompose()transactions. Routes to specialized validation logic based on composition rules.
function isValidComposition(uint256[] _tokenIds, uint256 _imageHash) public view returns (bool) {
if (isCompositionOnlyWithBaseLayers) {
return _isValidBaseLayersOnly(_tokenIds, _imageHash);
} else {
return _isValidWithCompositions(_tokenIds, _imageHash);
}
}getCompositionPrice(uint256 _tokenId) and getTotalCompositionPrice(uint256[] _tokenIds):
function getTotalCompositionPrice(uint256[] _tokenIds) public view returns(uint256) {
uint256 totalCompositionPrice = 0;
for (uint i = 0; i < _tokenIds.length; i++) {
require(_tokenLayersExist(_tokenIds[i]));
totalCompositionPrice = SafeMath.add(totalCompositionPrice, tokenIdToCompositionPrice[_tokenIds[i]]);
}
totalCompositionPrice = SafeMath.div(SafeMath.mul(totalCompositionPrice, 105), 100); // 5% platform fee
return totalCompositionPrice;
}Calculates the total ETH required to mint a composition, including a 5% platform fee (*105/100). Uses SafeMath to prevent integer overflow.
Administrative Functions
setCompositionPrice(uint256 _tokenId, uint256 _price)
function setCompositionPrice(uint256 _tokenId, uint256 _price) public onlyOwnerOf(_tokenId) {
require(tokenIdToCompPricePermission[_tokenId] == true);
_setCompositionPrice(_tokenId, _price);
}Allows token owners to adjust their layer’s composition price — but only if tokenIdToCompPricePermission was set to true during minting. This enables dynamic creator pricing strategies.
payout(address _to) and setMinCompositionFee(uint256 _price)
These owner-only functions are for withdrawing the accumulated platform fees (payout), and adjusting the global minimum composition price floor.
Private Helper Functions (The Engine Room)
_isUnique(uint256[] _layers, uint256 _imageHash)
function _isUnique(uint256[] _layers, uint256 _imageHash) private view returns (bool) {
return compositions[keccak256(_layers)] == false && imageHashes[_imageHash] == 0;
}This enforces uniqueness by ensuring no existing composition uses the exact same layer combination (compositions[keccak256(_layers)]), and no existing token has the same visual content (imageHashes[_imageHash]), preventing both structural and visual duplication — a critical feature for collectible NFTs.
_isValidBaseLayersOnly()vs_isValidWithCompositions()
These two specialized validation paths:
- Base-layers-only mode: Ensures all input tokens are base layers (length=1), preventing recursive compositions.
2. Compositions-allowed mode: Recursively flattens nested compositions while enforcing MAX_LAYERS and deduplication.
_trim(uint256[] _layers, uint _size)
Cleans up pre-allocated arrays by copying only the used portion — a common Solidity pattern to avoid returning arrays with empty trailing elements.
_tokenLayersExist(uint256 _tokenId)
Simple existence check: a token exists if its tokenIdToLayers entry is non-empty. More reliable than checking ownerOf() alone, as tokens could theoretically be burned.
The Ethmoji Wrapper Contract
The Ethmoji.sol contract inherits from Composable and adds project-specific initialization.
contract Ethmoji is Composable {
function initialize(address newOwner) public {
require(!_initialized);
isCompositionOnlyWithBaseLayers = true; // Start in safe mode
minCompositionFee = .001 ether; // 0.001 ETH floor
owner = newOwner;
_initialized = true;
}
function compose(uint256[] _tokenIds, uint256 _imageHash) public payable whenNotPaused {
Composable.compose(_tokenIds, _imageHash);
// Immediately withdraw royalties to layer owners
for (uint256 i = 0; i < _tokenIds.length; i++) {
_withdrawTo(ownerOf(_tokenIds[i]));
}
}
}The overridden compose() function automatically triggers royalty withdrawals via _withdrawTo(), improving user experience by eliminating the need for layer owners to manually claim payments.
Although innovative at its time, this contract has patterns that would need to be revised according to today’s standards. It does handle security well enough via:
- Pull payment pattern to prevent reentrancy in royalty distribution
- Uniqueness checks via dual hashing to prevent duplicate NFTs
- MAX_LAYERS cap mitigates gas exhaustion attacks
- Pausable modifier enables emergency response.
Areas for Improvement (by Modern Standards)
- Solidity 0.4.21: Lacks modern safety features like
revert()with reason strings, custom errors, and improved overflow protection (thoughSafeMathis used) - Nested loops in
compose(): O(n²) deduplication could be optimized withmapping(uint256 => bool)for O(1) lookups - No access control for
initialize(): TheEthmojicontract'sinitialize()should use a proper proxy pattern (like OpenZeppelin'sInitializable) - Hardcoded 5% fee: Could be made configurable via governance
- No events for price changes: Limited off-chain indexing capabilities
Composability Patterns: Lessons for Modern NFT Development
The Composable.sol contract pioneered several patterns now common in NFT infrastructure:
1. Layered Asset Composition: The tokenIdToLayers mapping enables recursive asset construction — a precursor to modern "soulbound" tokens and modular NFT frameworks.
2. Dynamic Creator Royalties: The per-token compositionPrice with auto-incrementing rates creates a sustainable monetization model where popular layers earn more as they're reused.
3. Content-Aware Uniqueness: By hashing both structural composition (keccak256(layers)) and visual content (imageHash), the contract ensures true uniqueness beyond simple token IDs.
4. Safe Payment Orchestration: The integration of PullPayment demonstrates how to distribute value across multiple parties without exposing the contract to reentrancy — a pattern still recommended today.
Other Contracts in the ethmoji-contracts/contracts include the Proxy implementations:
Proxy.sol: Minimal Upgradeable Forwarder. This stores an implementation address and usesdelegatecallin a fallback function to forward all calls to the logic (implementation) contract. Preserves storage/layout while allowing upgrades to the logic contract.OwnableProxy.sol: Access-Controlled Proxy that inheritsProxy.soland OpenZeppelins’Ownable.sol. It just restrictssetImplementation()to the contract owner, preventing unauthorized upgrades.EthmojiProxy.sol: It extendsOwnableProxy.solwith Ethmoji-specific initialization. This is the address deployed on mainnet. Users interact with it but all logic executes against the currently setEthmoji.solimplementation.Avatar.sol: User profile and display layer. It links an Ethereum address to a specific Ethmoji tokenID. It handles equipping/setting a composition as a public avatar, emits events for off-chain indexers, and may cache/render metadata for OpenSea’s profile UI.
Getting Started with the Code
For developers wanting to experiment with this architecture:
# Clone the archived repository
git clone https://github.com/ProjectOpenSea/ethmoji-contracts
# Install dependencies (requires legacy tooling)
yarn add [email protected] [email protected]
# Start local blockchain
ganache-cli
# Compile and deploy
truffle migrate --resetBut be aware: This codebase uses deprecated tooling. For production use, migrate patterns to OpenZeppelin Contracts v4.x+ and Solidity ^0.8.0 with native overflow protection.
Conclusion
OpenSea’s Composable.sol represents a foundational piece of NFT infrastructure that demonstrated how smart contracts could enable complex, layered digital art economies. While modern development would implement these patterns with updated tooling and security practices, the core architectural insights (recursive composition, dynamic royalties, content-aware uniqueness, and safe payment distribution) remain highly relevant.
For developers building the next generation of composable digital assets, studying this contract offers valuable lessons in balancing flexibility, security, and user experience. As the NFT ecosystem evolves toward more sophisticated composability standards like ERC-6551 (Token Bound Accounts), the principles pioneered in Ethmoji continue to inform the future of on-chain creativity.
Disclaimer: This article is for educational purposes only. The ethmoji-contracts repository is archived and not maintained. Always conduct thorough security audits before deploying smart contracts to production environments.