Highlight Smart Contracts: Extensibility

Prerequisite reading - Overview

Smart contracts are immutable. This makes upgrading dApps difficult. When dealing with NFTs and other tokens, it’s key that data is preserved.

For example, imagine if Highlight forced creators to issue new NFTs and bridge old token ownership over every time the protocol either upgraded, or creators wanted to issue NFTs with new mechanisms. It’s critical that the sanctity of the community contract that is first deployed is protected.

Therefore, our smart contracts were constructed so almost everything about a community is extensible and configurable. As a starting point, our communities are upgradeable via a proxy/implementation pattern. But this design choice only serves system-wide upgrades, and leaves communities reliant on Highlight. Creators should be empowered independently. It is a function not protected by a well-defined and auditable smart contract interface, whereas encoded extensibility is. For example, one can easily corrupt contract data with a botched upgrade. Further, creators should be able to opt out of upgradeability via proxy, by switching to a locked beacon, which they are.

In fact, extensibility itself is a choice for Highlight communities. Extensibility is sometimes thought of as antithetical to some of the ethos of smart contract blockchains. We disagree. We believe that protocol participants should be able to choose to enable or disable extensibility. Extensibility is a superset primitive. Communities on Highlight can choose to forgo their extensibility at any time by “freezing” all or part of their community protocol. This is all made possible by building extensibility into the protocol from the outset, which is possible precisely due to the privilege they are forfeiting.

A Light Foray Into TokenManagers

Each token on a community is initially minted by a registered TokenManager. Token minting is available exclusively through TokenManagers. TokenManagers define how minting works. Creators can register a new token manager by calling:

function registerTokenManager(address _tokenManager) external;

Registered TokenManagers interface with the community via these functions:

function managerMintNewToOne(
    address to,
    uint256[] calldata amounts,
    string[] calldata uris,
    bool[] calldata isMembership
) external returns (uint256[] memory);

function managerMintNewToMultiple(
    address[] calldata to,
    uint256[] calldata amounts,
    string calldata _uri,
    bool isMembership
) external returns (uint256);

function managerMintExistingMinimized(
    address to,
    uint256 tokenId,
    uint256 amount
) external;

This is really powerful, because by deploying new TokenManagers, creators and Highlight are able to facilitate any mint mechanism they want on any type of token. The ERC1155 standard in conjunction with this model is powerful. For example, using this model, communities are able to quickly and easily facilitate a 10,000 PFP 1/1 mint, and then distribute a social token currency for holders of the aforementioned NFTs. All in the same community! They’re also able to define if the tokens minted gate the community itself, by denoting isMembership.

A simple use-case here is defining payment gateways for a token sale. Although Highlight enables on-chain payments via the aforementioned CentralPaymentsManager, an alternative implementation not reliant on the platform is one that manages token sales via a TokenManager, perhaps one titled PaymentsTokenManager or something similar.

Besides providing these points of leverage, a key advantage here is that storing and managing payments on a TokenManager doesn’t pollute the key community contract with potentially temporaneous data, as the community is expected to live forever.

After a token is minted by a TokenManager, the token is managed by that TokenManager.

mapping(uint256 => address) internal _tokenToManager;

There are three characteristics that define a token:

  1. Supply: is it a 1/1 NFT, a semi-fungible token, or a currency

  2. Movement: how does the token behave on transfers, burns

  3. Content: what is the metadata - name, description, media, etc.

All of these are managed and configurable via the TokenManager that minted the token initially.

We’ll now discuss each characteristic, and then finally some meta abilities that one can employ with a TokenManager.

Supply

Supply can be modified by minting more of an existing token or by burning a token. Since minting more of an existing token can only be done by the TokenManager of the token, we can use the TokenManager to define who/what/when/how can the supply of a token be updated. The current default TokenManager used to mint tokens on Highlight (GlobalBasicTokenManager) doesn’t expose a function to mint existing tokens, meaning the ability is restricted for any given token, until someone swaps in a TokenManager with the ability.

For example, consider this implementation of an inflationary mechanism, which lets sufficient holders of a social currency on the community mint one more of a semi-fungible token to another recipient:

