ERC-7752: Private Equity Token

Overview

This topic of discussion is an ERC token standard representing VC and PE securities. This new interface standardizes equity management for issuers, angel investors, asset managers, transfer agents, and financial intermediaries. Creating an equity token standard requires a large industry effort. That’s why this token standard adheres to the same design principles as the Open Cap Format (OCF), an industry-approved data schema for cap tables. Now everyone can adopt the same OCF-compliant token standard for Ethereum and beyond!

Building off of ERC-3643 enables features such as:

  • Transfer agent controls (account and asset freeze, account recovery, etc.)
  • Compatibility with onchain identity management
  • Customized compliance modules

Issuance Identifiers

ERC-7752 is an multi-token version of ERC-3643 where each equity investment has its own tokenId. Thus, methods like freezePartialTokens have been removed in favor of freezeSecurity to target a specific tokenId. The tokenId is required for any method that modifies an account’s balance.

Assigning a unique tokenId to all issuances of equity tokens helps to access vesting details, tax lot identification, and a verifiable audit trail.

Token Lock Mechanism

To support collateral arrangements and atomic trade settlement, token agents may lock tokens on behalf of users prior to initiating settlement. Token agents are typically either the issuer, the issuer’s authorized transfer agent, or a qualified custodian.

Use Cases

Startup companies, investment managers, broker-dealers, and angel investors can use ERC-7752 to represent many types of securities, including stock, capital interests, employee stock options, and convertibles.

Interface

The current interface uses ONCHAINID but should be generalizable e.g. extends identity to EAS.

library Types {
  enum Status {
    Outstanding,
    Canceled,
    Transferred,
    Converted,
    Repurchased,
    PartiallyExercised,
    FullyExercised,
    Forfeited,
    Expired
  }
}

interface IERC7752 {
    event TokensLocked(address indexed caller, uint256 indexed tokenId, uint256 indexed amount);
    event TokensUnlocked(address indexed caller, uint256 indexed tokenId, uint256 indexed amount);
    event Approval(address indexed owner, address indexed approved, uint256 indexed tokenId);
    event ApprovalForAll(address indexed owner, address indexed operator, bool indexed approved);
    event Transfer(address indexed from, address indexed to, uint256 indexed tokenId);
    event TransferValue(uint256 indexed fromTokenId, uint256 indexed toTokenId, uint256 indexed amount);
    event TokenInvalidated(uint256 indexed tokenId);
    event URI(string uri, uint256 indexed tokenId);
    event AgentAddedForToken(uint256 indexed tokenId, address indexed agent);
    event AgentRemovedForToken(uint256 indexed tokenId, address indexed agent);

    /// @dev Emitted when the token information is updated.
    event UpdatedTokenInformation(
        string name,
        string prefix,
        string version,
        address indexed onchainID
    );

    /**
     *  this event is emitted when the IdentityRegistry has been set for the token
     *  the event is emitted by the token constructor and by the setIdentityRegistry function
     *  `_identityRegistry` is the address of the Identity Registry of the token
     */
    event IdentityRegistryAdded(address indexed _identityRegistry);

    /**
     *  this event is emitted when the Compliance has been set for the token
     *  the event is emitted by the token constructor and by the setCompliance function
     *  `_compliance` is the address of the Compliance contract of the token
     */
    event ComplianceAdded(address indexed _compliance);

    /// @dev Emitted when the contract is paused.
    event Paused(address indexed account);

    /// @dev Emitted when the contract is unpaused.
    event Unpaused(address indexed account);

    /// @dev Emitted when a security is issued.
    event Issued(address indexed to, uint256 indexed id, uint256 amount);

    /// @dev Emitted when a security is canceled (burned).
    event Canceled(address indexed from, uint256 indexed id, uint256 amount);

    /// @dev Emitted when an address is frozen or unfrozen.
    event AddressFrozen(address indexed account, bool isFrozen);

    /// @dev Emitted when tokens are frozen.
    event TokenFrozen(uint256 indexed tokenId);

    /// @dev Emitted when tokens are unfrozen.
    event TokenUnfrozen(uint256 indexed tokenId);

