Captable manages ownership and voting rights in a company. It supports different classes of shares which are different tokens which may have different rights and voting weights. Each share class is represented with a separate ERC20 token. Shares aren’t fungible across classes, although they may be convertible from one class into another.

Given that the goal of Captable is to represent shares of stock of legal companies in a broad array of jurisdictions, it has been built to allow a very high degree of configuration, restrictions and forced actions.

Key concepts

Share classes

Captable allows the management of a large number of different classes of stock or different tokens. There’s currently a hard coded limit of 128 classes. Share classes can be completely standalone or may have another class that those shares of this class can convert to.

A separate ERC20 token is created for every class, we call these tokens EquityToken. These tokens are controlled by Captable so all issuance and conversions can be managed from a central place. If transfer restrictions allow, these tokens are compatible with different DeFi apps like DEXes or lending markets.

For each class, there are three important figures that Captable keeps track of:

  • Authorized shares: the total number of shares of this class that can be issued.

  • Convertible shares: the total number of shares of other classes that can be converted into this class of shares and therefore issued. If no shares have been converted yet, this figure will be the sum of the authorized shares of all classes that convert into a class. If no classes convert into a class, it will be zero.

  • Issued shares: amount of shares of the class that have been issued and are outstanding. It’s equal to the total supply of the token associated with this class.

One can think of the authorized amount of a class that converts into another one as reserved shares. This gives the certainty to holders of a class that they will always be able to perform the conversion at any time. To allow this, Captable never allows issuing shares if that could result in the inability to convert shares.

Transfer restrictions

Shareholders can perform transfers of shares by using the standard ERC20 transfer functions in the Equity token contract associated with a particular class. However, before a transfer occurs, the token checks with Captable is checked for whether the transfer is allowed.

Transfers can be restricted in different ways in order for the company to stay compliant. The two types of restrictions are bouncers and controllers. These restrictions only apply for shareholder initiated transfers. If both a bouncer and a controller are checked for a certain transfer, both must allow the transfer for it to go through.


A Bouncer is an optional transfer restriction which applies to all token transfers within a certain class. There are a few embedded/default basic bouncers which have been built into the protocol, but any arbitrary logic can be used to determine whether a transfer can go through or not.

The embedded bouncer options are:

  • Allow all: all transfers are allowed.

  • Deny all: all transfers are denied and shares of this class are non-transferable.

  • Allow transfer to class holder: only transfers to other accounts that already own some shares of the class are allowed.

  • Allow transfer to all holders: only transfers to other accounts that own any shares of any class are allowed.

Any smart contract which complies to the IBouncer interface can be used as a bouncer to check for transfer validity. For example, a company may want to curate a custom list of accounts allowed to receive shares depending on whether they have gone through a KYC process.

When choosing an embedded bouncer as the bouncer for a class, an address with a specific flag format is used. The format of this bouncer flag is: 0x00...[embedded bouncer id][02] e.g. 0x0000000000000000000000000000000000000102 is embedded bouncer 1 (‘Allow all’)

It is important to note that bouncers aren’t checked when share conversions occur. For example, a company may have a class of shares with a higher voting weight per share which is non-transferable (uses the ‘Deny all’ bouncer), but allows the shareholder to convert those into regular shares if they want to transfer them.


As opposed to bouncers that apply to all holders within a class, controllers are per-account restrictions. An account which holds shares of a certain class can be attached a controller which will be able to block its transfers based on some arbitrary logic.

Controllers (or account controllers) are used to control a certain account’s transfer based on some logic which is individualized. A controller is also able to forfeit shares from an account based on its logic.

An example controller that we have built is a vesting controller. It allows to issue shares to an account but ensure that the account can never transfer shares that they still have not vested. It also allows some authorized account to cancel their vesting and forfeit the shares which didn’t vest.

Differently than with bouncers, controller checks do apply to share conversions. If a controller would have blocked a transfer of a certain amount, it is likely that it would also be blocking a conversion in the same amount (e.g. our own vesting controller works this way).


