ERC-7752: Lot Token

Overview

This topic of discussion is an ERC token standard representing lot tokens. This new interface standardizes discrete holdings directly inside the token contract.

Issuance Identifiers

ERC-7752 is an ERC-1155 Multi-Token where each tokenId a distinct acquisition of tokens. Each token lot has a sole owner.

Rationale

Assigning a unique tokenId to all token lots makes it easier to access detailed data related to vesting schedules, lockups, and financial accounting.

Use Cases

Businesses can use ERC-7752 to represent many types of securities, including stock, investment fund interests, employee stock options, debt instruments, and convertibles. Capitalization (as in the accounting method of recording the cost of an asset) and capital gains accounting and reporting can be done onchain.

Individuals can use ERC-7752 to tokenize personal assets like their home, a boat, or other property.

Events can be indexed to form a more complete financial report of onchain accounts.

// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.17;

/**
 * @title ERC-7752 Lot Token Standard
 * @dev See https://eips.ethereum.org/EIPS/eip-7752
 * @notice Interface for the Lot Token contract, defining the essential functions and events.
 */
interface ILotToken {
    // ------------------------------------------------------------------------
    // Data Structures
    // ------------------------------------------------------------------------

    struct Lot {
        bytes32 parentLotId; // Parent lot (0x0 if none).
        uint96 quantity; // Quantity of the asset in the lot.
        uint96 costBasis; // Cost basis per unit.
        uint64 acquisitionDate; // Original acquisition timestamp.
        uint64 lastUpdate; // Last update timestamp.
        bool isValid; // Whether the lot is currently active.
        address owner; // Owner of the lot
    }

    // ------------------------------------------------------------------------
    // Events
    // ------------------------------------------------------------------------

    /**
     * @notice Emitted when a new lot is created (initial issuance or partial sale).
     * @param owner The user who owns this lot.
     * @param lotId The unique hash-based identifier of the lot.
     * @param parentLotId The lotId from which this lot originated (0x0 if none).
     * @param quantity The quantity in this new lot.
     * @param costBasis The cost basis per unit.
     * @param acquisitionDate The original acquisition date of the lot.
     * @param lastUpdate The timestamp when this lot was created/updated.
     */
    event LotCreated(
        address indexed owner,
        bytes32 indexed lotId,
        bytes32 indexed parentLotId,
        uint96 quantity,
        uint96 costBasis,
        uint64 acquisitionDate,
        uint64 lastUpdate
    );

    /**
     * @notice Emitted when a lot is transferred from one owner to another.
     * @param lotId The ID of the lot being transferred.
     * @param from The address transferring the lot.
     * @param to The address receiving the lot.
     * @param quantity The quantity being transferred.
     */
    event LotTransferred(bytes32 indexed lotId, address indexed from, address indexed to, uint96 quantity);

    /**
     * @notice Emitted when a lot is adjusted, creating a new lot from an old one.
     * @param oldLotId The ID of the original lot.
     * @param newLotId The ID of the new adjusted lot.
     * @param operator The address performing the adjustment.
     * @param newQuantity The new quantity of the adjusted lot.
     * @param newCostBasis The new cost basis of the adjusted lot.
     * @param reason The reason for the adjustment.
     */
    event LotAdjusted(
        bytes32 indexed oldLotId,
        bytes32 indexed newLotId,
        address operator,
        uint96 newQuantity,
        uint96 newCostBasis,
        string reason
    );

    /**
     * @notice Emitted when a lot is invalidated.
     * @param lotId The ID of the lot being invalidated.
     */
    event LotInvalidated(bytes32 indexed lotId);

    /**
     * @notice Emitted when an operator is approved for all owned lots.
     * @param owner The owner of the lot.
     * @param operator The operator being approved.
     * @param approved Whether the operator is approved.
     */
    event ApprovalForAll(address indexed owner, address indexed operator, bool approved);

    /**
     * @notice Emitted when a lot is approved for a specific operation.
     * @param lotId The ID of the lot being approved.
     * @param owner The owner of the lot.
     * @param spender The spender being approved.
     * @param amount The amount being approved.
     */
    event LotApproval(bytes32 indexed lotId, address indexed owner, address indexed spender, uint96 amount);

    // ------------------------------------------------------------------------
    // Core Functions
    // ------------------------------------------------------------------------

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

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

    /**
     * @dev Sets the name of the token.
     * @param _name The name of the token.
     */
    function setName(string memory _name) external;

    /**
     * @dev Sets the prefix of the token.
     * @param _prefix The prefix of the token.
     */
    function setPrefix(string memory _prefix) external;