    /// @dev Emitted when a recovery is successful.
    event RecoverySuccess(address indexed lostWallet, address indexed newWallet, address indexed investorOnchainID);

    /**
     * @dev Initializes the contract.
     * @param identityRegistryAddress Address of the Identity Registry contract.
     * @param complianceAddress Address of the Compliance contract.
     * @param tokenName Name of the token.
     * @param tokenPrefix Prefix of the token.
     * @param tokenURI The base URI for the tokens.
     * @param tokenIdentity On-chain identity address of the token.
     */
    function init(
        address identityRegistryAddress,
        address complianceAddress,
        string memory tokenName,
        string memory tokenPrefix,
        string memory tokenURI,
        address tokenIdentity
    ) external;

    function totalSupply() external returns (uint256);

    /**
     * @dev Pauses all token transfers.
     *
     * Requirements:
     * - The caller must have the `AgentRole`.
     */
    function pause() external;

    /**
     * @dev Unpauses all token transfers.
     *
     * Requirements:
     * - The caller must have the `AgentRole`.
     */
    function unpause() external;

    /**
     * @dev Sets the Identity Registry contract address.
     * @param identityRegistryAddress Address of the new Identity Registry.
     *
     * Requirements:
     * - The caller must be the owner.
     */
    function setIdentityRegistry(address identityRegistryAddress) external;

    /**
     * @dev Sets the Compliance contract address.
     * @param complianceAddress Address of the new Compliance contract.
     *
     * Requirements:
     * - The caller must be the owner.
     */
    function setCompliance(address complianceAddress) external;

    /**
     * @dev Sets the name of the token.
     * @param _name New name of the token.
     *
     * Requirements:
     * - The caller must be the owner.
     */
    function setName(string calldata _name) external;

    /**
     * @dev Sets the prefix of the token.
     * @param _prefix New prefix of the token.
     *
     * Requirements:
     * - The caller must be the owner.
     */
    function setPrefix(string calldata _prefix) external;

    /**
     * @dev Sets the on-chain identity of the token.
     * @param _onchainID New on-chain identity address.
     *
     * Requirements:
     * - The caller must be the owner.
     */
    function setOnchainID(address _onchainID) external;

    /**
     * @dev Mints new tokens (certificates).
     * @param to Address of the recipient.
     * @param amount Amount of tokens to mint.
     * @param certificateURI URI of the certificate metadata.
     * @param data Additional data for compliance.
     *
     * Requirements:
     * - The caller must have the `AgentRole`.
     */
    function mint(
        address to,
        uint256 amount,
        string memory certificateURI,
        bytes memory data
    ) external returns (uint256 tokenId);

    /**
     * @dev Burns tokens (certificates).
     * @param from Address from which to burn tokens.
     * @param id Token ID to burn.
     * @param amount Amount of tokens to burn.
     *
     * Requirements:
     * - The caller must have the `AgentRole`.
     */
    function burn(
        address from,
        uint256 id,
        uint256 amount
    ) external;

    /**
     * @dev Freezes an address, restricting token transfers.
     * @param account Address to freeze.
     *
     * Requirements:
     * - The caller must have the `AgentRole`.
     */
    function freezeAddress(address account) external;

    /**
     * @dev Unfreezes an address, allowing token transfers.
     * @param account Address to unfreeze.
     *
     * Requirements:
     * - The caller must have the `AgentRole`.
     */
    function unfreezeAddress(address account) external;

    /**
     * @dev Returns the URI for a specific token ID.
     * @param tokenId Token ID to query.
     */
    function uri(uint256 tokenId) external view returns (string memory);

    /**
     * @dev Returns true if the contract is paused.
     */
    function paused() external view returns (bool);

    /**
     * @dev Returns true if the given address is frozen.
     * @param account Address to query.
     */
    function isFrozen(address account) external view returns (bool);

    /**
     * @dev Returns the Identity Registry address.
     */
    function identityRegistry() external view returns (IIdentityRegistry);

    /**
     * @dev Returns the Compliance contract address.
     */
    function compliance() external view returns (IModularCompliance);