Creating share classes

New share classes can only be created by the Safe of the organization. They are created with the following function:

function createClass(
        string calldata className,
        string calldata ticker,
        uint256 authorized,
        uint32 convertsToClassId,
        uint64 votingWeight,
        IBouncer bouncer
 ) external returns (uint256 classId, EquityToken token)
  • Class name/ticker: metadata for identifying the class. They can’t be modified after creation.

  • Authorized: number of initially authorized shares for the class. If shares of the class can convert into another class, this class must have a sufficient unissued amount of shares authorized to accommodate for the potential conversion of all authorized shares of this class. This amount can be modified at a later point by the Safe except if the share class has been frozen.

  • Converts to class ID: ID of the class into which shares of this class can convert. It cannot be modified after creation.

  • Voting weight: multiplier for token balances when getting voting power. Setting a voting weight of zero makes the share class non-voting.

  • Bouncer: address of the bouncer contract for the class. It cannot be unspecified, instead one of the embedded bouncer types can be used (see above). The bouncer can be modified at a later point by the Safe except if the share class has been frozen.

When created, the Safe is set as the initial manager for the class. Managers can issue shares and set account controllers in the class.

Issuing shares

A manager for the class can issue any account any amount of shares so long as the full amount of issued shares is within the authorized amount for the class. Issuing shares always goes through and is not subject to any bouncer or controller restrictions.

The recipient of the issued shares will be minted that amount of tokens in the EquityToken associated with the class.

Direct issuance is done via Captable:issue(address *account*, uint256 *classId*, uint256 *amount*)

Controlled shares

At the time of issuing shares, managers can set a controller in the receiving account using Captable:issueAndSetController(address account, uint256 classId, uint256 amount, IAccountController controller, bytes calldata controllerParams) external.

By doing this, the controller specified will be notified that a new account is under its control and it will now be able to block transfers of that account for this share class. Controllers are very critical and must be set with caution.

An important consideration is that only one controller can be set at the same time for a share class/account pair and the controller controls the entire account. This has two notable side effects:

  • If this function is called again setting a different controller, the previously set controller will stop being the controller. This could also be done by a different manager, which would effectively override whatever the first manager did.

  • Even if this function is called when issuing one share, the controller will have full control over previously owned shares.

Class managers

As mentioned in some sections above, there’s a level of privileged permissions on a per share class basis that can be done by class managers. Only the Safe can add or remove managers using Captable:setManager(uint256 classId, address manager, bool isManager)

Even though there are critical parameter changes for a class that are solely reserved for the Safe, class managers have a lot of power and it should be granted with extreme caution. It is highly unrecommended (and it should be a red flag) that an externally-owned account is set as a class manager, and in most cases, only other smart contracts should be managers. Managers should be considered a local ‘owner’ for the class and, as such, the same level of scrutiny must be had.

The actions that class managers can perform are:

  • Issue shares: a class manager can issue any amount of shares (never surpassing the authorized amount) to any account

  • Setting account controller: a manager in a class can set an account controller for any account. An account controller can block all transfers for that account or forfeit shares.

  • Forcing transfers: managers can directly force a transfer from any account to any other account. Forced transfers bypass account controller checks.

Note that while the Safe is the sole manager of all share classes by default and has the unique ability to set accounts as managers, if the Safe were to remove itself as a manager for a class, it will no longer be able to perform the actions explained above. After the Safe freezes the class, this will be locked forever and the current set of managers (including the ability to have none or just an automated one) will be locked.

Managers were built as a way to extend the capabilities of Captable and allow further programmability over the captable of the company. Unless for this purpose, companies should probably never add managers to their classes and just have the Safe as the default manager for issuance.

Transferring shares

By shareholder

Shareholders can perform transfers by interacting directly with the Equity token of the specific class they want to transfer. All regular ERC-20 transfer methods plus EIP-2612 permits are available to perform transfers.

As explained in the ‘Transfer restrictions’ section above, when performing share transfers, the Equity token will check with Captable whether the transfer is allowed based on its bouncer and the sender’s account controller.

