← Back to list

Overview of ProjectOpenSea/EthMoji Composable.sol contract

“Smart contracts for Ethmoji — composable art on the blockchain.”

Eli Jah · 2026-04-16 14:08 · 5 claps · 8.3 min read
#opensea #solidity #nft #blockchain #smart-contracts
Open on Medium ↗
Wiki topics: CRY · Crypto & Web3

Overview of ProjectOpenSea/EthMoji Composable.sol contract

[embed]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 tokens ownerOf(uint256 tokenID) , manages transfers with transferFrom() and safeTransferFrom(), maintains token metadata (name and symbol), and provides totalSupply() to check the total minted tokens.
  • Ownable.sol: Implements basic Access Control. It receives an address owner in its constructor and provides the onlyOwner modifier to functions, allowing only the specified owner to be able to call it. It also provides transferOwnership(), 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 with transfer(), payments are recorded in a payments mapping. Recipients can then call withdraw() to claim their funds. The asyncSend() internal function queues payments safely before sending. This pattern is essential for the royalty distribution mechanism in compose().
  • Pausable.sol: Provides emergency stop functionality by adding paused boolean state variable. It provides whenNotPaused and whenPaused modifiers, allowing contract owner to halt critical functions during emergencies. It is applied to compose() 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 tracking
  • compositions: 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 monetization
  • isCompositionOnlyWithBaseLayers: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 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

  • 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 expensive compose() 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:

  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.

  • _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)

  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:

  • Proxy.sol: Minimal Upgradeable Forwarder. This stores an implementation address and uses delegatecall in 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 inherits Proxy.sol and OpenZeppelins’ Ownable.sol. It just restricts setImplementation() to the contract owner, preventing unauthorized upgrades.
  • EthmojiProxy.sol: It extends OwnableProxy.sol with Ethmoji-specific initialization. This is the address deployed on mainnet. Users interact with it but all logic executes against the currently setEthmoji.sol implementation.
  • 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 truffle@4.1.5 zeppelin-solidity@1.9.0

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


메타데이터
post_id
eebea7fcc928
slug
overview-of-projectopensea-ethmoji-composable-sol-contract-eebea7fcc928
url
https://medium.com/@elijah_28569/overview-of-projectopensea-ethmoji-composable-sol-contract-eebea7fcc928
canonical_url
https://medium.com/@elijah_28569/overview-of-projectopensea-ethmoji-composable-sol-contract-eebea7fcc928
author_url
https://medium.com/@elijah_28569
status
ok
fetched_at
2026-06-23 21:39:52