contract WackyTokenManager is ... {
  ...
  function mintGiftIfWealthy(
    address to,
    uint256 tokenId,
    uint256 amount
  ) external override nonReentrant {
    require(IERC1155.balanceOf(msg.sender, 110) > 1000);
    require(amount == 1 && tokenId == 10);
    require(to != msg.sender);
    ICommunity(community).managerMintExistingMinimized(
      to, 
      tokenId, 
      amount
    );
  }
  ...
}

Movement

We’ve created an interface called IPostSafeTransfer. It is used by TokenManagers as a hook to add constraints or additional behaviour to token movement.

interface IPostSafeTransfer {
    function postSafeTransferFrom(
        address operator,
        address from,
        address to,
        uint256 id,
        uint256 amount,
        bytes memory data
    ) external; 
}

When tokens are transferred on a community, it will check if the TokenManager(s) managing the moved token(s) implement this interface. If they do, the implementation of the function on the interface is executed on the TokenManager.

For example, consider an NFT that incentivizes ownership retention for the first year by attaching a required payment to each transfer. An NFT like this could be easily implemented via the postSafeTransferFrom hook, including the timed aspect of the feature. And then deprecated right after by swapping the TokenManager for another.

Subprotocols

To study the third configurable token characteristic, content, let’s first examine the entire TokenManager interface. Doing so will introduce us to a major theme in a Highlight community protocol: subprotocols!

Here are the remaining functions on the TokenManager interface.

function canUpdateMetadata(
  address sender,
  uint256 tokenId,
  string calldata oldTokenUri,
  string calldata newTokenUri
) external view returns (bool);

function canSwap(
  address sender, 
  address newTokenManager
) external view returns (bool);

function canMintExisting(
  address sender, 
  uint256 tokenId,
  address[] calldata to, 
  uint256[] calldata amounts, 
  bytes calldata data
) external view returns (bool);

We can actually ignore the last one. It’s out of scope for these blog posts, and hints at a future iteration of the protocol not yet implemented. Let’s focus on canUpdateMetadata first.

Content

Similar to the postSafeTransferFrom hook, community contracts expose a function to update token metadata, that calls canUpdateMetadata on the token’s manager. canUpdateMetadata returns a boolean denoting if the metadata for the token can be updated at the particular time. For example, consider a randomized NFT mint, where minters are assigned tokens randomly, but the first 100 are enabled to actually update the NFT metadata for a period of time, if the updater owns multiple. Or, using updatable metadata as an incentive for highly engaged community members.

// implementation of first example 
function canUpdateMetadata(
  address sender,
  uint256 tokenId,
  string calldata oldTokenUri,
  string calldata newTokenUri
) external view returns (bool) {
   return (
     tokenId <= 100 && 
     now < updateTimePeriodEnd && 
     _userOwnsMultipleNFTs(sender, 202, 10202)
   ); 
}

Swapping

Similar to how token metadata can be changed, so can the TokenManager of a token. The community contract exposes a function that first calls the current manager of a token, to check if the manager can be swapped for another. On-chain entities being able to define how new behaviour can replace itself is an incredibly powerful paradigm. Tread carefully, as how canSwap is implemented, can completely modify all three token characteristics. Implemented without caution, it presents the risk of inadvertently opening up tokens to bad actors. Used well, it’s a powerful tool. A prime pattern that can be used here is gradual decentralization.

For example, consider a TokenManager that at first can be swapped out only by the creator, but is swapped for a TokenManager that can be swapped by a trusted community member. A token’s manager can quickly change permissions like this. Any given swap can either reduce or increase the space-time quantitative potential of swaps. Reducing this to zero could be achieved through implementations like time-locks, or flat out returning false, permanently locking in a TokenManager and the characteristics of the tokens it manages along with it.

This paradigm is so powerful that I’ve coined a new term for it.

CAGE

CAGE: Composable Autocratic Governing Entity. TokenManagers are CAGEs.

  • Composable

    • Via canSwap, chains of TokenManagers compose upon each other, each defining the circumstances under which the next one is introduced.
  • Autocratic

    • Each TokenManager is autocratic in that it defines how itself is ousted from power. While any given implementation of canSwap may not necessarily be autocratic (eg. one account can swap), the TokenManager itself is an autocratic entity, since it is the only one that defines how it can be swapped out
  • Governing

    • A CAGE must govern something other than how it can be changed. In the case of TokenManagers, this is a number of things, such as updating token metadata.
  • Entity

    • Any data structure on-chain, if CAGE is used in a smart contract blockchain context