    /**
     * @dev Returns the name of the token.
     */
    function name() external view returns (string memory);

    /**
     * @dev Returns the prefix of the token.
     */
    function prefix() external view returns (string memory);

    /**
     * @dev Returns the on-chain identity of the token.
     */
    function onchainID() external view returns (address);

    /**
     * @dev Returns the version of the token.
     */
    function version() external pure returns (string memory);

    function batchSafeTransfer(
      uint256[] calldata _tokenIds,
      address[] calldata _toList,
      uint256[] calldata _amounts,
      bytes[] calldata _dataList
    ) external;
    function batchSetAddressFrozen(address[] calldata _userAddresses, bool[] calldata _freeze) external;
    function isTokenFrozen(uint256 tokenId) external view returns (bool);
    function batchFreezeTokens(uint256[] calldata _ids) external;
    function batchForcedTransfer(
        address[] calldata _fromList,
        address[] calldata _toList,
        uint256[] calldata _ids,
        uint256[] calldata _amounts
    ) external;
    function batchUnfreezeTokens(uint256[] calldata _ids) external;
    function recoveryAddress(
        address _lostWallet,
        address _newWallet,
        address _investorOnchainID,
        uint256[] calldata _ids
    ) external returns (bool);

    function forcedTransfer(
      address _from,
      address _to,
      uint256 _id,
      uint256 _amount
    ) external returns (uint256 newTokenId);
    
    function setAddressFrozen(address _userAddress, bool _freeze) external;

    /**
     * @dev Freezes a specific token ID.
     * @param _id Token ID to freeze.
     *
     * Requirements:
     * - The caller must have the `AgentRole`.
     */
    function freezeToken(uint256 _id) external;

    /**
     * @dev Unfreezes a specific token ID.
     * @param _id Token ID to unfreeze.
     *
     * Requirements:
     * - The caller must have the `AgentRole`.
     */
    function unfreezeToken(uint256 _id) external;

    // Function to get token balance
    function balanceOf(uint256 tokenId) external view returns (uint256);

    // Function to get the account balance
    function balanceOf(address account) external view returns (uint256);

    // Function to get token owner
    function ownerOf(uint256 tokenId) external view returns (address);

    function approve(address to, uint256 tokenId) external;

    /// @dev Returns the approved address for a token ID, or zero if no address set
    function getApproved(uint256 tokenId) external view returns (address);

    /// @dev Approve or remove an operator for the caller
    function setApprovalForAll(address operator, bool approved) external;

    /// @dev Returns if the operator is allowed to manage all of the assets of owner
    function isApprovedForAll(address owner, address operator) external view returns (bool);

    /// @dev Transfers token from one address to another using approval mechanism
    function transferFrom(
        address from,
        address to,
        uint256 tokenId,
        uint256 amount,
        bytes memory data
    ) external returns (uint256 newTokenId);

    function getSecurity(uint256 tokenId) external view returns (
      address owner,
      uint256 balance,
      string memory uri,
      bytes memory data,
      Types.Status status,
      uint256 newTokenId,
      uint256 residualTokenId
  );

  /// @dev Function to lock tokens owned by the caller
  function lockTokens(uint256 tokenId, uint256 amount) external;

  /// @dev Function to unlock tokens owned by the caller
  function unlockTokens(uint256 tokenId, uint256 amount) external;
}

ERC-1155 Private Equity Structure: Asset Classes and Issuances

  1. Asset Class as Token Type

• Each asset class (e.g., preferred stock, common stock, convertible note) is represented as a unique ERC-1155 token type.

• Within each asset class, different issuances are distinguished by their token ID.

  1. Issuance as Unique Token ID

• For each new issuance within an asset class, a unique token ID is created.

• This token ID can then carry its own URI metadata to specify issuance-specific terms like the issuance date, purchase price, restrictions, and any other relevant details.

Refined Example Scenario

Let’s imagine a private equity scenario for a company that has multiple asset classes and needs to issue new securities over time:

Asset Class 1: Common Stock

Token Type: Common Stock

Token IDs (Issuances):

