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
- 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.
- 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
- 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.
- 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.
- 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.
- 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.
- 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.