    /**
     * @dev Creates a brand-new lot (e.g., an initial issuance) for a user.
     * @param owner The owner of the lot.
     * @param quantity The total quantity for this lot.
     * @param costBasis The cost basis per unit.
     * @param acquisitionDate The original acquisition date.
     * @return lotId The unique ID of the new lot.
     */
    function createLot(address owner, uint96 quantity, uint96 costBasis, uint64 acquisitionDate)
        external
        returns (bytes32 lotId);

    /**
     * @dev Transfers a lot or partial lot from 'from' to 'to', physically/fully (raw).
     * @param lotId The ID of the lot to transfer from.
     * @param to The new owner.
     * @param quantity The quantity to transfer.
     */
    function transfer(bytes32 lotId, address to, uint96 quantity) external;

    /**
     * @dev Transfers a lot or partial lot from 'from' to 'to', physically/fully (raw).
     * @param lotId The ID of the lot to transfer from.
     * @param from The current owner of the lot.
     * @param to The new owner.
     * @param quantity The quantity to transfer.
     */
    function transferFrom(bytes32 lotId, address from, address to, uint96 quantity) external;

    /**
     * @dev Creates a new lot as a child of an old lot, typically used for spin-offs,
     *      cost basis corrections, partial reclassifications, etc.
     * @param oldLotId The old lot to be adjusted.
     * @param newQuantity The quantity for the new lot.
     * @param newCostBasis The cost basis for the new lot.
     * @param reason A short string explaining the adjustment type.
     * @return newLotId The ID of the newly created lot.
     */
    function adjustLot(bytes32 oldLotId, uint96 newQuantity, uint96 newCostBasis, string calldata reason)
        external
        returns (bytes32 newLotId);

    /**
     * @dev Returns the stored data for a specific lot.
     * @param lotId The lot ID to query.
     * @return parentLotId Hash of the parent lot, or 0x0 if none.
     * @return isValid Whether the lot is active.
     * @return quantity The quantity.
     * @return costBasis The cost basis.
     * @return acquisitionDate The original acquisition timestamp (unmodified).
     * @return lastUpdate The last time this lot was updated.
     */
    function getLot(bytes32 lotId)
        external
        view
        returns (
            bytes32 parentLotId,
            bool isValid,
            uint96 quantity,
            uint96 costBasis,
            uint64 acquisitionDate,
            uint64 lastUpdate
        );
}

ERC Pull Request

TODO: Need to update the PR.

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

1 Like

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

The beauty of ERC-20 compliant RWA tokens category is that they allow permissioned dex, lending, voting, etc, protocols to be built relatively easily. If we are sticking to ERC-1155 standard, not only the token management together with its permissions will become exponentially more complex, but also we will lose the future integration potential.

P.S.

Having struggled a lot with ERC-3643, we have built TokenF on-chain RWA framework. You will find there a list of projects with different RWA approaches including ERC-1155.

1 Like

ERC-7752 uses a linked ERC-1155 tokenId list, making it super easy to audit the history of any given token, including unique URIs for each transfer of a token, which can point to an offchain contract. This functionality is efficient and adds a major compliance benefit for private RWA issuers.

Edit: ERC-7752 is purposely not backwards compatible ERC-20 RWAs as a tradeoff for the benefit of detailed tracking of token lots.

A couple non-editorial comments:

  • Why define mint/burn functions? The only actor that will be able to call those will likely be the contractā€™s deployer. The deployer of a contract knows the contractā€™s API. By defining mint/burn in your standard, you limit the flexibility of implementations. Youā€™ll note that none of the major token standards define these functions. If you choose to keep these functions as part of ERC-7752, Iā€™d strongly recommend including your reasoning in your Rationale section.
  • Similar comments about pause/unpause, forcedTransfer, and the freeze family of functions. Just define the events and the criteria for emitting them.

The only argument I can envision for making these functions part of the core standard is because you expect someone other than the contractā€™s deployer to call them. I could maybe imagine a RestrictedTokenPolicy interface with standard implementations like CanadianRestrictedTokenPolicy and NorwegianRestrictedTokenPolicy that you could give AgentRole to to manage your ERC-7752 token. Is that kind of what you have in mind?

3 Likes

Good work :+1: it seems to be similar to the features in CeFi

Hi @SamWilsn, thank you for your comments! FYI, I have been working on a refined proposal for the new token standard that will address these points, among others.

I agree with your recommendation. The new proposal will introduce a base token contract without access control, because the updated ERC proposal will encompass any asset with token lots. There currently is no token standard that does this.

Access control can be added to the token, and in many cases will be, but this does not have to be a part of the core standard. Iā€™ll be updating this post and the GitHub PR to reflect this.