Token ID 1: Issuance for Round A investors with metadata including issuance date, price per share, voting rights, and transfer restrictions.

Token ID 2: Issuance for Round B investors with potentially different terms or restrictions (e.g., higher price per share or longer lock-up).

Token ID 3: Issuance for Round C with different pricing or specific investor rights.

Each issuance of common stock (Token IDs 1, 2, 3) within the Common Stock type can have unique metadata reflecting terms specific to that issuance.

Asset Class 2: Preferred Stock

Token Type: Preferred Stock

Token IDs (Issuances):

Token ID 10: Issuance for Series A Preferred with metadata for liquidation preference, dividend rights, and voting rights specific to Series A.

Token ID 11: Issuance for Series B Preferred, which could have additional rights or preferences, such as enhanced liquidation preference or anti-dilution provisions.

Each issuance of preferred stock (Token IDs 10, 11) under the Preferred Stock type can carry distinct rights as specified in its metadata.

Asset Class 3: Employee Stock Options

Token Type: Employee Stock Options

Token IDs (Issuances):

Token ID 20: Issuance for employee stock options granted in 2023, with metadata specifying the vesting schedule, exercise price, and expiration date.

Token ID 21: Issuance for a new batch of options granted in 2024, which might have a different vesting schedule or exercise price.

Each option issuance (Token IDs 20, 21) under the Employee Stock Options type can have different terms per issuance.

Asset Class 4: Convertible Notes

Token Type: Convertible Notes

Token IDs (Issuances):

Token ID 30: Convertible notes issued in 2023, with metadata outlining the conversion terms, maturity date, interest rate, and conversion trigger.

Token ID 31: Convertible notes issued in 2024, which may have different interest rates, conversion rates, or other terms.

Each issuance of convertible notes (Token IDs 30, 31) under the Convertible Notes type can have unique terms and conditions.

Key Benefits of This Structure

  1. Efficient Management of Multiple Securities

• Using ERC-1155 allows a single contract to handle multiple asset classes, with each class having distinct issuances represented by different token IDs. This reduces contract complexity and transaction costs compared to deploying separate contracts.

  1. Custom Metadata per Issuance

• Each token ID can have issuance-specific URI metadata to capture essential terms like the issuance date, restrictions, pricing, or rights. This enables detailed tracking and reporting of each issuance under a single asset class.

  1. Streamlined Auditing and Compliance

• By structuring each issuance as a unique token ID within the broader asset class, it becomes easier to track the history of each security and maintain compliance records. Each issuance’s metadata provides a transparent record for auditing and legal purposes.

  1. Reduced Gas Costs and Simplicity

• ERC-1155’s single-contract structure is more gas-efficient than deploying a separate ERC-20 or ERC-721 contract for each security type and issuance. Issuing a new token ID under an existing token type is cheaper and easier to manage on-chain.

  1. Flexibility in Investor Rights and Terms

• Within each asset class, you can issue tokens with different terms by using distinct token IDs. This is ideal for private equity, where each funding round or issuance may have slightly different rights, vesting conditions, or pricing.

Summary

In this ERC-1155-based private equity structure:

Asset Classes (e.g., common stock, preferred stock, employee stock options, convertible notes) are represented as distinct token types.

Each issuance within an asset class (e.g., different funding rounds or option grants) is represented by a unique token ID.

Metadata for each token ID provides transaction-specific details, making ERC-1155 an ideal choice for managing the nuanced and varied structure of private equity securities in a single, efficient contract.

This approach leverages ERC-1155’s strengths, allowing for flexible, gas-efficient, and transparent management of private equity tokens across multiple classes and issuances.

ERC Pull Request

4 Likes

I don’ think the batch interfaces are useful or necessary.

ok I see they are part of erc-3643. I don’t like them there either.

I think your specification can be simplified by explicitly requiring erc-3643 compliance. You can remove all of the erc-3643 spec thereby.

What do you see as the issue with the batch interfaces?

The original post didn’t mention that there are ERC-3643 breaking changes. I’ve updated the post with the details.

1 Like

What do you see as the issue with the batch interfaces?