Appropriately, the acronym also loosely fits the concept.

CommunityReadManagers

CommunityReadManagers are another entity that’s a CAGE. An initial CommunityReadManager is deployed alongside each community. It operates similarly to TokenManagers, except it governs matters concerning the entire community, as opposed to a single token. Think of it as a federal governing body, whereas a TokenManager is a municipal one. Here’s a part of the interface:

function canSwap(
  address sender, 
  address newCommunityReadManager
) external view returns (bool);

function canSetContractMetadata(
  address sender,
  bool setContractUri,
  bool setName,
  string calldata newContractUri,
  string calldata newName
) external view returns (bool);

function canSetRoyaltyCut(
  address sender, 
  uint32 newRoyaltyCut
) external view returns (bool);

As you can see, CommunityReadManagers operate similarly to TokenManagers. The difference is in the permissions themselves. Here, it protects:

  1. Updating of community metadata (name, contract metadata)

  2. Royalty cut, which is a value that ERC-2981-accepting marketplaces use to understand what percentage of a token sale to send to the royalty splitting contract deployed with each community

  3. Swapping of itself

Thus, it is clearly a CAGE—and I’ll let you think up eccentric use-cases here.

Soulbound NFTs

We’ve implemented a simple TokenManager that restricts transfers on tokens it manages, called NonTransferableTokenManager. Read it here. In recent months, Vitalik Buterin has adopted the term “Soulbound” to roughly designate non-transferable NFTs (with an identity context). There are many ways to understand these: badges, identifying markers, community awards, non-transferable, soulbound, etc.

We think this is an exciting concept to play with in communities. Especially since these NonTransferableTokenManagers can be swapped in and out, permanently swapped in on old tokens, or anything in between, creating interesting ways to gamify non-transferability.

A Final Word on Subprotocols

Modifying token behaviour and modifying token behaviour modification are highly compelling meta-themes. To be clear, all of canUpdateMetadata, canSwap, canUpdateCommunityMetadata, and canSetRoyaltyCut are subprotocols. All of them are permissions that can hold state, and be backed by any conceivable program.

Miscellaneous

Royalties

Royalties on Highlight are a fork of 0xSplits, who’ve done great work on infinitely composable royalty splits. This by itself is extensible. However, we’ve also modified the protocol in various ways, one of which so that communities can define who can set and update royalty recipients. The technical details fall outside the scope of this blog post, but look for a deep dive in the future. For the time being, this section of the protocol can be explored here.

There are clear extensible use-cases here, and we’re excited to help creators utilize them. For example, rewarding community discord moderators with a portion of royalties.

Upgrading via Proxy

It’s worth mentioning that three core smart contracts are upgradeable in the Highlight protocol. These are the Royalties manager, the PermissionsRegistry, and communities via the Beacon Proxy pattern. These exist to help us iterate quickly, make bug fixes, and optimizations. The decisions for all three have already paid off significantly. All upgrade vectors are protected by multi-signature smart contract wallets, and communities have the option to opt out, opting into locked implementations.

Couple things of note:

  1. There is a COMMUNITY_ADMIN role on communities, which is unused but serves as an easy, additional access control layer. Give the role out to trusted and engaged community members, and use it to gate things easily via TokenManagers or the CommunityReadManager.

  2. Intra protocol communication is done entirely through interfaces. Solidity interfaces are useful tools in highly flexible systems such as the Highlight protocol, and the subsystems that inhabit it.

Finally… who’s going to be the first person to build DAOTokenManager, a TokenManager that’s a DAO?

Thanks for reading. Stay tuned for our next instalment, which will cover how Highlight smart contracts are cost-efficient. Explore the protocol directly here. Interested in working on cutting edge smart contract architecture and web3 infrastructure? We’re hiring.

Subscribe to Highlight
Receive the latest updates directly to your inbox.
Mint this entry as an NFT to add it to your collection.
Verification
This entry has been permanently stored onchain and signed by its creator.