Forced transfers

Both the class manager and the controller (on an account basis) can forcibly transfer any account’s shares to any other account, bypassing the bouncer check.

Converting shares

If a share class can be converted into another class, a holder can decide to perform a conversion of a certain amount of shares at any time (unless a controller for the origin conversion class blocks it) using Captable:convert(uint256 fromClassId, uint256 amount)

Conversions cannot be forced by the Safe, account controller or any class managers. A way to implicitly force conversions is to change the bouncer of the class to the ‘Deny all’ bouncer which would force all holders to convert should they want to move their shares.

Modifying class parameters

The Safe of an organization can perform the following actions to change parameters:

  • Captable:setAuthorized(uint256 classId, uint256 newAuthorized): changes the authorized amount of shares in a class. It must be at least the amount of shares currently issued plus the sum of all authorized shares of classes that convert into it.

  • Captable:setBouncer(uint256 classId, IBouncer bouncer): sets a new bouncer to control transfer restrictions in the class.

  • Captable:setManager(uint256 classId, address manager, bool isManager): changes the manager status for an account.


The Safe can decide to freeze a class to disable any future changes to any of its parameters. Freezing a class is a non-reversible action. Once a class is frozen the current set of parameters will be kept forever.

Only Safe can use Captable:freeze(uint256 classId) to freeze the parameters of a class.

Checking voting power

Both Captable and EquityToken have been built to be conformant with OpenZeppelin ERC20Votes (in the case of Captable, just to a subset of it) so they can be used out of the box with Governor (OpenZeppelin’s governance contract). ERC20Votes also supports single voting delegation.

Performing checks of voting power against Captable directly will result in checking for what the absolute voting power of a certain account is across all classes of shares, taking into account the different voting weights.

It is also possible to check directly with any EquityToken contracts to get voting power within a class alone. This allows the possibility of having both global voting for some matters while also being able to conduct certain votes in which only shares of a certain class can be voted.

Due to how ERC20Votes works, in order for an account’s votes to be counted and be able to participate in votes, the account must first delegate their votes to someone even if its to itself.


Custom bouncers

Apart from the embedded bouncers described above, it is possible to set a smart contract as the bouncer with custom logic.

Bouncers must conform to the IBouncer interface:

interface IBouncer {
    function isTransferAllowed(address from, address to, uint256 classId, uint256 amount)
        returns (bool);

When set, Captable will perform a staticall to the bouncer before any transfer initiated by a shareholder. Bouncers can perform any arbitrary checks to determine whether to allow a transfer but must not try to modify storage. In order for the transfer to go through, the bouncer must not revert and return true to Bouncer.isTransferAllowed.

It should be possible to reuse a Bouncer across many organizations for checks that can apply across companies. For example, an identity provider may provide a generic bouncer that returns true only if the recipient address has undergone a KYC process with them.

It is possible to obtain which specific company is performing the check with Captable(msg.sender) within the bouncer’s context.

Custom controllers

Any smart contract that conforms to the IAccountController interface (also contains the IBouncer function to check for transferability) can be set as a controller:

abstract contract IAccountController is IBouncer {
    function addAccount(address owner, uint256 classId, uint256 amount, bytes calldata extraParams) external virtual;

An optional implementation is provided with AccountController which is meant to be used as the contract to derive from when building an account controller.

Vesting controller

We have implemented a Vesting account controller both as an example of how to build controllers.

Smart contract managers

As explained through the document, managers must be set with extreme caution due to their critical powers within a share class.

They were designed to allow extensibility by creating smart contracts which can interact with Captable directly.

The entrypoints that a manager has are:

  • Captable:issue and Captable:issueAndSetController: for issuance

  • Captable:setController: to set account controllers

  • Captable:managerForcedTransfer: to perform arbitrary transfers

For example, a fundraising smart contract could be written which when set as the manager would be able to mint shares in exchange for an investment in the company. Another example could be automatic stock options execution, which would issue shares to the purchaser when exercising the option.

Last updated