Concatenation is the superior batch ABI. It even works for batching different methods. All of the top trading bots are doing it.

2 Likes

i don’t agree on that, most users still use EOAs and as long as ERC-3074 is not integrated (should be part of Pectra upgrade) they cannot batch transactions natively, therefore it is good to have batch transactions defined on the ABIs, not everyone uses trading bots or smart accounts. Also, having an external contract implementing the batching logic and calling the token contract repeatedly is going to cost additional gas compared to the batch implemented on the token contract directly.

We disagree on the meaning of ABI. You are confusing this because the non-standard Solidity 4byte ABI is the most popular ABI, and because Solidity’s json output calls its list of methods “abi”. However, ABI is a more general term referring to the binary input format itself, not the list of methods.

You would like there to be separate methods for batching. I would like for all methods to be batchable.

Both ABIs work for EOAs.

Yes they can, if we adopt the concatenation batch ABI.

Irrelevant; this demonstrates you do not understand. My theory about why you do not understand heads this reply.

Thank you for the clarification. I appreciate the insight into using ABI concatenation as a method for batching transactions. It’s definitely an interesting approach and I can see how it could be powerful in certain contexts, e.g. for advanced users like trading bots.

That said, in my experience developing in Solidity over the past six years, and working closely with a team of developers, this method of batching through ABI concatenation is not something we’ve commonly encountered or used. Most of the development community, from what I’ve observed, tends to favor more explicit and user-friendly interfaces, especially when it comes to contract interactions. The standard practice involves defining specific batch methods in the ABI to accommodate the needs of the majority of users and developers, particularly those using EOAs or relying on standard wallets and interfaces.

While ABI concatenation might offer more flexibility in certain scenarios, it also introduces a level of complexity that might not be necessary or practical for most use cases. The typical user interacting with smart contracts is not using raw ABI concatenation but rather calling clearly defined methods through familiar interfaces.

I agree that in a more abstract sense, ABI refers to the binary input format and not just the list of methods in a Solidity-generated JSON. However, for most real-world applications, especially those targeting a broad user base, the practical advantages of explicit batch methods in the ABI outweigh the theoretical flexibility provided by concatenation.

It’s certainly a technique worth being aware of, but in terms of practicality and adoption within the community, the current standard approach of defining batch methods seems to best serve the majority of users and use cases.

The fact that concatenation isn’t currently supported by solidity is not a reason to oppose it.

This EIP concerns the future. Past tendencies are gravitationally bound by the current limitations of solidity.

The concatenated methods are also clearly defined.

Obviously not, because the batching deficiency is cited in the rationale of several EIPS like 3074.

@wjmelements @Joachim-Lebrun thank you for the discussion. I think this thread will be helpful for others in consideration of batch interfaces in other specs.

That being said, I am updating the spec to remove partial freeze/unfreeze in favor of freezing a specified securityId. This simplifies the implementation and cuts down on the contract size.

@wjmelements, I suppose the concatenation you mention would further reduce compiled contract size (please correct me if I’m wrong) because the batch methods would not need to be explicitly implemented.

Edit: Are you referring to something like Multicall3?

2 Likes

@mrosendin I highly recommend for recoveryAddress or setCompliance considering adding storing a zk-hash that can be used later to recovery instead of directly pushing addresses, balances,etc.

1 Like

No

Two examples

The bytecode is pretty straightforward (examine with evm -d) but there aren’t any docs and it’s impossible within solidity today. I have a WIP proposal for solidity 4byte that would look like

function transfer(address to, uint256 value) external batchable;

If a method is batchable, then check after the calldata for that method for the next method in the batch, which must also be batchable. The main issue I’m having with this design are current constructs like msg.value.

I’ve revised the design. This new specification came along with helpful feedback from @Joachim-Lebrun.

The proposed new spec is based on the ERC-1155 Multi-Token Standard. Each asset class gets its own ERC-1155 token and each equity issuance has a unique token ID. Benefits include easier tax lot tracking, compliance restrictions targeting a specific issuance, and OCF adherence. I am testing this for startup companies and SPVs and scaling for large enterprises and investment funds.

1 Like