Terminology

  • Channel: A modified ERC-1155 collection. The foundational entity of the protocol.
  • Transport Layer: Application specific contracts sitting on top of channels.
  • Interaction Logic: Custom logic which governs who is allowed to create and mint tokens in a channel.

Channel Factory

Channels are created by calling the channel factory contract. The factory creates a new ERC1967Proxy to one of the channel type implementations (Infinite or Finite). Anyone can create a channel by interacting with the factory.

Channels

Channel.sol calls to 2 external contracts to manage mint fees and interaction logic. Any fee or logic contract can be passed to the channel. The initial protocol release comes with:

CustomFees.sol - Custom incentive structure which allows admins / managers to define how mint fees (if any) are distributed to different parties. Mint fees can be set in ETH or ERC20.

DynamicLogic.sol - Logic which governs the interaction power (num creations / num mints) allotted to users in a channel based on results of external calls to other contracts at runtime.

Channel fees

Mint fees and protocol reward splits are set at the channel level for all tokens. By default, the channel has no fees. Fees are set by calling the setFees function on the channel either during initialization in setupActions[] or after the channel is created. Fees can be disabled by setting the fee contract to address 0.

/**
* @notice Set the fee structure for the channel
* @dev Address 0 is acceptable, and is treated as a no-op on fee computation
* @dev Only call into the fee contract if the address is not 0
* @param fees Address of the fee contract
* @param data Fee contract initialization data
*/
function setFees(address fees, bytes calldata data) external onlyAdminOrManager {
    feeContract = IFees(fees);

    if (fees != address(0)) {
        feeContract.setChannelFees(data);
    }

    emit ConfigUpdated(
        msg.sender, 
        ConfigUpdate.FEE_CONTRACT, 
        address(feeContract), 
        address(logicContract)
    );
}

Channel logic

Interaction logic is set at the channel level for all tokens. By default, the channel has no logic, meaning everyone can create and mint an unlimited number of tokens. The interaction logic is set by calling the setLogic function on the channel either during initialization in setupActions[] or after the channel is created. All logic can be disabled by setting the logic contract to address 0. Logic for each actor (creator / minter) can be set independently by passing 0 bytes respectively.

/**
* @notice Set the logic structure for the channel
* @dev Address 0 is acceptable, and is treated as a no-op on logic validation
* @dev Only call into the logic contract if the address is not 0
* @param logic Address of the logic contract
* @param creatorLogic Creator logic initialization data
* @param minterLogic Minter logic initialization data
*/
function setLogic(
    address logic,
    bytes calldata creatorLogic,
    bytes calldata minterLogic
) external onlyAdminOrManager {
    logicContract = ILogic(logic);

    if (logic != address(0)) {
        logicContract.setCreatorLogic(creatorLogic);
        logicContract.setMinterLogic(minterLogic);
    }

    emit ConfigUpdated(
        msg.sender, 
        ConfigUpdate.LOGIC_CONTRACT, 
        address(feeContract), 
        address(logicContract)
    );
}

Rewards

Protocol rewards and escrowed finite channel rewards are managed by Rewards.sol. Each channel has its own instance of Rewards.sol which is created during channel initialization.

Protocol rewards are pushed to recipients on every mint.

function _distributePassThroughSplit(Split memory split) internal {
    _validateIncomingValue(
        split.token, 
        split.totalAllocation
    );
    _validateIncomingAllocations(
        split.recipients, 
        split.allocations, 
        split.totalAllocation
    );

    if (split.token.isNativeToken()) {
        for (uint256 i; i < split.recipients.length; i++) {
        _processETHTransfer(
            split.recipients[i], 
            split.allocations[i]
        );
        }
    } else {
        for (uint256 i; i < split.recipients.length; i++) {
        if (msg.sender != split.recipients[i]) {
            /// @dev if the recipient is the sender, no need to transfer
            _transferExternalERC20(
                split.recipients[i], 
                split.allocations[i], 
                split.token
            );
        }
        }
    }
}

Escrowed rewards are distributed after finite channels are settled.

function _distributeEscrowSplit(Split memory split) internal {
    _validateIncomingAllocations(
        split.recipients, 
        split.allocations, 
        split.totalAllocation
    );

    if (split.token.isNativeToken()) {
        for (uint256 i; i < split.recipients.length; i++) {
        if (split.recipients[i] != address(0)) {
            /// @dev if the address is zero, keep the funds in the contract for the admins to withdraw later
            // otherwise process the transfer
            _processETHTransfer(
                split.recipients[i], 
                split.allocations[i]
            );
        }
        }
    } else {
        for (uint256 i; i < split.recipients.length; i++) {
        if (split.recipients[i] != address(0)) {
            /// @dev if the address is zero, keep the funds in the contract for the admins to withdraw later
            // otherwise process the transfer
            _transferEscrowERC20(
                split.recipients[i], 
                split.allocations[i], 
                split.token
            );
        }
        }
    }
}

Access control

Channels have 1 admin and 0 or more managers. Admins can add and remove managers, but otherwise have the same authority as managers. Both roles can:

  • update the channel fees, logic, and metadata
  • cancel finite channels
  • authorize channel upgrades

Admins can remove all managers and transfer ownership to address zero to lock the channel settings forever.

Admins and managers are assumed to be trusted parties which define the rules of engagement for the channel. Users visit the channel and either create or mint tokens.

Upgrades

Over time, we expect the protocol to evolve. When we upgrade the protocol, we register the new implementation address in the global UpgradePath.sol contract. This gives admins / managers a choice to either upgrade their channel to the new implementation, or keep using the old version. The SDK provides a method getOptimalUpgradePath that operators can use to determine if upgrades are available.

Transport Layers

The transport layer is the home of application specific functionality that site on top of channels. The transport layer receives messages from the channel on each creation or mint event and does some amount of extra processing.

We currently support 2 transport layers: Infinite and Finite.

Infinite Channels

Infinite Channels are very simple. They accept new tokens forever, and mints of them for a fixed period. The job of the infinite transport layer is to make sure the sale duration is respected.

function _transportProcessNewToken(uint256 tokenId) internal override {
    _setTokenSale(tokenId);
}

function _transportProcessMint(uint256 tokenId) internal override {
    if (block.timestamp > saleEnd[tokenId]) {
    revert SaleOver();
    }
}

Finite Channels

Finite Channels accept new tokens, and mints of those tokens, for a fixed period. Finite channels are meant to operate as a contest, in which rewards are escrowed by the admins on initialization and distributed after the contest ends by calling a settle function. The transport layer for finite channels is responsible for managing the contest state and maintaining the correct ranks, ordered by mints, of tokens in the channel. It does this by managing a doubly linked list.

function _processIncoming(uint256 tokenId) internal {
    TokenConfig memory incomingToken = tokens[tokenId];
    bytes32 id = keccak256(abi.encode(tokenId));

    if (length == 0) {
        _insertBeginning(tokenId, id);
        return;
    }

    if (nodes[id].tokenId == tokenId) {
        _promoteNode(tokenId, id);
        return;
    }

    if (incomingToken.totalMinted >= tokens[nodes[head].tokenId].totalMinted) {
        _insertBeginning(tokenId, id);
        return;
    }

    bytes32 currentId = head;
    while (currentId != 0x00) {
        Node memory currentNode = nodes[currentId];
        if (incomingToken.totalMinted >= tokens[currentNode.tokenId].totalMinted) {
        _insertMiddle(tokenId, id, currentNode);
        return;
        }

        if (currentNode.next == 0x00) {
        _insertEnd(tokenId, id);
        return;
        }
        currentId = currentNode.next;
    }
}