Start now →

Overview of ProjectOpenSea/EthMoji Composable.sol contract

By Eli Jah · Published April 16, 2026 · 11 min read · Source: Blockchain Tag
EthereumNFTsBlockchain
Overview of ProjectOpenSea/EthMoji Composable.sol contract

Overview of ProjectOpenSea/EthMoji Composable.sol contract

Eli JahEli Jah9 min read·Just now

--

GitHub - ProjectOpenSea/ethmoji-contracts: Ethmoji smart contracts

Ethmoji smart contracts. Contribute to ProjectOpenSea/ethmoji-contracts development by creating an account on GitHub.

github.com

Press enter or click to view image in full size

“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:

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.

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;

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 onlyOwner

Purpose: Mints a new “base layer” token that can be used in compositions.

Step-by-Step Execution:

  1. _getNextTokenId() calculates the next available token ID using totalSupply().add(1)
  2. _mint(_to, newTokenIndex) assigns ownership via ERC721's internal mint function
  3. tokenIdToLayers[newTokenIndex] = [newTokenIndex] initializes the layer array with the token's own ID (base layers contain only themselves)
  4. _isUnique() verifies no existing composition uses this exact layer+image combination
  5. Records uniqueness in compositions and imageHashes mappings
  6. Emits BaseTokenCreated event for off-chain indexing
  7. Initializes pricing parameters via private helper functions.

* compose(): The Core Composition Engine

function compose(
uint256[] _tokenIds,
uint256 _imageHash
) public payable whenNotPaused

Purpose: Creates a new NFT by combining existing tokens as layers, distributing royalties to layer owners.

Detailed Execution Flow

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
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;
}
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.

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
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

function isValidComposition(uint256[] _tokenIds, uint256 _imageHash) public view returns (bool) {
if (isCompositionOnlyWithBaseLayers) {
return _isValidBaseLayersOnly(_tokenIds, _imageHash);
} else {
return _isValidWithCompositions(_tokenIds, _imageHash);
}
}
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

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.

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)

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.

These two specialized validation paths:

  1. 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.

Cleans up pre-allocated arrays by copying only the used portion — a common Solidity pattern to avoid returning arrays with empty trailing elements.

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:

Areas for Improvement (by Modern Standards)

  1. Solidity 0.4.21: Lacks modern safety features like revert() with reason strings, custom errors, and improved overflow protection (though SafeMath is used)
  2. Nested loops in compose(): O(n²) deduplication could be optimized with mapping(uint256 => bool) for O(1) lookups
  3. No access control for initialize(): The Ethmoji contract's initialize() should use a proper proxy pattern (like OpenZeppelin's Initializable)
  4. Hardcoded 5% fee: Could be made configurable via governance
  5. 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:

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 --reset

But 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.

This article was originally published on Blockchain Tag and is republished here under RSS syndication for informational purposes. All rights and intellectual property remain with the original author. If you are the author and wish to have this article removed, please contact us at [email protected].

NexaPay — Accept Card Payments, Receive Crypto

No KYC · Instant Settlement · Visa, Mastercard, Apple Pay, Google Pay

Get Started →