Refactored and newly audited smart contracts for OtoCo: From ERC-20 to ERC-721

This post covers the technical details regarding our new smart contract design and explains our use of the ERC-721 token standard.

Since OtoCo launched over a year ago, our entity assembler smart contract spawned more than 500 onchain LLCs.

Over May and June, we earmarked a substantial part of the community funds we raised earlier this year to refactor our smart contracts and audit them.

The result is a faster, cheaper and more robust deployment of onchain entities as one of the primitives of Web3.

Entities as NFTs

In contrast with our initial design, each OtoCo entity will now be represented by an NFT minted by the OtoCo Master contract. Company information, such as the name, is contained within the metadata of the NFT, which can be looked up on Etherscan.

The owner of this NFT will have Manager privileges of that entity within the OtoCo dashboard and be able to add and remove plugins, initiate funding launchpools, sign documents, and more.

Various “jurisdiction”-specific contracts (for Delaware Series LLCs, Wyoming Series LLCs, and Unincorporated DAOs) are responsible for fetching the proper name formatting and metadata regarding the entity, and several plugin contracts work to add features to entities within OtoCo.

We will dive deeper into some of the most important functions within OtoCo’s smart contracts in the next three sections.

1. OtoCoMaster.Sol

This contract contains methods that allow OtoCo to receive payments and deploy new companies. It also contains a list of deployed entities as NFTs, with their details.

There are Administration Methods (only callable by the Manager of the contract) and Public Methods.

Key Administration Methods:

  • initialize(): Initialize the implementation contract and create initial jurisdictions.
  • changeBaseFees(): Set the new base fees to be paid in ETH along with the dApp transactions.
  • addJurisdiction(): Add a new jurisdiction to the network.
  • withdrawFees(): Withdraw the ETH stored on the contract.

Key Public Methods:

  • seriesCount(): Return the total number of series, across all jurisdictions.
  • baseFee(): Base fees charged for entity deployment/closing and plugin attachment/removal. This amount is a percentage and represents a value in ETH that needs to be sent along with the transaction.
  • createSeries(uint16 jurisdiction, address controller, string name): Create a new entity and set the controller. In order to create a new series, it is required to set the jurisdiction, name and the controller of the entity.
  • closeSeries(uint256 tokenId): Close the desired entity. This function can only be called by the controller of the entity.

2. OtoCoJurisdiction.Sol

This contract contains the entity naming rules and metadata for the NFTs. It can be used to add new jurisdictions, with their own rules, to the OtoCoMaster.sol contract without requiring an upgrade to the contract.

Key Public Methods

  • getSeriesNameFormatted(uint256 count, string name): Format and return the entity name to map the requirements of the jurisdiction.
  • getJurisdictionName(): Return the name of the jurisdiction.
  • getJurisdictionBadge(): Return the default NFT image URI for the jurisdiction.
  • getJurisdictionGoldBadge(): Return the original edition NFT image URI for the jurisdiction.

3. OtoCoPlugin.Sol

This contract introduces a key element of composability to OtoCo, in line with our vision to make it an open architecture platform where users can add various bolt-ons to their entity.

To achieve this, we use the OtoCoPlugin.sol contract which allows for each plugin and feature within OtoCo to have a dedicated smart contract (such as Token.sol), with the main plugin contract providing functions inherited by plugin-specific contracts.

Key Public Methods

  • otocoMaster(): Reference to the OtoCoMaster contract to transfer ETH paid for services. This function appears on all plugins, which makes it possible to verify the correct OtoCo Master attachment.
  • addPlugin(uint256 tokenId, bytes pluginData): Add the plugin to the entity selected according to the parameters selected.
  • attachPlugin(uint256 tokenId, bytes pluginData): Attach a pre-existing plugin to an entity.
  • removePlugin(uint256 tokenId, bytes pluginData): Remove the plugin to the entity selected according to the parameters selected.


To cover the implementation side of OtoCo, we will now walk you through the example of a user creating a Delaware LLC and attaching the “token foundry” plugin.

When an entity is created on the front-end of OtoCo, the createSeries method from the OtoCoMaster.sol smart contract takes three key parameters:

  • Jurisdiction—the jurisdiction of the entity, selected by the user
  • Controller—the address of the Manager, which is the wallet address the user connected with
  • Name—the entity’s name, chosen by the user

At the moment of creatrion, an internal transaction will be made to a jurisdiction smart contract to return the proper formatting of the entity’s name according to where it is based, in our example Delaware.sol.

The key methods from OtoCoMaster.sol, such as createSeries, can be seen in the code snippet below.

function createSeries(uint16 jurisdiction, address controller, string memory name) public enoughAmountFees() payable {

// Get next index to create tokenIDs

uint256 current = seriesCount;

// Initialize Series data

series[current] = Series(




IOtoCoJurisdiction(jurisdictionAddress[jurisdiction]).getSeriesNameFormatted(seriesPerJurisdiction[jurisdiction], name)


// Mint NFT

_mint(controller, current);

// Increase counters




Typically in less than 1 minute after the transaction is submitted, user will have the entity activated, with legal validity in the chosen jurisdiction (either Delaware, Wyoming, or as an unincorporated entity in the case of an Unincorporated DAO).

The controller’s wallet (Manager) - which can be a multisig or even a DAO contract! - will hold the NFT and enjoy Manager privileges (see above).

If user would like to add a plugin, such as OtoCo’s “token foundry” to create ERC-20 tokens (for instances to create mirror tokens reflecting % Membership in the LLC by multiple Members), user will submit a transaction to the Token.sol contract. The addPlugin method will act as an initializer and read the parameters from the plugin the user is trying to add.

In this case, parameters for the token foundry are:

[uint256, string, string, address] that represent the token's totalSupply, name, symbol and the holder that will receive the total supply of the tokens after deployment. This can all be seen in the code snippet below.

function addPlugin(uint256 seriesId, bytes calldata pluginData) public onlySeriesOwner(seriesId) transferFees() payable override {


uint256 supply,

string memory name,

string memory symbol,

address holder

) = abi.decode(pluginData, (uint256, string, string, address));

address newToken = Clones.clone(tokenContract);

ISeriesToken(newToken).initialize(name, symbol, supply, holder);



emit TokenAdded(seriesId, newToken);


This contract will deploy the ERC-20 token contract along with sending the balance to the addresses specified by the user who initiaties the token foundry contract.

Up to this point, we have used OtoCoMaster.sol to create the entity, Delaware.sol to format and return the name and metadata about the jurisdiction, and Token.sol to deploy ERC-20 tokens.

Creating an entity based in a different jurisdiction, and attaching a different plugin, follows a similar implementation pattern and many of the same methods—but with contracts specific to those features.

The following links direct to the addresses of each of the plugins below:

This concludes the technical breakdown of OtoCo’s smart contracts. For a more in-depth look at the technology, and to view the entirety of OtoCo’s open source code, please visit our Github!

Subscribe to The Otonomist newsletter and stay updated.

Don't miss anything. Get all the latest posts delivered straight to your inbox. It's free!
Great! Check your inbox and click the link to confirm your subscription.
Error! Please enter a valid email address!