diff --git a/AGENTS.md b/AGENTS.md index 47248563..af97791e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -98,3 +98,7 @@ DeactivateReportGas=true npx hardhat test - `contracts/interfaces/` - All supported interfaces and standards - `hardhat.config.js` - Build configuration (EVM & Solidity version) - `package.json` - Dependencies and scripts + +## Note + +After each implemented feature or fix, provide a one-line GitHub commit message for all changes since the last commit. diff --git a/CLAUDE.md b/CLAUDE.md index 47248563..af97791e 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -98,3 +98,7 @@ DeactivateReportGas=true npx hardhat test - `contracts/interfaces/` - All supported interfaces and standards - `hardhat.config.js` - Build configuration (EVM & Solidity version) - `package.json` - Dependencies and scripts + +## Note + +After each implemented feature or fix, provide a one-line GitHub commit message for all changes since the last commit. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 57b4be84..376519b4 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -22,6 +22,8 @@ This would make it easier to keep your changes separate from the upstream CMTAT It also simplifies upgrading to newer versions of CMTAT, since updating a submodule is typically cleaner and more straightforward than maintaining a fork. +If you want to create a version for another blockchain, you can find more details here: [CMTAT-equivalency-assessment](https://github.com/CMTA/CMTAT-equivalency-assessment) + ## Opening an issue You can [open an issue] to suggest a feature, a difficulty you have or report a minor bug. For serious bugs in an audited version please do not open an issue, instead refer to our [security policy] for appropriate steps. See [SECURITY.md](./SECURITY.MD). diff --git a/README.md b/README.md index cfd7c4ee..ab7f98a9 100644 --- a/README.md +++ b/README.md @@ -107,14 +107,30 @@ CMTAT implements a wide set of Ethereum standards: CMTAT provides cross-chain compatibility through `ERC20CrossChain` and related deployment options: - **ERC-7802**: native `crosschainMint` / `crosschainBurn` support for bridge-style interoperability. -- **Chainlink CCIP (CCT)**: compatible burn/mint token flow, including `getCCIPAdmin()` support through the `CCIPModule`. +- **Chainlink CCIP (CCT)**: compatible burn/mint token flow, including `getCCIPAdmin()` support through the `CCIPModule`. Scripts and examples are available in the project [CMTAT-CCIP](https://github.com/CMTA/CMTAT-CCIP) - **LayerZero**: supported through external OFT adapters (ERC-3643 and ERC-7802 variants) in [CMTAT-LayerZero](https://github.com/CMTA/CMTAT-LayerZero). For full architecture, permissions, and operational notes, see: - [Cross-chain bridge integration](./doc/technical/cross-chain-bridge-integration.md) - [Main documentation](./doc/README.md) -## Other Blockchain Implementations +## Other CMTA projects + +### Deployment version + +This section regroups projects which implements new CMTAT deployment version for EVM/Ethereum with additionnal features + +- [SnapshotEngine](https://github.com/CMTA/SnapshotEngine) + +The SnapshotEngine is a smart contract system designed to perform ERC-20 on-chain snapshots, making it easier to distribute dividends or other token-based rewards directly on-chain The Snapshone engine can be external or integrated directly in the main token contract. + +- [CMTAT-FIX](https://github.com/CMTA/CMTAT-FIX) ([Nethermind](https://www.nethermind.io)) + +Integration of FIX descriptor support for CMTAT. + +This project provides a modular engine system that enables CMTAT tokens to store, manage, and verify FIX (Financial Information eXchange) protocol descriptors on-chain. + +### Blockchain Implementations CMTAT is blockchain-agnostic and also has implementations/adaptations beyond this Solidity EVM repository: @@ -125,7 +141,33 @@ CMTAT is blockchain-agnostic and also has implementations/adaptations beyond thi - **Aztec (privacy-focused variant)**: [private-CMTAT-aztec](https://github.com/CMTA/private-CMTAT-aztec) - **Zama Confidential variant**: [CMTAT-Confidential](https://github.com/CMTA/CMTAT-Confidential) - A confidential security token implementation combining CMTAT compliance features with the Zama Confidential Blockchain Protocol for private balances. +- [CMTAT-Canton](https://github.com/CMTA/CMTAT-Canton): CMTAT version for Canton + +### Utility contract + +- [RulEngine](https://github.com/CMTA/RuleEnginehttps://github.com/CMTA/RuleEngine: ) + +The RuleEngine is an external contract used to apply transfer restrictions to another contract, such as CMTAT and ERC-3643 tokens. Acting as a controller, it can call different contract rules and apply these rules on each transfer. + +- [Rules](https://github.com/CMTA/Rules) + +**Rules** is a collection of on-chain compliance and transfer-restriction rules designed for use with the [CMTA RuleEngine](https://github.com/CMTA/RuleEngine) and the [CMTAT token standard](https://github.com/CMTA/CMTAT). + +- [SnapshotEngine](https://github.com/CMTA/SnapshotEngine) + +The SnapshotEngine is a smart contract system designed to perform ERC-20 on-chain snapshots, making it easier to distribute dividends or other token-based rewards directly on-chain + +- [IncomeVault](https://github.com/CMTA/IncomeVault) + +The `IncomeVault` is a prototype to perform coupon-payment dividend with a CMTAT and the snapshotEngine + +- [CMTAT-Factory](https://github.com/CMTA/CMTAT-Factory) + +Factory to deploy CMTAT with Transparent, UUPS and Beacon proxy using **deterministic addresses (via CREATE2** + +- [Delivery vs. payment protocol](https://github.com/CMTA/DVP) +The DvP (Delivery versus Payment) smart contract (DVP.sol) interacts with an Asset Token smart contract (Delivery) and a *Payment Order Token* smart contract (Payment). ## Security diff --git a/contracts/modules/4_CMTATBaseERC1404.sol b/contracts/modules/4_CMTATBaseERC1404.sol index e25c9a0f..939eaa06 100644 --- a/contracts/modules/4_CMTATBaseERC1404.sol +++ b/contracts/modules/4_CMTATBaseERC1404.sol @@ -3,14 +3,16 @@ pragma solidity ^0.8.20; import {CMTATBaseRuleEngine} from "./3_CMTATBaseRuleEngine.sol"; +import {CMTATBaseAccessControl} from "./2_CMTATBaseAccessControl.sol"; /* ==== Wrapper === */ // Use by detectTransferRestriction import {ERC20Upgradeable} from "./wrapper/core/ERC20BaseModule.sol"; // Extensions import {ERC20EnforcementModule} from "./wrapper/extensions/ERC20EnforcementModule.sol"; // Controllers -import {ValidationModuleERC1404, IERC1404Extend} from "./wrapper/extensions/ValidationModule/ValidationModuleERC1404.sol"; +import {ValidationModuleERC1404, IERC1404, IERC1404Extend} from "./wrapper/extensions/ValidationModule/ValidationModuleERC1404.sol"; import {ValidationModuleRuleEngine} from "./wrapper/extensions/ValidationModule/ValidationModuleRuleEngine.sol"; +import {ERC1404ExtendInterfaceId} from "../library/ERC1404ExtendInterfaceId.sol"; abstract contract CMTATBaseERC1404 is CMTATBaseRuleEngine, @@ -62,6 +64,25 @@ abstract contract CMTATBaseERC1404 is return CMTATBaseRuleEngine.canTransferFrom(spender, from, to, value); } + /** + * @notice ERC-165 interface detection + * @dev advertises support for both the canonical ERC-1404 interface + * (`IERC1404`, id `0xab84a5c8`) and its CMTAT extension + * (`IERC1404Extend`, id `0x78a8de7d`). + * @dev The extension id is taken from {ERC1404ExtendInterfaceId} because + * Solidity's `type(IERC1404Extend).interfaceId` excludes inherited + * functions and would therefore only cover `detectTransferRestrictionFrom`. + * @inheritdoc CMTATBaseAccessControl + */ + function supportsInterface( + bytes4 interfaceId + ) public view virtual override(CMTATBaseAccessControl) returns (bool) { + return + interfaceId == type(IERC1404).interfaceId || + interfaceId == ERC1404ExtendInterfaceId.ERC1404EXTEND_INTERFACE_ID || + super.supportsInterface(interfaceId); + } + /*////////////////////////////////////////////////////////////// INTERNAL/PRIVATE FUNCTIONS //////////////////////////////////////////////////////////////*/ diff --git a/contracts/modules/5_CMTATBaseERC20CrossChain.sol b/contracts/modules/5_CMTATBaseERC20CrossChain.sol index 569cb8cd..337d8baa 100644 --- a/contracts/modules/5_CMTATBaseERC20CrossChain.sol +++ b/contracts/modules/5_CMTATBaseERC20CrossChain.sol @@ -5,7 +5,6 @@ pragma solidity ^0.8.20; import {AccessControlUpgradeable} from "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol"; /* ==== Module === */ import {CMTATBaseERC1404, CMTATBaseRuleEngine, ERC20Upgradeable} from "./4_CMTATBaseERC1404.sol"; -import {CMTATBaseAccessControl} from "./2_CMTATBaseAccessControl.sol"; import {CMTATBaseCommon} from "./0_CMTATBaseCommon.sol"; import {ERC20BurnModuleInternal} from "./wrapper/core/ERC20BurnModule.sol"; import {ERC20MintModuleInternal} from "./wrapper/core/ERC20MintModule.sol"; @@ -72,8 +71,8 @@ abstract contract CMTATBaseERC20CrossChain is ERC20CrossChainModule, CCIPModule, return CMTATBaseCommon.symbol(); } - function supportsInterface(bytes4 _interfaceId) public view virtual override(CMTATBaseAccessControl, ERC20CrossChainModule) returns (bool) { - return ERC20CrossChainModule.supportsInterface(_interfaceId)|| CMTATBaseAccessControl.supportsInterface( _interfaceId); + function supportsInterface(bytes4 _interfaceId) public view virtual override(CMTATBaseERC1404, ERC20CrossChainModule) returns (bool) { + return ERC20CrossChainModule.supportsInterface(_interfaceId)|| CMTATBaseERC1404.supportsInterface( _interfaceId); } /*////////////////////////////////////////////////////////////// diff --git a/doc/ERCSpecification/draft-erc-1404-restricted.md b/doc/ERCSpecification/draft-erc-1404-restricted.md index 267cc183..126a1e16 100644 --- a/doc/ERCSpecification/draft-erc-1404-restricted.md +++ b/doc/ERCSpecification/draft-erc-1404-restricted.md @@ -1,25 +1,22 @@ ---- -eip: 1404 -title: Simple Restricted Token -description: A token interface extension for enforcing transfer restrictions with machine-readable status codes. -author: Ron Gierlach (@rongierlach), James Poole (@pooleja), Mason Borda (@masonicgit), Lawson Baker (@lwsnbaker), Ryan Sauge (@rya-sge) -discussions-to: https://ethereum-magicians.org/t/erc-1404-simple-restricted-token-standard/1405 -status: Draft -type: Standards Track -category: ERC -created: 2018-07-27 -requires: 20 ---- +|eip |title | authors| status | discussions-to | type | category | created | +|---------|----|---------|-------|----------------|-----|----------|---------| +| 1404 | Simple Restricted Token Standard|Ron Gierlach <@rongierlach>, James Poole <@pooleja>, Mason Borda <@masonicgit>, Lawson Baker <@lwsnbaker> | Draft | https://github.com/simple-restricted-token/simple-restricted-token/issues | Standards | ERC | 2018-07-27 | + +# Simple Restricted Token Standard + +## Simple Summary + +A simple and interoperable standard for issuing tokens with transfer restrictions. The following draws on input from top issuers, law firms, relevant US regulatory bodies, and exchanges. ## Abstract -Current token standards have provided the community with a platform on which to develop a decentralized economy that is focused on building Ethereum applications for the real world. As these applications mature and face consumer adoption, they begin to interface with corporate governance requirements as well as regulations. They must not only be able to meet corporate and regulatory requirements but must also be able to integrate with technology platforms underpinning their associated businesses. What follows is a simple and extendable standard that seeks to ease the burden of integration for wallets, exchanges, and issuers. +Current ERC token standards have provided the community with a platform on which to develop a decentralized economy that is focused on building Ethereum applications for the real world. As these applications mature and face consumer adoption, they begin to interface with corporate governance requirements as well as regulations. They must not only be able to meet corporate and regulatory requirements but must also be able to integrate with technology platforms underpinning their associated businesses. What follows is a simple and extendable standard that seeks to ease the burden of integration for wallets, exchanges, and issuers. ## Motivation -Token issuers need a way to restrict transfers of [ERC-20](./eip-20.md) tokens to be compliant with securities laws and other contractual obligations. Current implementations do not address these requirements. +Token issuers need a way to restrict transfers of ERC-20 tokens to be compliant with securities laws and other contractual obligations. Current implementations do not address these requirements. -A few examples: +A few emergent examples: - Enforcing Token Lock-Up Periods - Enforcing Passed AML/KYC Checks @@ -30,143 +27,56 @@ Furthermore, standards adoption amongst token issuers has the potential to evolv The following design gives greater freedom / upgradability to token issuers and simultaneously decreases the burden of integration for developers and exchanges. -Additionally, this standard provides a pattern by which human-readable messages may be returned when token transfers are reverted. Transparency as to _why_ a token's transfer was reverted is of equal importance to the successful enforcement of the transfer restriction itself. - -## Specification - -The key words "MUST", "MUST NOT", "REQUIRED", "SHOULD", and "MAY" in this document are to be interpreted as described in RFC 2119 and RFC 8174. - -[ERC-1404](./eip-1404.md) extends [ERC-20](./eip-20.md). All [ERC-20](./eip-20.md) functions, events, and semantics remain unchanged; compliant tokens MUST additionally implement the following methods. - -### Methods - -- #### `detectTransferRestriction(address,address,uint256)` - - Returns a restriction code for the proposed transfer of `value` tokens from `from` to `to`, or `0` if the transfer is unrestricted. The restriction logic is defined by the issuer. - - MUST be called inside the token's `transfer` and `transferFrom` methods. When a non-zero code is returned, the transfer SHOULD fail consistently with [ERC-20](./eip-20.md) expectations; reverting is RECOMMENDED. Implementations MAY return `false` instead. - - ```solidity - function detectTransferRestriction(address from, address to, uint256 value) external view returns (uint8); - ``` +Additionally, we see fit to provide a pattern by which human-readable messages may be returned when token transfers are reverted. Transparency as to _why_ a token's transfer was reverted is of equal importance to the successful enforcement of the transfer restriction itself. -- #### `messageForTransferRestriction(uint8)` +A widely adopted standard for detecting restrictions and messaging errors within token transfers will highly convenience the exchanges, wallets, and issuers of the future. - Returns the human-readable message corresponding to `restrictionCode`. - - ```solidity - function messageForTransferRestriction(uint8 restrictionCode) external view returns (string memory); - ``` - -### Additional Specifications - -- Implementations MAY add [ERC-165](./eip-165.md) interface detection support for [ERC-1404](./eip-1404.md). - -- Implementations MAY apply analogous restriction checks to token supply-changing operations (for example, mint and burn). Such checks are optional and are not required for [ERC-1404](./eip-1404.md) compliance. - -- The [ERC-165](./eip-165.md) interface identifier for [ERC-1404](./eip-1404.md) is `0xab84a5c8`. - -- Compliance-related contracts MAY implement [ERC-1404](./eip-1404.md) restriction interfaces without directly implementing the full [ERC-20](./eip-20.md) token interface. In such cases, this standard only defines the behavior of `detectTransferRestriction` and `messageForTransferRestriction`. - -- Restriction checks SHOULD be deterministic for the same state and inputs, and SHOULD avoid reliance on manipulable off-chain data during execution. - -- The string returned by `messageForTransferRestriction` SHOULD NOT be treated as an authorization primitive. +## Specification -- Implementations SHOULD define and manage restriction code allocation carefully, because `uint8` limits the available code space to 256 values (`0` to `255`). +The ERC-20 token provides the following basic features: +```solidity +contract ERC20 { + function totalSupply() public view returns (uint256); + function balanceOf(address who) public view returns (uint256); + function transfer(address to, uint256 value) public returns (bool); + function allowance(address owner, address spender) public view returns (uint256); + function transferFrom(address from, address to, uint256 value) public returns (bool); + function approve(address spender, uint256 value) public returns (bool); + event Approval(address indexed owner, address indexed spender, uint256 value); + event Transfer(address indexed from, address indexed to, uint256 value); +} +``` +The ERC-1404 standard builds on ERC-20's interface, adding two functions: +```solidity +contract ERC1404 is ERC20 { + function detectTransferRestriction (address from, address to, uint256 value) public view returns (uint8); + function messageForTransferRestriction (uint8 restrictionCode) public view returns (string); +} +``` + +The logic of `detectTransferRestriction` and `messageForTransferRestriction` are left up to the issuer. + +The only requirement is that `detectTransferRestriction` must be evaluated inside a token's `transfer` and `transferFrom` methods. + +If, inside these transfer methods, `detectTransferRestriction` returns a value other than `0`, the transaction should be reverted. ## Rationale -The standard proposes two functions on top of the [ERC-20](./eip-20.md) standard. The rationale for each is described below. +The standard proposes two functions on top of the ERC-20 standard. Let's discuss the rationale for each. 1. `detectTransferRestriction` - This function is where an issuer enforces the restriction logic of their token transfers. Some examples of this might include, checking if the token recipient is whitelisted, checking if a sender's tokens are frozen in a lock-up period, etc. Because implementation is up to the issuer, this function serves solely to standardize _where_ execution of such logic should be initiated. Additionally, 3rd parties may publicly call this function to check the expected outcome of a transfer. Because this function returns a `uint8` code rather than a boolean or just reverting, it allows the function caller to know the reason why a transfer might fail and report this to relevant counterparties. 2. `messageForTransferRestriction` - This function is effectively an accessor for the "message", a human-readable explanation as to _why_ a transaction is restricted. By standardizing message look-ups, we empower user interface builders to effectively report errors to users. -3. Optional [ERC-165](./eip-165.md) support - Implementations may expose [ERC-165](./eip-165.md) support for interface discovery. ## Backwards Compatibility -By design [ERC-1404](./eip-1404.md) is interface-compatible with [ERC-20](./eip-20.md), while transfer restrictions may introduce behavioral differences. - -## Test Cases - -The following table-driven cases SHOULD be verified for every implementation. The examples use a whitelist-based policy (code `0` = no restriction, `1` = sender not whitelisted, `2` = recipient not whitelisted) to keep the expected values concrete, but the same structure applies to any issuer-defined policy. - -### `detectTransferRestriction` - -| Scenario | Expected return | -|---|---| -| Both `from` and `to` satisfy the policy | `0` | -| `from` violates the policy | Non-zero restriction code | -| `to` violates the policy; `from` does not | Non-zero restriction code distinct from the sender case | -| Both `from` and `to` violate the policy | Non-zero code; the specific code returned depends on the implementation's evaluation order | - -### `messageForTransferRestriction` - -| Input | Expected output | -|---|---| -| `0` | A deterministic human-readable string indicating no restriction (e.g., `"No restriction"`) | -| Any known restriction code | The corresponding deterministic human-readable message | - -### Transfer enforcement - -| Scenario | Expected behavior for `transfer` and `transferFrom` | -|---|---| -| `detectTransferRestriction` returns `0` | Transfer succeeds | -| `detectTransferRestriction` returns a non-zero code | Transfer reverts (preferred) or returns `false` | - -### ERC-165 interface detection (optional) - -For implementations that expose [ERC-165](./eip-165.md) support: - -| Input to `supportsInterface` | Expected return | -|---|---| -| `0xab84a5c8` ([ERC-1404](./eip-1404.md) interface identifier) | `true` | -| `0x01ffc9a7` ([ERC-165](./eip-165.md) interface identifier) | `true` | -| Any unrecognized selector | `false` | - -A complete Foundry test suite covering all the cases above is available at [`test/ERC1404.t.sol`](../assets/eip-1404/test/ERC1404.t.sol). - -## Reference Implementation - -A complete reference implementation built with Foundry and OpenZeppelin Contracts v5 is provided in the assets folder: - -| File | Description | -|------|-------------| -| [`src/IERC1404.sol`](../assets/eip-1404/src/IERC1404.sol) | Interface — extends `IERC20` with the two [ERC-1404](./eip-1404.md) functions | -| [`src/ERC1404.sol`](../assets/eip-1404/src/ERC1404.sol) | Concrete implementation — whitelist-based, with [ERC-165](./eip-165.md) support | -| [`test/ERC1404.t.sol`](../assets/eip-1404/test/ERC1404.t.sol) | Foundry test suite covering all mandatory behaviors | - -The concrete implementation applies a whitelist policy and defines the following restriction codes. Note that codes `1` and `2` are specific to this implementation; [ERC-1404](./eip-1404.md) does not standardize restriction code values beyond reserving `0` as the "no restriction" sentinel. - -| Code | Constant | Message | -|------|----------|---------| -| `0` | `TRANSFER_OK` | `"No restriction"` | -| `1` | `SENDER_NOT_WHITELISTED` | `"Sender not whitelisted"` | -| `2` | `RECIPIENT_NOT_WHITELISTED` | `"Recipient not whitelisted"` | - -Notable design decisions in this implementation: - -- `transfer` and `transferFrom` revert with a typed `TransferRestricted(uint8 code, string message)` error on non-zero codes, rather than returning `false`. -- `detectTransferRestriction` checks the sender before the recipient, so callers can distinguish the two failure cases with a single view call before submitting a transaction. -- `supportsInterface(0xab84a5c8)` returns `true`, enabling on-chain interface discovery. -- Mint and burn operations apply analogous restriction checks, as permitted by the specification. -- Implementations that also conform to [ERC-7943](./eip-7943.md) will likely revert with that standard's typed errors rather than `TransferRestricted(uint8 code, string message)`. Those errors do not carry a restriction code or human-readable message. - -This example is provided for educational purposes only and has not been audited. Do not use in production without a thorough independent security review. - -## Security Considerations - -- Implementations are expected to encode policy in `detectTransferRestriction`, so mistakes in this logic can block valid transfers or allow restricted transfers. - -- Restriction checks that are not deterministic for the same state and inputs can create inconsistent behavior, and reliance on manipulable off-chain data can undermine enforcement. - -- Returning machine-readable codes improves integration, but the string returned by `messageForTransferRestriction` remains informational and is not an authorization primitive. - -- Using `uint8` for restriction codes limits the available code space to 256 values (`0` to `255`), which can create ambiguity if code allocation is not managed carefully. +By design ERC-1404 is fully backwards compatible with ERC-20. +Some examples of how it may be integrated with common types of restricted tokens may be found [here](https://github.com/simple-restricted-token/simple-restricted-token-standard#readme). -- [ERC-1404](./eip-1404.md) interface support alone is not evidence that a contract implements full [ERC-20](./eip-20.md) transfer behavior; a compliance-focused contract can expose [ERC-1404](./eip-1404.md) restriction interfaces and interface support while omitting parts of the [ERC-20](./eip-20.md) interface. +## Test Cases & Implementation -- The original [ERC-1404](./eip-1404.md) text did not require [ERC-165](./eip-165.md) signaling. Therefore, older implementations may still implement [ERC-1404](./eip-1404.md) while not returning `true` for the [ERC-165](./eip-165.md) interface identifier `0xab84a5c8`. +See the reference implementation and tests [here](https://github.com/simple-restricted-token/reference-implementation#readme). +See some examples of common usage patterns for ERC-1404 [here](https://github.com/simple-restricted-token/simple-restricted-token-standard#readme). ## Copyright -Copyright and related rights waived via [CC0](../LICENSE.md). +Copyright and related rights waived via [CC0](https://creativecommons.org/publicdomain/zero/1.0/). diff --git a/doc/ERCSpecification/draft-erc-1450.md b/doc/ERCSpecification/draft-erc-1450.md new file mode 100644 index 00000000..6a0e513c --- /dev/null +++ b/doc/ERCSpecification/draft-erc-1450.md @@ -0,0 +1,2861 @@ +--- +eip: 1450 +title: RTA-Controlled Security Token +description: Security tokens where the Registered Transfer Agent has exclusive control over transfers for regulatory compliance +author: Howard Marks (@howardmarks) , Devender Gollapally (@devender-startengine) , Joe Mathews (@se-joe) , Jordan Jahja (@jordan-jahja) , John Shiple (@johnshiple), David Zhang (@david-colab) +discussions-to: https://ethereum-magicians.org/t/erc-1450-rta-controlled-security-token-standard/26385 +status: Last Call +last-call-deadline: 2026-07-14 +type: Standards Track +category: ERC +created: 2018-09-25 +requires: 20, 165, 6093 +--- + +## Abstract + +[ERC-1450](./eip-1450.md) facilitates the recording of ownership and transfer of securities sold in compliance with Securities Act Regulations CF, D, and A. This standard is informed by practical operational experience from SEC-registered transfer agents, broker-dealers, and alternative trading systems that have collectively managed billions in compliant securities offerings. The design addresses the full lifecycle of digital securities from issuance through secondary trading. + +The standard introduces a unique RTA-controlled model where the Registered Transfer Agent maintains exclusive authority over all token operations. Unlike permissionless tokens, ERC-1450 enforces strict compliance by requiring the RTA to execute all mints, burns, and transfers, while disabling direct value movement via `transfer()` and `approve()`. Holder-initiated transfer requests are permitted via `requestTransferWithFee()`, but no value moves unless the RTA authorizes and executes the transfer. The standard also enables compliant secondary markets through a broker registration system, where vetted brokers can request transfers with fees on behalf of holders. This design ensures regulatory compliance with SEC requirements and state blue sky laws while providing liquidity options and maintaining read-only compatibility with existing [ERC-20](./eip-20.md) infrastructure. + +Key features include RTA-exclusive control, restricted [ERC-20](./eip-20.md) interface for ecosystem integration, and built-in mechanisms for regulatory compliance including recovery procedures for lost tokens and support for court-ordered transfers. The standard MAY optionally implement [EIP-3668](./eip-3668.md) (CCIP-Read) for off-chain compliance pre-checks, improving user experience by allowing wallets to validate transfers before gas payment. + +## Motivation + +With the advent of the JOBS Act in 2012 and subsequent regulations (Regulation Crowdfunding in 2016, amended Reg A and Reg D), there has been significant expansion in exemptions for securities offerings. The regulated securities market has grown substantially, with billions in offerings across thousands of companies. + +Experience from operating SEC-registered transfer agents has revealed critical gaps in existing token standards for securities. While standards like [ERC-3643](./eip-3643.md) provide on-chain compliance mechanisms, they don't address the unique regulatory requirements of U.S. securities law, particularly the role of Registered Transfer Agents. + +Current challenges that ERC-1450 addresses: +- **Transfer Controller Authority**: SEC regulations require Registered Transfer Agents to maintain exclusive control over securities transfers, similar to designated controller requirements in other jurisdictions +- **Recovery Mechanisms**: Legal requirements for recovering lost or stolen securities +- **Court Orders**: Ability to execute court-ordered transfers (divorce, estate, fraud recovery) +- **Regulatory Reporting**: Clear audit trails for regulatory examinations +- **Cost Efficiency**: Leveraging existing transfer agent infrastructure for compliance + +ERC-20 tokens do not support the regulated roles of Funding Portal, Broker Dealer, RTA, and Investor and do not support the Bank Secrecy Act/USA Patriot Act KYC and AML requirements. Other improvements (notably Simple Restricted Token Standards) have tried to tackle KYC and AML regulatory requirements. This approach assigns exclusive control over `transferFrom`, `mint`, and `burnFrom` to a designated transfer agent who performs KYC and AML compliance. + +This standard codifies operational requirements into a technical specification that bridges traditional securities regulation with blockchain technology. + +## Specification +ERC-1450 extends [ERC-20](./eip-20.md). + +### Optional Dependencies +The following standards MAY be implemented for enhanced functionality but are NOT required for compliance: +- **[EIP-3668 (CCIP-Read)](./eip-3668.md)**: MAY be used for off-chain compliance pre-checks. Implementations choosing to support this MUST implement the `preCheckCompliance` and `preCheckComplianceCallback` functions as specified. +- **[ERC-1820 (Registry)](./eip-1820.md)**: MAY be used for interface registration. Implementations can optionally register their interfaces in the ERC-1820 registry for improved discoverability. + +In addition to the optional standards above, the following interface components defined in this specification are OPTIONAL extensions. Implementations MAY omit them; implementations that provide them MUST follow the behavior specified for them (including emitting the specified events): + +- **Document management** (`setDocument`, `getDocument`, `removeDocument`, `getAllDocuments`) +- **Structured recovery workflow** (`initiateRecovery`, `cancelRecovery`, `executeRecovery`, `getRecoveryDetails`, `hasPendingRecovery`) — the REQUIRED baseline for lost-wallet recovery and court-ordered transfers is `controllerTransfer`, which all implementations MUST provide +- **KYC status view** (`isKYCVerified`) +- **Gasless fee approval** (`requestTransferWithPermit`) +- **Off-chain compliance pre-checks** (`preCheckCompliance`, `preCheckComplianceCallback`) +- **Transfer request status view** (`getRequestStatus`) — implementations MAY instead expose equivalent request data through other read methods (e.g., a public storage mapping) + +### ERC-1450 +ERC-1450 is an interface standard that defines a security token where only the Registered Transfer Agent (RTA) has authority to execute transfers, mints, and burns. The token represents securities issued by an owner (the issuer) and managed exclusively by an RTA. + +The standard enforces strict role separation: +- **Owner/Issuer**: The entity that creates and owns the security +- **RTA**: The only entity authorized to transfer, mint, or burn tokens +- **Token Holders**: Cannot initiate transfers directly (unlike standard [ERC-20](./eip-20.md)) + +ERC-1450 explicitly disables direct value movement by requiring the `transfer` and `approve` functions to always revert. Only the RTA can execute token movements via `transferFrom`, `mint`, and `burnFrom` functions. Holder-initiated transfer requests are permitted via `requestTransferWithFee()`, but no value moves unless the RTA authorizes and executes the transfer. Registered brokers can also request transfers on behalf of holders through the same mechanism. This design ensures regulatory compliance by centralizing all token operations through the regulated RTA. + +Critical security feature: The `changeIssuer` function can only be called by the RTA, not the owner. This protects against compromised issuer keys - even if an issuer's private key is stolen, the attacker cannot change the RTA or steal tokens. + +### Issuers and RTAs +Implementations must initialize the following parameters upon deployment: +- `owner`: The issuer's address +- `transferAgent`: The RTA's address (preferably an RTAProxy contract) +- `name`: The security's name +- `symbol`: The security's trading symbol +- `decimals`: The number of decimal places (0 for indivisible shares, up to 18 for fractional) + +#### Access Control Model +The interface defines three levels of access control: + +**RTA-Only Functions:** +- `changeIssuer`: Change the token issuer/owner (only callable by RTA, not by issuer) +- `transferFrom`: Transfer tokens between accounts +- `mint`: Create new tokens +- `burnFrom`: Destroy existing tokens +- All batch operations and fee collection functions + +**Owner-Only Functions:** +- `setTransferAgent`: One-time setup to RTAProxy (locked after initial setup) + +**Public Functions:** +- `isTransferAgent`: Check if an address is the current RTA +- Standard [ERC-20](./eip-20.md) view functions (`balanceOf`, `totalSupply`, etc.) + +#### Security and Compliance +The RTA maintains exclusive control over all token movements, ensuring: +- Complete audit trail for regulatory reporting +- Enforcement of transfer restrictions +- Recovery mechanisms for lost tokens +- Execution of court orders +- Prevention of unauthorized transfers + +### [ERC-20](./eip-20.md) Extension +[ERC-20](./eip-20.md) tokens provide the following functionality: + +```solidity +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +interface IERC20 { + function totalSupply() external view returns (uint256); + function balanceOf(address account) external view returns (uint256); + function transfer(address to, uint256 amount) external returns (bool); + function allowance(address owner, address spender) external view returns (uint256); + function transferFrom(address from, address to, uint256 amount) external returns (bool); + function approve(address spender, uint256 amount) external returns (bool); + + event Transfer(address indexed from, address indexed to, uint256 value); + event Approval(address indexed owner, address indexed spender, uint256 value); +} +``` + +[ERC-165](./eip-165.md) interface for standard interface detection: + +```solidity +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +interface IERC165 { + /** + * @notice Query if a contract implements an interface + * @param interfaceId The interface identifier, as specified in [ERC-165](./eip-165.md) + * @return bool True if the contract implements `interfaceId` + */ + function supportsInterface(bytes4 interfaceId) external view returns (bool); +} +``` + +[ERC-20](./eip-20.md) is extended as follows: + +```solidity +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +/** + * @title ERC-1450: RTA-Controlled Security Token (Restricted ERC-20 Interface) + * @notice Facilitates compliance with Securities Act Regulations CF, D, and A + * @dev This standard extends ERC-20 with RTA-controlled transfer restrictions + * + * Key Features: + * - RTA (Registered Transfer Agent) exclusive control over transfers + * - Direct value movement disabled (transfer, approve functions always revert) + * - Holder-initiated transfer requests permitted via requestTransferWithFee (requires RTA execution) + * - Built-in recovery mechanisms for lost tokens + * - Support for court-ordered transfers + * - Restricted ERC-20 interface for read operations and ecosystem integration + * - ERC-6093 compliant error messages for tooling interoperability + */ +interface IERC1450 is IERC20, IERC165 { + // ============ ERC-6093 Standard Errors ============ + // Using standard errors from ERC-6093 for consistent tooling support + + // Standard ERC-20 errors (from ERC-6093) + error ERC20InsufficientBalance(address sender, uint256 balance, uint256 needed); + error ERC20InvalidSender(address sender); + error ERC20InvalidReceiver(address receiver); + error ERC20InvalidApprover(address approver); + error ERC20InvalidSpender(address spender); + error ERC20InsufficientAllowance(address spender, uint256 allowance, uint256 needed); + + // Standard Access Control errors (from ERC-6093) + // NOTE: We retain OpenZeppelin error names for tooling compatibility, but semantics differ: + // - "Owner" in ERC-1450 means "Issuer" (the entity that created the token) + // - Unlike OpenZeppelin's Ownable, only the RTA can change the issuer, not the issuer themselves + error OwnableUnauthorizedAccount(address account); // Non-issuer attempts issuer-only operation + error OwnableInvalidOwner(address owner); // Invalid issuer address (e.g., zero address) + + // ============ ERC-1450 Specific Errors ============ + // Custom errors only when ERC-6093 standard errors are insufficient + + error ERC1450TransferDisabled(); // For disabled transfer/approve functions + error ERC1450OnlyRTA(); // Operation restricted to RTA only + error ERC1450TransferAgentLocked(); // RTA proxy is locked from changes + error ERC1450ComplianceCheckFailed(address from, address to); // KYC/AML failure + + // Events + event IssuerChanged(address indexed previousIssuer, address indexed newIssuer); + event TransferAgentUpdated(address indexed previousAgent, address indexed newAgent); + + /** + * @notice Emitted when tokens are minted with regulation tracking + * @param to Recipient of the minted tokens + * @param amount Number of tokens minted + * @param regulationType Regulation under which tokens were issued + * @param issuanceDate Original share issuance date + * @param tokenizationDate When tokenized on blockchain (block.timestamp) + * @dev MUST be emitted for every mint operation + * Allows reconstruction of entire cap table from events + * Critical for regulatory reporting and audit trails + */ + event TokensMinted( + address indexed to, + uint256 amount, + uint16 indexed regulationType, + uint256 issuanceDate, + uint256 tokenizationDate + ); + + /** + * @notice Emitted when tokens are burned with regulation tracking + * @param from Address from which tokens were burned + * @param amount Number of tokens burned + * @param regulationType Regulation type of burned tokens + * @param issuanceDate Original issuance date of burned tokens + * @dev MUST be emitted for every burn operation + * Critical for maintaining accurate cap table and regulatory reporting + */ + event TokensBurned( + address indexed from, + uint256 amount, + uint16 indexed regulationType, + uint256 issuanceDate + ); + + /** + * @notice Emitted when tokens are transferred with specific regulation tracking + * @param from Source address + * @param to Destination address + * @param amount Number of tokens transferred + * @param regulationType Regulation type of the transferred tokens + * @param issuanceDate Original issuance date of the transferred tokens + * @dev Provides complete traceability of regulated token movements + * Essential for compliance reporting and audit trails + */ + event RegulatedTransfer( + address indexed from, + address indexed to, + uint256 amount, + uint16 indexed regulationType, + uint256 issuanceDate + ); + + // Core RTA Functions + + /** + * @notice Change the issuer (owner) of the token contract + * @param newIssuer Address of the new issuer + * @dev Only callable by the RTA. Must be restricted with onlyTransferAgent modifier. + * Emits IssuerChanged event (not OwnershipTransferred) + * + * IMPORTANT: This differs from OpenZeppelin's Ownable pattern: + * - In Ownable: owner can transfer ownership themselves + * - In ERC-1450: ONLY the RTA can change the issuer + * - Terminology: "Issuer" = the token creator/owner, not the controller + * - This prevents compromised issuer keys from hijacking the token + * + * The issuer maintains rights to: + * - Receive proceeds from offerings + * - Update corporate documents (via ERC-1643) + * - Make business decisions + * But CANNOT control token transfers or change the RTA + */ + function changeIssuer(address newIssuer) external; + + /** + * @notice Update the transfer agent address (one-time use or RTA-only after initial setup) + * @param newTransferAgent Address of the new transfer agent (should be RTAProxy contract) + * @dev After initial setup to RTAProxy, only the RTA can rotate itself via the proxy. + * This prevents compromised issuers from changing the RTA. + */ + function setTransferAgent(address newTransferAgent) external; + + /** + * @notice Check if an address is the current transfer agent + * @param account Address to check + * @return bool True if the address is the current transfer agent + */ + function isTransferAgent(address account) external view returns (bool); + + // ERC-20 Overrides (Restricted Functions) + + /** + * @notice Transfer tokens - DISABLED for security tokens + * @dev Must always revert with ERC1450TransferDisabled() + * Uses specific error for disabled functionality per [ERC-6093](./eip-6093.md) guidelines + */ + function transfer(address to, uint256 amount) external override returns (bool); + + /** + * @notice Approve spending - DISABLED for security tokens + * @dev Must always revert with ERC1450TransferDisabled() + * Uses specific error for disabled functionality per [ERC-6093](./eip-6093.md) guidelines + */ + function approve(address spender, uint256 amount) external override returns (bool); + + /** + * @notice Get spending allowance - DISABLED for security tokens + * @dev Must always return 0 + */ + function allowance(address owner, address spender) external view override returns (uint256); + + /** + * @notice Transfer tokens - DISABLED for security tokens + * @dev Must always revert with ERC1450TransferDisabled() + * Uses specific error for disabled functionality per [ERC-6093](./eip-6093.md) guidelines + * Use transferFromRegulated() for actual transfers with regulation tracking + */ + function transferFrom(address from, address to, uint256 amount) external override returns (bool); + + // RTA-Controlled Functions + + /** + * @notice Transfer tokens between accounts with regulation tracking (RTA only) + * @param from Source address + * @param to Destination address + * @param amount Number of tokens to transfer + * @param regulationType Type of regulation for the transferred tokens + * @param issuanceDate Original issuance date of the transferred tokens + * @dev Only callable by the registered transfer agent + * The regulation type and issuance date specify which tokens to transfer + * MUST revert if sender has insufficient tokens of the specified regulation/issuance + * Callers SHOULD first check holdings via getHolderRegulations() or getDetailedBatchInfo() + * This enables precise control over which token batches are moved + * The RTA determines the transfer strategy (FIFO, LIFO, tax optimization, etc.) + */ + function transferFromRegulated(address from, address to, uint256 amount, uint16 regulationType, uint256 issuanceDate) external returns (bool); + + /** + * @notice Mint new tokens with regulation tracking (RTA only) + * @param to Address to receive the minted tokens + * @param amount Number of tokens to mint + * @param regulationType Type of regulation under which shares were issued (uint16 for global compatibility) + * @param issuanceDate Unix timestamp when shares were originally issued (not tokenization date) + * @dev Only callable by the registered transfer agent + * Every security MUST specify a regulation type - there are no "unregulated" securities + * For tokenizing existing securities, issuanceDate should be when investor originally purchased + * For new issuances, issuanceDate would typically be block.timestamp + * RTA uses issuanceDate to calculate holding periods for regulatory compliance + */ + function mint(address to, uint256 amount, uint16 regulationType, uint256 issuanceDate) external returns (bool); + + /** + * @notice Batch mint tokens with regulation tracking (RTA only) + * @param recipients Array of addresses to receive the minted tokens + * @param amounts Array of token amounts to mint for each recipient + * @param regulationTypes Array of regulation types for each mint + * @param issuanceDates Array of issuance timestamps for each mint + * @dev Only callable by the registered transfer agent + * All arrays MUST be the same length + * Each mint operation follows the same rules as individual mint() + * Reverts if any single mint would fail + * Emits TokensMinted event for each successful mint + * Enables efficient bulk issuance while maintaining compliance tracking + */ + function batchMint( + address[] calldata recipients, + uint256[] calldata amounts, + uint16[] calldata regulationTypes, + uint256[] calldata issuanceDates + ) external returns (bool); + + /** + * @notice Burn tokens from an account (RTA only) - Uses RTA's chosen strategy + * @param from Address from which to burn tokens + * @param amount Number of tokens to burn + * @dev Only callable by the registered transfer agent + * The RTA determines which tokens to burn based on their strategy (FIFO, LIFO, tax optimization, etc.) + * Use burnFromRegulated() to burn specific regulation/issuance tokens + */ + function burnFrom(address from, uint256 amount) external returns (bool); + + /** + * @notice Burn tokens from an account with regulation tracking (RTA only) + * @param from Address from which to burn tokens + * @param amount Number of tokens to burn + * @param regulationType Type of regulation for the tokens to burn + * @param issuanceDate Original issuance date of the tokens to burn + * @dev Only callable by the registered transfer agent + * Burns specific tokens identified by regulation type and issuance date + * MUST revert if holder has insufficient tokens of the specified regulation/issuance + * Callers SHOULD first check holdings via getHolderRegulations() or getDetailedBatchInfo() + * MUST emit TokensBurned event with the specific regulation details + */ + function burnFromRegulated(address from, uint256 amount, uint16 regulationType, uint256 issuanceDate) external returns (bool); + + /** + * @notice Burn tokens of a specific regulation type (RTA only) + * @param from Address from which to burn tokens + * @param amount Number of tokens to burn + * @param regulationType Specific regulation type to burn + * @dev Only callable by the registered transfer agent + * Useful for partial redemptions, buybacks, or regulation-specific corporate actions + * Reverts if holder has insufficient tokens of the specified regulation type + * MUST emit TokensBurned event with the specific regulation details + */ + function burnFromRegulation(address from, uint256 amount, uint16 regulationType) external returns (bool); + + /** + * @notice Get token decimals (OPTIONAL per [EIP-20](./eip-20.md)) + * @return uint8 The number of decimal places (0-18) + * @dev Set at deployment based on security type: + * - 0 for traditional indivisible shares + * - Greater than 0 for fractional shares (mutual funds, REITs, fractional trading) + * - Must be immutable after deployment + * + * NOTE: Per [EIP-20](./eip-20.md), name(), symbol(), and decimals() are OPTIONAL + * Implementations SHOULD provide these for better UX + * Wallets MUST NOT assume these functions exist + */ + function decimals() external view returns (uint8); + + // Introspection for Restricted ERC-20 Interface Detection + + /** + * @notice Check if this is a security token with restricted transfers + * @return bool Always returns true for ERC-1450 tokens + * @dev Critical for wallets/DEXs to detect restricted tokens and handle appropriately. + * Prevents users from attempting transfers that will always fail. + */ + function isSecurityToken() external pure returns (bool); + + /** + * @notice Returns the contract implementation version + * @return string Semantic version string (e.g., "1.17.0") + * @dev Enables upgrade detection for UUPS-upgradeable contracts. + * Version SHOULD match the reference implementation package version. + * Useful for: + * - Detecting available upgrades by comparing deployed vs local version + * - Audit trails showing which version was deployed + * - Debugging and support by identifying exact implementation + */ + function version() external pure returns (string memory); + + /** + * @notice [EIP-165](./eip-165.md) support for interface detection + * @param interfaceId The interface identifier to check + * @return bool True if the contract implements the interface + * @dev MUST return true for: + * - 0x01ffc9a7: [ERC-165](./eip-165.md) interface ID + * - 0xXXXXXXXX: IERC1450 interface ID (see Interface Detection section for calculation) + * + * MUST return false for: + * - 0x36372b07: [ERC-20](./eip-20.md) interface ID + * + * Returning false for [ERC-20](./eip-20.md) prevents wallets from assuming standard transfer behavior. + * While we are ABI-compatible with [ERC-20](./eip-20.md), we are not behaviorally compatible + * since transfer() and approve() always revert. + */ + function supportsInterface(bytes4 interfaceId) external view returns (bool); + + // KYC/AML Status Check (OPTIONAL) + + /** + * @notice Check if an address has been KYC verified (OPTIONAL) + * @param account Address to check + * @return verified Whether the address is linked to a verified person + * @return expiryDate When the KYC verification expires (0 if not verified) + * @dev This is a view function that queries the RTA's off-chain database + * Never returns actual identity information, only verification status + * Implementations MAY choose to implement this for transparency + * Returns false for addresses that have never been verified + */ + function isKYCVerified(address account) external view returns (bool verified, uint256 expiryDate); + + // Regulation Tracking Query Functions + + /** + * @notice Get regulation information for tokens held by an address + * @param holder Address to query + * @return regulationTypes Array of regulation types for holder's tokens + * @return amounts Array of token amounts per regulation type + * @return issuanceDates Array of original issuance dates + * @dev MUST return arrays of equal length representing holder's token composition + * Implementation MUST store this data persistently on-chain + * RTA uses this for transfer calculations and compliance checks + */ + function getHolderRegulations(address holder) external view returns ( + uint16[] memory regulationTypes, + uint256[] memory amounts, + uint256[] memory issuanceDates + ); + + /** + * @notice Get total tokens minted under a specific regulation + * @param regulationType The regulation type to query + * @return totalSupply Total tokens minted under this regulation + * @dev Useful for regulatory reporting and tracking offering limits + */ + function getRegulationSupply(uint16 regulationType) external view returns (uint256 totalSupply); + + /** + * @notice Get detailed batch information for a holder's tokens + * @param holder Address to query + * @return count Number of unique batches the holder has + * @return regulationTypes Array of regulation types for each batch + * @return issuanceDates Array of issuance dates for each batch + * @return amounts Array of token amounts for each batch + * @dev Returns comprehensive batch-level details for a holder + * Useful for detailed cap table management and regulatory reporting + * Each index represents a unique batch of tokens with specific regulation/issuance + */ + function getDetailedBatchInfo(address holder) external view returns ( + uint256 count, + uint16[] memory regulationTypes, + uint256[] memory issuanceDates, + uint256[] memory amounts + ); + + // Batch Operations for Gas Efficiency + + /** + * @notice Batch transfer tokens between multiple address pairs with regulation tracking (RTA only) + * @param froms Array of source addresses + * @param tos Array of destination addresses + * @param amounts Array of token amounts to transfer + * @param regulationTypes Array of regulation types for each transfer + * @param issuanceDates Array of issuance dates for each transfer + * @return bool True if all transfers succeed + * @dev All arrays must have equal length. Reverts if any transfer fails. + * Only callable by the registered transfer agent. + * MUST revert if any sender has insufficient tokens of the specified regulation/issuance + * The RTA determines the transfer strategy for each transfer + * Useful for dividend distributions, corporate actions, etc. + */ + function batchTransferFrom( + address[] calldata froms, + address[] calldata tos, + uint256[] calldata amounts, + uint16[] calldata regulationTypes, + uint256[] calldata issuanceDates + ) external returns (bool); + + /** + * @notice Batch burn tokens from multiple addresses with regulation tracking (RTA only) + * @param froms Array of addresses from which to burn tokens + * @param amounts Array of token amounts to burn from each address + * @param regulationTypes Array of regulation types for each burn + * @param issuanceDates Array of issuance dates for each burn + * @return bool True if all burns succeed + * @dev All arrays must have equal length. Reverts if any burn fails. + * Only callable by the registered transfer agent. + * MUST revert if any holder has insufficient tokens of the specified regulation/issuance + * Useful for redemptions, corporate buybacks, etc. + */ + function batchBurnFrom( + address[] calldata froms, + uint256[] calldata amounts, + uint16[] calldata regulationTypes, + uint256[] calldata issuanceDates + ) external returns (bool); + + // Fee Collection Mechanism + + /** + * @notice Register or deregister a broker for this token (RTA only) + * @param broker Address of the broker to register/deregister + * @param isApproved Whether to approve or revoke broker status + * @dev Only callable by RTA after due diligence. Brokers must: + * 1. Apply off-chain to the RTA + * 2. Pass KYC/AML and regulatory checks + * 3. Sign broker agreement + * 4. Be approved by RTA compliance team + * + * RECOMMENDED: Brokers SHOULD use a proxy pattern similar to RTAProxy + * for key management and operational security. This allows: + * - Secure key rotation without re-registration + * - Multi-signature controls for broker operations + * - Business continuity during personnel changes + * - Protection against individual key compromise + * + * The broker address registered here MAY be: + * - Direct broker-controlled address (simple but less secure) + * - BrokerProxy contract address (recommended for production) + */ + function setBrokerStatus(address broker, bool isApproved) external; + + /** + * @notice Check if an address is a registered broker + * @param broker Address to check + * @return bool True if the address is an approved broker + */ + function isRegisteredBroker(address broker) external view returns (bool); + + /** + * @notice Request a transfer with fee payment + * @param from Source address for the transfer + * @param to Destination address for the transfer + * @param amount Number of tokens to transfer + * @param feeAmount Amount of fee being paid (in the configured fee token) + * @return requestId Unique identifier for this request (for idempotency and tracking) + * @dev Creates a new transfer request with initial status: RequestStatus.Requested + * + * Fee payment: + * - Fee MUST be paid in the configured fee token (see getFeeToken()) + * - Caller MUST have approved the token contract to spend feeAmount + * - Fee is transferred via safeTransferFrom from msg.sender + * + * Fee payment responsibility: + * - Holder-initiated (msg.sender == from): Holder pays the fee + * - Broker-initiated (msg.sender != from): Broker pays fee on behalf of client + * - Settlement failures: Fees are NOT automatically refunded (RTA discretion) + * + * Request lifecycle: + * 1. Request created with unique requestId (status: Requested) + * 2. RTA reviews request (status: UnderReview - optional) + * 3. RTA approves/rejects (status: Approved or Rejected) + * 4. If approved, RTA executes transfer (status: Executed) + * 5. Optional: Request expires after timeout (status: Expired) + * + * Idempotency guarantee: + * - Each requestId is unique and can only be executed ONCE + * - Duplicate execution attempts MUST revert + * - Clients can safely retry requests with new requestId if needed + * + * Authorization requirements: + * - If msg.sender == from: Token holder requesting their own transfer + * - If msg.sender != from: Must be a registered broker (via setBrokerStatus) + * - Reverts if neither condition is met + * + * Implementation MUST: + * - Verify fee token is configured (revert if not) + * - Generate unique requestId for each request + * - Emit TransferRequested event + * - Store request details for later processing + * - Prevent double-spending by tracking request status + */ + function requestTransferWithFee( + address from, + address to, + uint256 amount, + uint256 feeAmount + ) external returns (uint256 requestId); + + /** + * @notice Request transfer with [EIP-2612](./eip-2612.md) permit for gasless fee approval (OPTIONAL) + * @param from Source address for the transfer + * @param to Destination address for the transfer + * @param amount Number of tokens to transfer + * @param feeAmount Amount of fee being paid (in the configured fee token) + * @param deadline Permit signature deadline + * @param v ECDSA signature v parameter + * @param r ECDSA signature r parameter + * @param s ECDSA signature s parameter + * @return requestId Unique identifier for this transfer request + * @dev OPTIONAL: Allows fee payment without prior approve() transaction + * Uses [EIP-2612](./eip-2612.md) permit for gasless approval of fee token + * Particularly useful for first-time users who don't have fee token approvals + * MUST revert if the configured fee token doesn't support [EIP-2612](./eip-2612.md) + * Example: USDC, DAI, and other modern stablecoins support permit + * + * Implementation: + * 1. Call permit on the configured fee token with the provided signature + * 2. Then execute the same logic as requestTransferWithFee + * 3. This avoids users needing a separate approve transaction for fees + */ + function requestTransferWithPermit( + address from, + address to, + uint256 amount, + uint256 feeAmount, + uint256 deadline, + uint8 v, + bytes32 r, + bytes32 s + ) external returns (uint256 requestId); + + /** + * @notice Get current fee for a transfer + * @param from Source address + * @param to Destination address + * @param amount Transfer amount + * @return feeAmount Required fee amount in the configured fee token + * @dev Allows dynamic fee calculation based on transfer parameters + * Fee is always denominated in the configured fee token (see getFeeToken()) + * Fee type determines calculation: + * - Type 0 (flat): Returns feeValue directly + * - Type 1 (percentage): Returns (amount * feeValue) / 10000 + */ + function getTransferFee(address from, address to, uint256 amount) + external view returns (uint256 feeAmount); + + /** + * @notice Get the configured fee token address + * @return token The [ERC-20](./eip-20.md) token used for fee payments + * @dev Returns address(0) if no fee token is configured yet + * RTA MUST configure a fee token before transfers can be requested + * Common choices: USDC, USDT, or other stablecoins + */ + function getFeeToken() external view returns (address token); + + /** + * @notice Set the fee token (RTA only) + * @param newFeeToken Address of the [ERC-20](./eip-20.md) token to use for fees + * @dev Only callable by RTA to configure or change the fee token + * MUST emit FeeTokenUpdated event + * MUST NOT accept address(0) - use a stablecoin like USDC + * Changing fee token does not affect pending transfer requests + */ + function setFeeToken(address newFeeToken) external; + + /** + * @notice Set fee parameters (RTA only) + * @param feeType Type of fee structure (0: flat, 1: percentage in basis points) + * @param feeValue Fee amount or percentage basis points + * @dev Only callable by RTA to update fee structure + * MUST emit FeeParametersUpdated event + * For percentage fees, feeValue is in basis points (100 = 1%) + */ + function setFeeParameters(uint8 feeType, uint256 feeValue) external; + + /** + * @notice Withdraw collected fees (RTA only) + * @param amount Amount to withdraw (in the configured fee token) + * @param recipient Recipient address for fees + * @dev Only callable by RTA to collect accumulated fees + * Withdraws from the configured fee token balance + * MUST revert if insufficient collected fees + */ + function withdrawFees(uint256 amount, address recipient) external; + + /** + * @notice Process pending transfer request (RTA only) + * @param requestId ID of the transfer request + * @param approved Whether to approve or reject the transfer + * @dev RTA reviews and processes transfer requests after compliance checks + * MUST transition request through proper lifecycle states + * MUST emit events for each state transition + */ + function processTransferRequest(uint256 requestId, bool approved) external; + + /** + * @notice Reject transfer request with specific reason code (RTA only) + * @param requestId ID of the transfer request to reject + * @param reasonCode Rejection reason code (see Reason Codes section) + * @param refundFee Whether to refund the fee to the requester + * @dev Convenience function for rejections with detailed reason tracking + * MUST transition request status to Rejected + * MUST emit TransferRejected event with reasonCode and refundFee flag + * If refundFee is true, MUST return the fee to the original payer + * Alternative to processTransferRequest(requestId, false) with more detail + */ + function rejectTransferRequest(uint256 requestId, uint16 reasonCode, bool refundFee) external; + + // Transfer Request Lifecycle Management + + /** + * @notice Request lifecycle states + * @dev Requests MUST follow this state machine: + * Requested → UnderReview → (Approved | Rejected) → Executed + * + * State transitions: + * - Requested: Initial state when requestTransferWithFee is called + * - UnderReview: RTA begins compliance checks (optional intermediate state) + * - Approved: RTA approves after compliance checks pass + * - Rejected: RTA rejects for compliance or other reasons + * - Executed: Transfer completed and tokens moved + * - Expired: Request timed out without processing (if timeouts implemented) + * + * Idempotency: Each requestId MUST be unique and can only be executed ONCE + */ + enum RequestStatus { + Requested, // 0: Initial state + UnderReview, // 1: RTA is reviewing + Approved, // 2: Approved, awaiting execution + Rejected, // 3: Rejected, terminal state + Executed, // 4: Successfully executed, terminal state + Expired // 5: Timed out, terminal state + } + + /** + * @notice Get current status of a transfer request (OPTIONAL) + * @param requestId The transfer request ID + * @dev OPTIONAL: Implementations MAY instead expose equivalent request data + * through other read methods (e.g., a public storage mapping). + * @return status Current RequestStatus + * @return from Source address + * @return to Destination address + * @return amount Transfer amount + * @return requestedAt Timestamp when request was created + * @return processedAt Timestamp when request was processed (0 if pending) + */ + function getRequestStatus(uint256 requestId) external view returns ( + RequestStatus status, + address from, + address to, + uint256 amount, + uint256 requestedAt, + uint256 processedAt + ); + + /** + * @notice Update request status (RTA only) + * @param requestId The transfer request ID + * @param newStatus New status to transition to + * @dev MUST validate state transitions according to lifecycle rules + * MUST emit RequestStatusChanged event + * Invalid transitions MUST revert + */ + function updateRequestStatus(uint256 requestId, RequestStatus newStatus) external; + + // Events for transfer request lifecycle + event TransferRequested( + uint256 indexed requestId, + address indexed from, + address indexed to, + uint256 amount, + uint256 feePaid, + address requestedBy // holder or broker + ); + + event RequestStatusChanged( + uint256 indexed requestId, + RequestStatus indexed oldStatus, + RequestStatus indexed newStatus, + uint256 timestamp + ); + + event TransferExecuted( + uint256 indexed requestId, + address indexed from, + address indexed to, + uint256 amount + ); + + event TransferRejected( + uint256 indexed requestId, + uint16 reasonCode, // Gas-efficient reason codes (see Reason Codes section) + bool feeRefunded // Whether fee was refunded + ); + + event TransferExpired( + uint256 indexed requestId, + uint256 expiredAt + ); + + // Fee-related events + event FeeParametersUpdated(uint8 feeType, uint256 feeValue); + event FeeTokenUpdated(address indexed previousToken, address indexed newToken); + event FeesWithdrawn(uint256 amount, address indexed recipient); + event BrokerStatusUpdated(address indexed broker, bool isApproved, address indexed updatedBy); + + // Account restriction events + event AccountFrozen(address indexed account, bool frozen, address indexed frozenBy); + + // Reason Codes for Transfer Rejection + // Gas-efficient uint16 codes instead of strings + uint16 constant REASON_COMPLIANCE_FAILED = 1; // KYC/AML check failed + uint16 constant REASON_INSUFFICIENT_BALANCE = 2; // Sender lacks tokens + uint16 constant REASON_RESTRICTED_ACCOUNT = 3; // Account is frozen/restricted + uint16 constant REASON_TRANSFER_WINDOW_CLOSED = 4; // Outside allowed transfer window + uint16 constant REASON_EXCEEDS_HOLDING_LIMIT = 5; // Would exceed max holding + uint16 constant REASON_REGULATORY_HALT = 6; // Trading halted by regulator + uint16 constant REASON_COURT_ORDER = 7; // Blocked by court order + uint16 constant REASON_INVALID_RECIPIENT = 8; // Recipient not whitelisted + uint16 constant REASON_LOCK_PERIOD = 9; // Tokens are locked + uint16 constant REASON_RECIPIENT_NOT_VERIFIED = 10; // Recipient hasn't completed KYC/AML + uint16 constant REASON_ADDRESS_NOT_LINKED = 11; // Address not linked to verified identity + uint16 constant REASON_SENDER_VERIFICATION_EXPIRED = 12; // Sender's KYC expired + uint16 constant REASON_JURISDICTION_BLOCKED = 13; // Recipient in restricted jurisdiction + uint16 constant REASON_ACCREDITATION_REQUIRED = 14; // Recipient not accredited (Reg D) + uint16 constant REASON_OTHER = 999; // Other/unspecified reason + + // Fee Refund Policy + /** + * @notice Fee refund policy for rejected/expired transfers + * @dev Implementations MUST clearly document their refund policy: + * Option 1: AUTO_REFUND - Fees automatically refunded on rejection/expiry + * Option 2: NO_REFUND - Fees retained for processing costs + * Option 3: CONDITIONAL_REFUND - Refund based on reason code + * + * The refund policy SHOULD be: + * - Clearly documented in the contract + * - Communicated to users before fee payment + * - Consistently applied across all transfers + * - Emit TransferRejected event with feeRefunded flag + */ + + // Optional: Off-chain Compliance Pre-check (EIP-3668 CCIP-Read) + + /** + * @notice Pre-check transfer compliance off-chain (OPTIONAL) + * @param from Source address for the transfer + * @param to Destination address for the transfer + * @param amount Number of tokens to transfer + * @return bool True if transfer would likely pass compliance + * @dev OPTIONAL: Implements EIP-3668 (CCIP-Read) for off-chain compliance checks + * + * This check SHOULD verify: + * 1. Sender KYC/AML status is current (not expired) + * 2. Recipient has passed KYC/AML verification + * 3. Recipient address ownership is verified and linked to identity + * 4. Transfer doesn't violate jurisdiction restrictions + * 5. Accreditation requirements are met (if applicable for Reg D/Reg A) + * + * This function MAY revert with OffchainLookup error containing: + * - URLs for off-chain compliance services + * - Callback function to process off-chain response + * + * Purpose: Allow wallets to pre-screen transfers before users pay gas + * Benefits: + * - Show specific compliance failure reasons upfront + * - Improve UX by preventing failed transactions + * - Reduce wasted gas on non-compliant transfers + * + * IMPORTANT: This check is ADVISORY ONLY and NOT BINDING + * - The actual transfer still requires RTA approval + * - Compliance status may change between pre-check and execution + * - RTA has final authority on all transfers + * + * Example implementation: + * ```solidity + * error OffchainLookup(address sender, string[] urls, bytes callData, + * bytes4 callbackFunction, bytes extraData); + * + * function preCheckCompliance(address from, address to, uint256 amount) + * external view returns (bool) { + * revert OffchainLookup( + * address(this), + * urls, // RTA's compliance API endpoints + * abi.encodeWithSignature("checkCompliance(address,address,uint256)", from, to, amount), + * this.preCheckComplianceCallback.selector, + * "" + * ); + * } + * ``` + * + * Wallets supporting [EIP-3668](./eip-3668.md) will: + * 1. Catch the OffchainLookup error + * 2. Query the specified URLs with the provided callData + * 3. Call the callback function with response and UNMODIFIED extraData + * 4. Display compliance status to user based on callback result + * 5. Allow/prevent transfer request submission accordingly + */ + function preCheckCompliance(address from, address to, uint256 amount) + external view returns (bool); + + /** + * @notice Callback for processing off-chain compliance response (OPTIONAL) + * @param response Encoded response from off-chain compliance service + * @param extraData Additional data from original request (passed through unmodified) + * @return bool Compliance check result + * @dev Only called by wallets supporting EIP-3668 + * Validates and interprets off-chain compliance service response + * + * Per [EIP-3668](./eip-3668.md) specification: + * - Clients MUST pass extraData unmodified from the OffchainLookup error + * - The callback signature MUST match EIP-3668's standard format + * - This enables stateless operation and future extensibility + * + * Example client implementation: + * 1. Catch OffchainLookup(sender, urls, callData, callbackFunction, extraData) + * 2. Query off-chain service with callData + * 3. Call callbackFunction(response, extraData) with UNMODIFIED extraData + */ + function preCheckComplianceCallback(bytes calldata response, bytes calldata extraData) + external pure returns (bool); + + // ============ ERC-1643 Document Management Interface (OPTIONAL) ============ + // Implementations MAY support on-chain anchoring of document references. + // RTAs already maintain authoritative books and records off-chain per their + // regulatory obligations; on-chain anchoring is an optional transparency + // enhancement, not a compliance requirement. + + /** + * @notice Set a document for the token (RTA only) (OPTIONAL) + * @param _name Document name (unique identifier) + * @param _uri Document location (IPFS hash or HTTPS URL) + * @param _documentHash Hash of the document for verification + * @dev Part of ERC-1643 standard. Only callable by RTA. + * Used for storing references to legal documents, compliance certificates, + * court orders, recovery evidence, physical addresses, etc. + * Never store PII directly on-chain - use document URIs and hashes only. + * + * Common document names: + * - "PHYSICAL_ADDRESS": Issuer's registered address + * - "PROSPECTUS": Offering documents + * - "COURT_ORDER": Legal judgments + * - "RECOVERY_EVIDENCE": Lost wallet documentation + * + * Implementations providing this function MUST emit DocumentUpdated. + */ + function setDocument( + bytes32 _name, + string calldata _uri, + bytes32 _documentHash + ) external; + + /** + * @notice Get a specific document's details (OPTIONAL) + * @param _name Document name to retrieve + * @return documentUri Document location + * @return documentHash Document hash for verification + * @return timestamp When document was last updated + * @dev Part of ERC-1643 standard. Publicly accessible. + */ + function getDocument(bytes32 _name) + external view + returns ( + string memory documentUri, + bytes32 documentHash, + uint256 timestamp + ); + + /** + * @notice Remove a document (RTA only) (OPTIONAL) + * @param _name Document name to remove + * @dev Part of ERC-1643 standard. Only callable by RTA. + * Implementations providing this function MUST emit DocumentRemoved. + */ + function removeDocument(bytes32 _name) external; + + /** + * @notice Get all document names (OPTIONAL) + * @return Array of all document names that have been set + * @dev Part of ERC-1643 standard. Publicly accessible. + * Allows discovery of all documents associated with the token. + */ + function getAllDocuments() external view returns (bytes32[] memory); + + // ============ Recovery Workflow for Lost/Compromised Wallets (OPTIONAL) ============ + // Uses ERC-1643 for document management and aligns with ERC-1644 for controller operations. + // OPTIONAL: This structured, time-locked workflow is an extension. The REQUIRED + // baseline for recovery is controllerTransfer — the RTA verifies the investor's + // identity through its existing off-chain KYC records and executes the transfer. + // The structured workflow adds public evidence anchoring and a dispute window + // for deployments that want on-chain transparency of recovery operations. + + /** + * @notice Initiate recovery process for lost wallet (RTA only) (OPTIONAL) + * @param lostWallet Address that has lost access + * @param newWallet Proposed replacement address + * @param documentName Name/type of the supporting document (ERC-1643 compatible) + * @param uri URI pointing to the evidence document (IPFS/HTTPS) + * @param documentHash Hash of the document for integrity verification + * @return recoveryId Unique identifier for the recovery request + * @dev Creates a time-locked recovery request requiring multi-step verification: + * 1. Identity verification of the claiming party + * 2. Proof of ownership (off-chain documentation) + * 3. Time delay for potential disputes (e.g., 30 days) + * 4. Final execution after time lock expires + * + * Following ERC-1643 document management standards: + * - documentName: Type of evidence (e.g., "RECOVERY_AFFIDAVIT", "DEATH_CERTIFICATE") + * - uri: Link to encrypted document storage (never store PII on-chain) + * - documentHash: Cryptographic hash for document integrity + * + * Implementations providing this function MUST emit DocumentUpdated + * per ERC-1643 when evidence is submitted + */ + function initiateRecovery( + address lostWallet, + address newWallet, + bytes32 documentName, + string calldata uri, + bytes32 documentHash + ) external returns (uint256 recoveryId); + + /** + * @notice Cancel a pending recovery request (RTA only) (OPTIONAL) + * @param recoveryId The recovery request to cancel + * @dev Can be called if: + * - Original wallet owner proves they still have access + * - Evidence is found to be fraudulent + * - Court order requires cancellation + */ + function cancelRecovery(uint256 recoveryId) external; + + /** + * @notice Execute recovery after time lock expires (RTA only) (OPTIONAL) + * @param recoveryId The recovery request to execute + * @dev Transfers all tokens from lost wallet to new wallet + * Can only be executed after time lock period (e.g., 30 days) + * Implementations providing this function MUST emit ControllerTransfer + * per ERC-1644 + */ + function executeRecovery(uint256 recoveryId) external; + + /** + * @notice Controller transfer for court orders (RTA only) - ERC-1644 compatible + * @param from Source address + * @param to Destination address + * @param amount Number of tokens + * @param data Encoded data containing document references + * @param operatorData Encoded document information per ERC-1643: + * - documentName: Type (e.g., "COURT_ORDER", "REGULATORY_ACTION") + * - uri: Link to encrypted document storage + * - documentHash: Cryptographic hash for integrity + * @dev Immediate transfer without time lock for: + * - Court-ordered transfers (divorce, judgments) + * - Regulatory enforcement actions + * - Estate distributions with proper documentation + * + * Following ERC-1644 controller operation standards: + * - MUST emit ControllerTransfer event + * - Uses standard function name for tooling compatibility + * + * Following ERC-1643 document management: + * - MUST emit DocumentUpdated event when court order is attached + * - Never store PII on-chain, only document URIs and hashes + */ + function controllerTransfer( + address from, + address to, + uint256 amount, + bytes calldata data, + bytes calldata operatorData + ) external; + + // ============ Account Freezing ============ + + /** + * @notice Freeze or unfreeze an account (RTA only) + * @param account Address to freeze/unfreeze + * @param frozen True to freeze, false to unfreeze + * @dev Frozen accounts cannot send or receive tokens + * Used for compliance holds, regulatory actions, or suspicious activity + * Only RTA can freeze/unfreeze accounts + * MUST emit AccountFrozen event + */ + function setAccountFrozen(address account, bool frozen) external; + + /** + * @notice Check if an account is frozen + * @param account Address to check + * @return bool True if the account is frozen + * @dev Publicly accessible for transparency + * Wallets and exchanges can check before attempting transfers + */ + function isAccountFrozen(address account) external view returns (bool); + + // ============ Recovery System (OPTIONAL) ============ + + /** + * @notice Get recovery request details (OPTIONAL) + * @param recoveryId The recovery request ID + * @return lostWallet The wallet being recovered + * @return newWallet The replacement wallet + * @return documentName The type of evidence document (ERC-1643) + * @return documentUri The URI of the evidence document + * @return documentHash The hash of the evidence document + * @return initiatedAt Timestamp when recovery was initiated + * @return status Current status (pending/executed/cancelled) + */ + function getRecoveryDetails(uint256 recoveryId) + external view returns ( + address lostWallet, + address newWallet, + bytes32 documentName, + string memory documentUri, + bytes32 documentHash, + uint256 initiatedAt, + uint8 status + ); + + /** + * @notice Check if a wallet has a pending recovery + * @param wallet Address to check + * @return bool True if wallet has pending recovery + * @return uint256 Recovery ID if exists, 0 otherwise + */ + function hasPendingRecovery(address wallet) + external view returns (bool, uint256); + + // Standard Events from ERC-1644 (Controller Operations) + event ControllerTransfer( + address indexed controller, + address indexed from, + address indexed to, + uint256 value, + bytes data, + bytes operatorData + ); + + event ControllerRedemption( + address indexed controller, + address indexed tokenHolder, + uint256 value, + bytes data, + bytes operatorData + ); + + // ERC-1643 Standard Events (Document Management — OPTIONAL, required if + // the document management extension is implemented) + event DocumentUpdated( + bytes32 indexed _name, + string _uri, + bytes32 _documentHash + ); + event DocumentRemoved( + bytes32 indexed _name, + string _uri, + bytes32 _documentHash + ); + + // Recovery-specific Events (OPTIONAL, required if the recovery workflow + // extension is implemented) + event RecoveryInitiated( + uint256 indexed recoveryId, + address indexed lostWallet, + address indexed newWallet, + uint256 timelock + ); + event RecoveryCancelled(uint256 indexed recoveryId, address cancelledBy); + // RecoveryExecuted is replaced by ControllerTransfer event per ERC-1644 +} +``` + +### Interface Detection + +The `IERC1450` interface ID is calculated by XOR'ing the function selectors of all functions unique to the `IERC1450` interface (excluding inherited ERC-20 functions): + +```solidity +// IERC1450 unique functions (not in ERC-20): +bytes4 constant private CHANGEISSUER = bytes4(keccak256("changeIssuer(address)")); +bytes4 constant private SETTRANSFERAGENT = bytes4(keccak256("setTransferAgent(address)")); +bytes4 constant private ISTRANSFERAGENT = bytes4(keccak256("isTransferAgent(address)")); +bytes4 constant private ISSECURITYTOKEN = bytes4(keccak256("isSecurityToken()")); +bytes4 constant private MINT = bytes4(keccak256("mint(address,uint256,uint16,uint256)")); +bytes4 constant private BURNFROM = bytes4(keccak256("burnFrom(address,uint256)")); +bytes4 constant private BURNFROMREGULATION = bytes4(keccak256("burnFromRegulation(address,uint256,uint16)")); +bytes4 constant private GETHOLDERREGULATIONS = bytes4(keccak256("getHolderRegulations(address)")); +bytes4 constant private GETREGULATIONSUPPLY = bytes4(keccak256("getRegulationSupply(uint16)")); +// ... additional IERC1450-specific functions + +// The computed interface ID: +bytes4 constant public IERC1450_INTERFACE_ID = CHANGEISSUER ^ SETTRANSFERAGENT ^ ISTRANSFERAGENT ^ ISSECURITYTOKEN ^ MINT ^ BURNFROM ^ BURNFROMREGULATION ^ GETHOLDERREGULATIONS ^ GETREGULATIONSUPPLY /* ^ ... */; +// Actual value from reference implementation: +bytes4 constant public IERC1450_INTERFACE_ID = 0xaf175dee; +``` + +**Important Notes on Interface Detection**: +- Implementations MUST return `true` from `supportsInterface(0x01ffc9a7)` for [ERC-165](./eip-165.md) itself +- Implementations MUST return `true` from `supportsInterface(0xaf175dee)` for `IERC1450` +- Implementations SHOULD return `true` from `supportsInterface(type(IERC20Metadata).interfaceId)` for token metadata (`name()`, `symbol()`, `decimals()`) +- Implementations MUST NOT return `true` from `supportsInterface(0x36372b07)` for [ERC-20](./eip-20.md) + +**Why NOT Support ERC-20 Interface ID**: +While ERC-1450 is ABI-compatible with ERC-20 (same function signatures for view functions), returning `true` for the ERC-20 interface ID (`0x36372b07`) would be misleading: +- Wallets would assume they can call `transfer()` and `approve()` normally +- These functions always revert in ERC-1450, breaking user expectations +- Better to force explicit detection of `IERC1450` to ensure proper UI/UX +- Returning `true` for `IERC20Metadata` is acceptable since `name()`, `symbol()`, and `decimals()` work normally + +### RTA Proxy Pattern (REQUIRED Security Enhancement) + +To prevent security vulnerabilities where a compromised issuer could change the RTA and steal tokens, ERC-1450 implementations MUST use an RTA Proxy pattern. The reference implementation uses a **generic multi-signature wallet** that provides maximum flexibility while maintaining security: + +```solidity +/** + * @title RTAProxy + * @notice Multi-signature proxy contract for RTA operations + * @dev Deployed once and set as the permanent transferAgent in ERC-1450 tokens + * + * The RTAProxy pattern provides: + * - Protection against single key compromise via M-of-N multi-signature + * - Generic operation execution (can call any function on any target) + * - Signer management through multi-sig consensus + * - Complete audit trail of all RTA actions + * + * SECURITY REQUIREMENTS: + * - MUST use multiple signers (recommended: 2-of-3 or 3-of-5) + * - Signers SHOULD use hardware wallets or institutional custody + * - All operations MUST emit events for audit trail + */ +interface IRTAProxy { + // ============ Events ============ + + event SignerAdded(address indexed signer); + event SignerRemoved(address indexed signer); + event RequiredSignaturesUpdated(uint256 oldRequired, uint256 newRequired); + event OperationSubmitted(uint256 indexed operationId, address indexed submitter); + event OperationConfirmed(uint256 indexed operationId, address indexed signer); + event OperationExecuted(uint256 indexed operationId); + event OperationRevoked(uint256 indexed operationId, address indexed signer); + + // ============ Errors ============ + + error NotASigner(); + error AlreadyASigner(); + error AlreadyConfirmed(); + error NotConfirmed(); + error InsufficientConfirmations(); + error OperationAlreadyExecuted(); + error InvalidSignerCount(); + + // ============ Multi-Sig Operations ============ + + /** + * @notice Submit a new operation for multi-sig approval + * @param target The contract to call (e.g., ERC-1450 token address) + * @param data The encoded function call (e.g., abi.encodeWithSignature("mint(...)")) + * @param value ETH value to send (usually 0) + * @return operationId The ID of the submitted operation + * @dev Submitter automatically confirms the operation + * Operation auto-executes if submitter's confirmation meets threshold + */ + function submitOperation( + address target, + bytes memory data, + uint256 value + ) external returns (uint256 operationId); + + /** + * @notice Confirm a pending operation + * @param operationId The operation to confirm + * @dev Auto-executes when confirmation threshold is met + */ + function confirmOperation(uint256 operationId) external; + + /** + * @notice Revoke a previously given confirmation + * @param operationId The operation to revoke confirmation from + */ + function revokeConfirmation(uint256 operationId) external; + + /** + * @notice Manually execute an operation that has enough confirmations + * @param operationId The operation to execute + */ + function executeOperation(uint256 operationId) external; + + // ============ Signer Management (via multi-sig) ============ + + /** + * @notice Add a new signer (requires multi-sig approval) + * @param signer Address of the new signer + * @dev MUST be called through submitOperation (msg.sender == address(this)) + */ + function addSigner(address signer) external; + + /** + * @notice Remove a signer (requires multi-sig approval) + * @param signer Address of the signer to remove + * @dev MUST be called through submitOperation + * MUST NOT reduce signers below requiredSignatures + */ + function removeSigner(address signer) external; + + /** + * @notice Update required signature threshold (requires multi-sig approval) + * @param newRequiredSignatures New threshold + * @dev MUST be called through submitOperation + * MUST be > 0 and <= signers.length + */ + function updateRequiredSignatures(uint256 newRequiredSignatures) external; + + // ============ View Functions ============ + + /** + * @notice Get the list of current signers + * @return Array of signer addresses + */ + function getSigners() external view returns (address[] memory); + + /** + * @notice Check if an address has confirmed an operation + * @param operationId The operation ID + * @param signer The signer address + * @return bool True if the signer has confirmed + */ + function hasConfirmed(uint256 operationId, address signer) external view returns (bool); + + /** + * @notice Get operation details + * @param operationId The operation ID + * @return target The target contract + * @return data The encoded function call + * @return value The ETH value + * @return confirmations Number of confirmations + * @return executed Whether the operation has been executed + * @return timestamp When the operation was submitted + */ + function getOperation(uint256 operationId) external view returns ( + address target, + bytes memory data, + uint256 value, + uint256 confirmations, + bool executed, + uint256 timestamp + ); + + /** + * @notice Returns the contract implementation version + * @return string Semantic version string (e.g., "1.13.0") + */ + function version() external pure returns (string memory); +} +``` + +#### Security Benefits: +1. **Single Key Protection**: M-of-N multi-sig prevents any single compromised key from executing operations +2. **Issuer Protection**: Once RTAProxy is set as transferAgent, the issuer cannot unilaterally change it +3. **Flexible Operations**: Generic `submitOperation` can call any function on any target contract +4. **Signer Management**: Add/remove signers and adjust thresholds through multi-sig consensus +5. **Audit Trail**: All operations are logged on-chain via events +6. **Revocation**: Signers can revoke confirmations before execution if needed + +#### Implementation Flow (REQUIRED): +``` +1. Deploy RTAProxy with initial signers and threshold (e.g., 3 signers, 2-of-3) +2. Deploy ERC-1450 token with transferAgent = RTAProxy address +3. Token's setTransferAgent is locked after RTAProxy is set +4. RTA operations flow: Signer → submitOperation → confirmOperation → auto-execute +5. Signer changes require multi-sig approval through submitOperation +6. All operations emit events for regulatory audit trail +``` + +#### Example: Minting Tokens via Multi-Sig +```solidity +// Signer 1 submits mint operation +bytes memory mintData = abi.encodeWithSignature( + "mint(address,uint256,uint16,uint256)", + investor, + 1000, + 0x0006, // REG_US_CF + block.timestamp +); +uint256 opId = rtaProxy.submitOperation(tokenAddress, mintData, 0); + +// Signer 2 confirms (auto-executes if 2-of-3) +rtaProxy.confirmOperation(opId); +``` + +#### RTA Provider Change Process: +``` +When an issuer legitimately needs to change RTA providers: +1. Issuer contracts with new RTA provider +2. Current RTA validates the change request (legal docs, verification) +3. Current RTA transfers records to new RTA +4. Current RTA adds new RTA signers via multi-sig: submitOperation(addSigner(newSigner)) +5. Current RTA removes old signers via multi-sig: submitOperation(removeSigner(oldSigner)) +6. New RTA now controls all token operations +7. Process is logged on-chain for regulatory compliance +``` + +This cooperative process ensures: +- No unauthorized RTA changes (protects against key compromise) +- Legitimate business changes are possible (with proper verification) +- Similar to domain registrar transfers - requires current provider cooperation +- Creates audit trail for regulators + +### Broker Registration Pattern (RECOMMENDED) + +While the RTAProxy pattern is REQUIRED for RTA security, a similar BrokerProxy pattern is RECOMMENDED but not required for registered brokers. This provides operational security and key management flexibility for professional broker-dealers. + +#### Benefits of BrokerProxy: +- **Secure key rotation** without re-registration with the RTA +- **Multi-signature controls** for trader operations +- **Business continuity** during personnel changes +- **Institutional custody integration** (Fireblocks, Coinbase Custody, etc.) +- **Protection against individual key compromise** + +#### Implementation Options: + +**Option 1: Direct Registration (Simple)** +``` +RTA → registers → Broker EOA/Multisig +``` +- Suitable for: Individual brokers, small operations, testing +- Risk: Key compromise affects all broker operations +- Simplicity: No additional contract deployment needed + +**Option 2: Proxy Pattern (Recommended for Production)** +``` +RTA → registers → BrokerProxy → controls → Operator Multisig +``` +- Suitable for: Professional broker-dealers, high-volume operations +- Benefits: Key rotation, multi-trader support, institutional security +- Complexity: Requires additional proxy contract deployment + +#### Example BrokerProxy Interface: +```solidity +/** + * @title IBrokerProxy + * @notice Optional proxy pattern for registered brokers + * @dev While not required like RTAProxy, this pattern is RECOMMENDED for: + * - Professional broker-dealers with multiple traders + * - High-volume brokers needing key rotation + * - Brokers using institutional custody solutions + */ +interface IBrokerProxy { + // Events + event OperatorRotated(address indexed previousOperator, address indexed newOperator); + event RequestSubmitted(uint256 indexed requestId, address from, address to, uint256 amount); + + /** + * @notice Submit transfer request on behalf of client + * @dev Only callable by authorized operators of this broker + */ + function submitTransferRequest( + address token, + address from, + address to, + uint256 amount, + address feeToken, + uint256 feeAmount + ) external payable returns (uint256 requestId); + + /** + * @notice Rotate broker operator (multi-sig required) + * @param newOperator New operator address (should be multi-sig) + */ + function rotateOperator(address newOperator) external; + + /** + * @notice Check if address is authorized operator + */ + function isOperator(address account) external view returns (bool); +} +``` + +#### Why Not REQUIRE BrokerProxy? + +Unlike the RTA which has ultimate control over all tokens, brokers have limited authority: +1. **Lower Risk**: Brokers can only request transfers, not execute them +2. **Business Variety**: Some brokers are individuals, not institutions +3. **Market Flexibility**: Let brokers choose security appropriate to their needs +4. **Gradual Adoption**: Brokers can upgrade to proxy pattern as they grow + +The RTA treats both patterns equally - registration is by address whether direct or proxy. The RTA maintains the right to revoke any broker registration at any time, ensuring ultimate control regardless of the broker's implementation choice. + +### Fee System Implementation + +The fee system allows RTAs to charge for transfer processing using a single configured fee token (typically a stablecoin like USDC): + +#### Querying the Fee Token and Amount + +```solidity +// Get the configured fee token +address feeToken = token.getFeeToken(); +// Returns: 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48 (USDC on mainnet) + +// Get the fee amount for a transfer +uint256 feeAmount = token.getTransferFee(from, to, amount); +// Returns: 10000000 (10 USDC with 6 decimals) for flat fee +// OR calculated percentage for percentage-based fees +``` + +#### Frontend Implementation Pattern + +```javascript +// Get the fee token and amount +const feeToken = await token.getFeeToken(); +const feeAmount = await token.getTransferFee(from, to, amount); + +// Get token info for display +const tokenInfo = await getTokenInfo(feeToken); +const formattedFee = formatUnits(feeAmount, tokenInfo.decimals); + +// Display to user: "Transfer fee: 10.00 USDC" +console.log(`Transfer fee: ${formattedFee} ${tokenInfo.symbol}`); + +// Ensure user has approved fee token spending +await feeTokenContract.approve(token.address, feeAmount); + +// Submit transfer request with fee +const requestId = await token.requestTransferWithFee(from, to, amount, feeAmount); +``` + +#### RTA Fee Configuration + +RTAs configure the fee system through two functions: + +```solidity +// Set the fee token (typically USDC or another stablecoin) +token.setFeeToken(USDC_ADDRESS); + +// Set fee parameters +// Type 0: Flat fee (feeValue is the exact amount in fee token units) +token.setFeeParameters(0, 10000000); // 10 USDC flat fee + +// Type 1: Percentage fee (feeValue is basis points, 100 = 1%) +token.setFeeParameters(1, 50); // 0.5% fee +``` + +### Implementation Requirements for Regulated Securities +This standard requires implementers to maintain off-chain services and databases that record and track investor information including names, physical addresses, Ethereum addresses, and security ownership amounts. Before associating any Ethereum address with an investor's identity, implementers MUST verify ownership of that address through cryptographic proof (such as message signing), micro-deposits, or other secure verification methods to prevent fraudulent claims. + +The designated transfer agent must be able to produce current lists of all investors, including their identities and security ownership levels at any given moment. The system must support re-issuance of securities to investors for various operational and regulatory reasons. + +Private investor information must never be publicly exposed on a public blockchain. + +### KYC/AML Requirements + +Per SEC regulations and the Bank Secrecy Act, the RTA MUST ensure: + +1. **Identity Verification**: Every address holding tokens MUST be associated with a verified natural or legal person who has passed KYC/AML checks +2. **Address Ownership Proof**: Before any transfer, the RTA MUST verify that the recipient address is controlled by a verified person (via signature, micro-deposit, or other secure method) +3. **Ongoing Monitoring**: The RTA MUST maintain current KYC/AML status and may freeze accounts that fail periodic reviews +4. **No Anonymous Holdings**: Unlike permissionless tokens, ERC-1450 tokens CANNOT be held by anonymous or unverified addresses + +The RTA enforces these requirements through: +- `transferFrom`: RTA only executes after verifying both parties +- `requestTransferWithFee`: RTA rejects requests to unverified addresses with appropriate reason codes (10-14) +- `mint`: RTA only mints to verified addresses +- Recovery mechanisms: Require identity verification before execution + +Transfer rejections for KYC/AML failures use specific reason codes: +- `REASON_RECIPIENT_NOT_VERIFIED` (10): Recipient hasn't completed KYC/AML +- `REASON_ADDRESS_NOT_LINKED` (11): Address not linked to verified identity +- `REASON_SENDER_VERIFICATION_EXPIRED` (12): Sender's KYC expired +- `REASON_JURISDICTION_BLOCKED` (13): Recipient in restricted jurisdiction +- `REASON_ACCREDITATION_REQUIRED` (14): Recipient not accredited (for Reg D offerings) + +### Managing Investor Information +Special care and attention must be taken to ensure that the personally identifiable information of Investors is never exposed or revealed to the public. + +### Issuers who lost access to their address or private keys +With the RTA Proxy pattern implemented in ERC-1450, the impact of an Issuer losing access to their private key is significantly mitigated. Once the RTA is established (especially via RTAProxy), the RTA maintains exclusive control over critical operations including `changeIssuer`, `mint`, `burn`, and `transferFrom`. Even if the Issuer loses their private key or becomes compromised, the RTA can: +- Continue all token operations without issuer involvement +- Transfer ownership to a new issuer address through `changeIssuer` +- Protect token holders from any issuer-side security failures +- Maintain full regulatory compliance and operations + +This design ensures that securities tokens remain operational and secure regardless of issuer key management issues. The RTA acts as the security backstop, preventing any single point of failure from disrupting the securities' operations or endangering investor assets. + +If the Issuer loses access, the Issuer's securities must be rebuilt using off-chain services. The Issuer must create (and secure) a new address. The RTA can read the existing Issuer securities, and the RTA can `mint` Investor securities accordingly under a new ERC-1450 smart contract. + +### Registered Transfer Agents who lost access to their address or private keys +Professional RTAs MUST implement enterprise-grade key management solutions to prevent key loss scenarios. This includes: +- Multi-signature wallets requiring multiple parties to approve transactions +- Hardware Security Modules (HSMs) for key storage +- Multi-Party Computation (MPC) solutions like Fireblocks or similar institutional custody +- Geographically distributed key shards +- Regular key rotation and backup procedures + +With proper security infrastructure, RTA key loss should be virtually impossible. However, if catastrophic failure occurs: +1. With RTAProxy pattern: The proxy contract can facilitate controlled RTA rotation with proper authorization +2. Without RTAProxy: The Issuer can execute `setTransferAgent` to assign a new RTA address (if the issuer still has access) +3. Last resort: Securities must be reissued on a new contract with proper verification of all holdings + +The use of professional custody solutions by RTAs is not optional but essential for maintaining the security and reliability required for managing securities. + +### Handling Investors (security owners) who lost access to their addresses or private keys + +**Common Business Model - RTA Omnibus Custody:** +Many RTAs maintain securities in omnibus accounts with off-chain record keeping of individual investor holdings. This business model (not a protocol requirement) offers several advantages: +- Most investors never need to manage private keys +- Internal transfers occur off-chain in databases +- Securities only transfer to investor wallets upon explicit, verified request +- Investor key loss doesn't affect their holdings in the omnibus account + +Note: This is a business implementation choice, not enforced by the protocol itself. + +**Self-Custody Scenario:** +For investors who choose to hold securities in their own wallets, loss of credentials may occur due to: lost private keys, hacking, fraud, or life events (death, incapacitation). In these cases: + +If an Investor (or their legal representative) loses wallet access, they must go through a verified process with the RTA including: +1. Identity verification matching original KYC records +2. Notarized affidavit of lost access +3. Waiting period for potential disputes (as specified in recovery mechanism) +4. Supply and verify ownership of new wallet address + +Upon successful verification, the RTA can use the recovery mechanism to transfer securities from the lost wallet to the new verified wallet, maintaining full audit trail for regulatory compliance. + +Note: The omnibus custody model described above is a common business practice that provides professional security while maintaining investor flexibility to withdraw to self-custody when desired. This is an implementation choice, not a protocol requirement. + +### Regulation Tracking Implementation (Non-Normative) + +This section provides guidance for implementing regulation tracking in ERC-1450 tokens. Since securities must be issued under specific regulatory frameworks, implementations need to track which regulation applies to each token holder's shares for proper compliance enforcement. + +#### Regulation Type Encoding + +Implementations SHOULD use a uint16 value to encode regulation types, allowing for global regulatory frameworks. A suggested encoding pattern uses the high byte for country/region and the low byte for specific regulations: + +```solidity +// Example encoding (not part of the standard, implementations may vary): +// Format: 0xCCRR where CC = country code, RR = regulation code + +// US Regulations (0x00XX) +uint16 constant REG_US_S1 = 0x0001; // S-1 Registration (IPO) +uint16 constant REG_US_D_504 = 0x0002; // Regulation D Rule 504 ($10M) +uint16 constant REG_US_A_TIER_1 = 0x0004; // Regulation A Tier I ($20M) +uint16 constant REG_US_A_TIER_2 = 0x0005; // Regulation A Tier II ($75M) +uint16 constant REG_US_CF = 0x0006; // Regulation Crowdfunding ($5M) +uint16 constant REG_US_D_506B = 0x0007; // Regulation D 506(b) (no general solicitation) +uint16 constant REG_US_D_506C = 0x0008; // Regulation D 506(c) (accredited only) +uint16 constant REG_US_S = 0x0009; // Regulation S (offshore offerings) + +// EU Regulations (0x01XX) +uint16 constant REG_EU_PROSPECTUS = 0x0101; // EU Prospectus Regulation + +// Canadian Regulations (0x02XX) +uint16 constant REG_CA_OM = 0x0201; // Offering Memorandum +``` + +#### Storage Pattern + +Implementations MUST store regulation data on-chain to enable the RTA to enforce compliance. A recommended storage pattern: + +```solidity +struct TokenBatch { + uint256 amount; + uint16 regulationType; + uint256 issuanceDate; // Original share issuance, not tokenization +} + +mapping(address => TokenBatch[]) private holderBatches; +``` + +#### Compliance Checking + +When processing transfer requests, the RTA queries the regulation data to apply appropriate restrictions: + +1. **Time-based restrictions**: Calculate `currentTime - issuanceDate` to determine if holding periods have expired +2. **Investor restrictions**: Check if recipient meets requirements (e.g., accreditation for Reg D) +3. **Geographic restrictions**: Verify jurisdiction compliance for regulations like Reg S + +#### Example: Reg CF Maturation + +Regulation Crowdfunding shares have a 12-month resale restriction that expires over time: + +```solidity +// At mint time (tokenizing shares from March 2023 offering) +mint(investor, 1000, 0x0006, 1677628800); // REG_US_CF, March 1, 2023 + +// Transfer request in November 2024 +// RTA calculates: Nov 2024 - March 2023 = 20 months (> 12 months) +// Result: Shares have matured, transfer allowed to any eligible investor + +// Transfer request if it had been May 2023 +// RTA calculates: May 2023 - March 2023 = 2 months (< 12 months) +// Result: Transfer blocked with REASON_LOCK_PERIOD +``` + +#### RTA Transfer Strategy Flexibility + +The RTA has complete control over which token batches to transfer or burn. The blockchain stores raw batch data, while the RTA implements the optimal strategy for each situation: + +**Transfer Strategy Options:** + +```solidity +// Holder has: +// - 100 tokens: Reg CF, issued March 2023 +// - 200 tokens: Reg D, issued June 2023 +// - 50 tokens: Reg A, issued September 2023 + +// Transfer request for 150 tokens - RTA chooses strategy: +// Option A (FIFO): transferFromRegulated(..., 100, REG_CF, March2023) +// transferFromRegulated(..., 50, REG_D, June2023) +// Option B (LIFO): transferFromRegulated(..., 50, REG_A, Sept2023) +// transferFromRegulated(..., 100, REG_D, June2023) +// Option C (Tax Optimized): Transfer specific lots for best tax outcome +// Option D (Regulatory): Transfer only matured/unrestricted batches +``` + +**Burn Strategy Options:** + +```solidity +// Using burnFrom(address, amount) - RTA determines strategy +// Using burnFromRegulated(address, amount, regulation, date) - Precise control +// Using burnFromRegulation(address, amount, regulation) - Regulation-specific + +// RTA can optimize based on: +// - Tax implications (short vs long-term gains) +// - Regulatory maturity (expired lockups first) +// - Holder preferences (specific lot selection) +// - Corporate actions (specific share class redemption) +``` + +This flexibility ensures: +1. Tax optimization for holders +2. Regulatory compliance per jurisdiction +3. Support for various accounting methods +4. Adaptability to changing requirements + +### Corporate Actions (Non-Normative) + +This section describes recommended patterns for implementing common corporate actions in ERC-1450 tokens. These patterns demonstrate operational completeness while maintaining the core security and compliance model. + +#### Stock Splits and Reverse Splits + +For stock splits (e.g., 2-for-1) or reverse splits (e.g., 1-for-10), the RTA executes proportional adjustments: + +```solidity +// 2-for-1 split implementation pattern +function executeSplit(uint256 splitRatio) external onlyRTA { + address[] memory holders = getAllHolders(); // Off-chain tracked + for (uint i = 0; i < holders.length; i++) { + uint256 currentBalance = balanceOf(holders[i]); + uint256 additionalShares = currentBalance * (splitRatio - 1); + + // Mint additional shares + _mint(holders[i], additionalShares); + // Emit canonical events for indexers + emit Transfer(address(0), holders[i], additionalShares); + } +} +``` + +**Key Implementation Points:** +- RTA executes atomically across all holders +- Uses canonical `Transfer(0x0, holder, amount)` events for mints +- For reverse splits, use `_burn` with `Transfer(holder, 0x0, amount)` events +- Maintain audit trail with split ratio and execution timestamp + +#### Dividends and Distributions + +Dividend payments typically use stablecoin distributions based on a record date snapshot: + +**Option 1: Off-Chain Distribution List** +```solidity +// RTA takes snapshot at record date +mapping(address => uint256) recordDateBalances; + +// Off-chain: Calculate pro-rata distributions +// Execute stablecoin transfers via separate contract or traditional rails +``` + +**Option 2: On-Chain Claim Contract** +```solidity +contract DividendClaim { + IERC20 public paymentToken; // e.g., USDC + mapping(address => uint256) public claimableAmounts; + uint256 public recordDate; + + function claim() external { + require(rtaContract.isKYCVerified(msg.sender), "KYC required"); + uint256 amount = claimableAmounts[msg.sender]; + claimableAmounts[msg.sender] = 0; + paymentToken.transfer(msg.sender, amount); + } +} +``` + +**Implementation Notes:** +- Record date snapshot determines eligible holders +- Payment in stablecoins (USDC, USDT) or native tokens (ETH, MATIC) +- Tax withholding handled off-chain per jurisdiction +- RTA maintains distribution records for tax reporting + +#### Mandatory Redemptions and Calls + +For mandatory redemptions (e.g., bond calls, forced buybacks), use Controller Token Operation Standard (1644) semantics: + +```solidity +function executeMandatoryRedemption( + address holder, + uint256 amount, + uint256 pricePerShare, + string calldata reason +) external onlyRTA { + // Force transfer to redemption pool + _transferFrom(holder, redemptionPool, amount); + + // Record redemption terms + emit Redemption(holder, amount, pricePerShare, reason); + + // Payment handled via separate mechanism + // (stablecoin transfer, wire, check) +} +``` + +**Compliance Requirements:** +- Follow Document Management Standard (1643) for notices and terms +- Provide advance notice per security agreements (typically 30-60 days) +- Maintain redemption price and payment terms on-chain or via Document Management Standard +- Support partial redemptions for pro-rata calls + +#### Tender Offers + +Voluntary tender offers allow investors to optionally sell shares: + +```solidity +contract TenderOffer { + uint256 public offerPrice; + uint256 public offerExpiry; + address public offeror; + + function acceptOffer(uint256 amount) external { + require(block.timestamp < offerExpiry, "Offer expired"); + require(rtaContract.isKYCVerified(msg.sender), "KYC required"); + + // Transfer shares to offeror + token.requestTransferWithFee(msg.sender, offeror, amount, fee); + + // Payment handled separately + emit OfferAccepted(msg.sender, amount, offerPrice); + } +} +``` + +#### Mergers and Acquisitions + +For mergers requiring token swaps: + +```solidity +contract MergerExchange { + IERC1450 public oldToken; + IERC1450 public newToken; + uint256 public exchangeRatio; // e.g., 100 = 1:1, 150 = 1.5:1 + + function exchangeTokens(uint256 amount) external { + require(rtaContract.isKYCVerified(msg.sender), "KYC required"); + + // Burn old tokens + oldToken.requestTransferWithFee(msg.sender, address(0), amount, 0); + + // Mint new tokens at exchange ratio + uint256 newAmount = (amount * exchangeRatio) / 100; + newToken.mint(msg.sender, newAmount); + } +} +``` + +#### Implementation Considerations + +1. **Event Standardization**: Use canonical Transfer events for all balance changes to ensure indexer compatibility +2. **Record Dates**: Snapshot mechanisms must account for pending transfers and corporate action timelines +3. **Payment Rails**: Dividend and redemption payments typically use stablecoins or traditional banking +4. **Regulatory Notices**: Use Document Management Standard (1643) for required disclosures +5. **Tax Compliance**: Off-chain systems handle withholding and reporting requirements +6. **Audit Trail**: All corporate actions must maintain complete records for regulatory review + +These patterns demonstrate that ERC-1450 can handle the complete lifecycle of security token operations while maintaining regulatory compliance and investor protections. The RTA's exclusive control ensures all corporate actions are executed properly with appropriate verification and documentation. + +### Record Dates and Voting Mechanics (Non-Normative) + +Securities require record dates for corporate actions, proxy voting, and shareholder meetings. This section provides guidance on implementing these features without complicating the core token standard. + +#### Record Date Snapshots + +Record dates determine which shareholders are eligible for dividends, voting, or other corporate actions. Rather than building snapshots into the token itself, we recommend: + +**Option 1: Off-Chain Snapshots (Recommended)** +```solidity +// RTA maintains historical balances in database +// Query: SELECT balance FROM holdings WHERE address = ? AND timestamp <= ? +// This provides complete flexibility without on-chain gas costs +``` + +**Option 2: External Snapshot Contract** +```solidity +// Use existing snapshot solutions like OpenZeppelin's ERC20Snapshot +// or deploy a separate SnapshotManager contract +interface ISnapshotManager { + function takeSnapshot() external returns (uint256 snapshotId); + function balanceOfAt(address account, uint256 snapshotId) external view returns (uint256); +} +``` + +**Option 3: Simple Block-Based Recording** +```solidity +// For simple needs, just record block numbers +mapping(uint256 => uint256) public recordDates; // actionId => blockNumber + +// Off-chain services can reconstruct balances at any block +// using historical blockchain data +``` + +#### Voting and Proxy Management + +Shareholder voting for corporate governance (board elections, mergers, etc.) is typically handled off-chain with on-chain attestation: + +**Proxy Rules Documentation** +```solidity +// Store proxy voting rules via ERC-1643 document management +rtaContract.setDocument( + "PROXY_RULES_2024", + "ipfs://QmProxyVotingRulesHash", + block.timestamp +); +``` + +**Voting Process Pattern** +```solidity +contract VotingRegistry { + struct ProxyVote { + uint256 recordDate; + uint256 votingDeadline; + string proposalUri; // IPFS link to proposal details + mapping(address => bool) hasVoted; + mapping(uint256 => uint256) votes; // optionId => voteCount + } + + // RTA records votes submitted through traditional proxy channels + function recordVote( + uint256 voteId, + address shareholder, + uint256 optionId, + uint256 shares + ) external onlyRTA { + require(rtaContract.balanceOfAt(shareholder, recordDate) >= shares); + require(!hasVoted[shareholder], "Already voted"); + + votes[optionId] += shares; + hasVoted[shareholder] = true; + + emit VoteRecorded(voteId, shareholder, optionId, shares); + } +} +``` + +#### Quorum and Meeting Mechanics + +For shareholder meetings requiring quorum: + +```solidity +contract MeetingQuorum { + uint256 public quorumPercentage = 5000; // 50% in basis points + + function calculateQuorum(uint256 recordDate) external view returns (uint256) { + uint256 totalSupply = token.totalSupply(); + return (totalSupply * quorumPercentage) / 10000; + } + + function isQuorumMet(uint256 voteId) external view returns (bool) { + uint256 totalVoted = getTotalVotes(voteId); + uint256 required = calculateQuorum(votes[voteId].recordDate); + return totalVoted >= required; + } +} +``` + +#### Implementation Recommendations + +1. **Keep the Token Simple**: Don't add snapshot logic to the core ERC-1450 token. Use external contracts or off-chain systems. + +2. **Document Everything**: Use Document Management Standard (1643) to store: + - `PROXY_RULES` - Voting procedures and requirements + - `MEETING_NOTICE` - Shareholder meeting announcements + - `RECORD_DATE` - Official record date declarations + - `VOTING_RESULTS` - Final vote tallies and outcomes + +3. **Hybrid Approach**: Most voting happens off-chain through traditional proxy systems, with results attested on-chain by the RTA. + +4. **Regulatory Compliance**: Follow SEC rules for proxy solicitation, including: + - Proper notice periods (typically 10-60 days) + - Required disclosures in proxy statements + - Vote tabulation by independent inspectors of election + +5. **Audit Trail**: Maintain complete records of: + - Record date declarations + - Shareholder eligibility + - Votes submitted + - Final results and actions taken + +#### Example Integration + +```solidity +// Complete voting lifecycle example +contract ShareholderGovernance { + IERC1450 public token; + ISnapshotManager public snapshots; + + function initiateVote( + string memory proposalUri, + uint256 votingPeriodDays + ) external onlyRTA returns (uint256 voteId) { + // 1. Take snapshot for record date + uint256 snapshotId = snapshots.takeSnapshot(); + + // 2. Store proposal details via ERC-1643 + token.setDocument( + string(abi.encodePacked("PROPOSAL_", voteId)), + proposalUri, + block.timestamp + ); + + // 3. Set voting deadline + uint256 deadline = block.timestamp + (votingPeriodDays * 1 days); + + // 4. Emit event for indexers and shareholders + emit VoteInitiated(voteId, snapshotId, deadline, proposalUri); + + return voteId; + } +} +``` + +This approach answers the "how do I do meetings?" question while keeping the core ERC-1450 token simple and focused on transfer control. RTAs can implement voting and governance in whatever way best suits their jurisdiction and security type, using the token as the source of truth for ownership while handling the mechanics externally. + +### Tax and Withholding Obligations (Non-Normative) + +Tax compliance for security tokens is handled entirely off-chain by the RTA. This section documents common patterns for managing tax obligations without adding on-chain complexity. + +#### Tax Documentation Collection + +RTAs must collect appropriate tax documentation before enabling trading: + +**U.S. Persons:** +- **W-9 Forms**: Collected during KYC for U.S. tax residents +- **Taxpayer Identification Number (TIN)**: Stored securely off-chain +- **Backup Withholding**: Applied when W-9 not provided (24% as of 2024) + +**Non-U.S. Persons:** +- **W-8 Forms**: Various types (W-8BEN, W-8BEN-E, W-8IMY, etc.) +- **Foreign TIN**: When available under tax treaties +- **FATCA/CRS Compliance**: Automatic exchange of information + +Documentation references stored via Document Management Standard (1643): +```solidity +// Store encrypted reference to tax documentation +rtaContract.setDocument( + "TAX_DOCS_2024", + "ipfs://QmTaxDocumentationHash", // Encrypted off-chain storage + block.timestamp +); +``` + +#### Withholding at Source + +For dividends and distributions, withholding occurs off-chain: + +```solidity +// Example: Dividend payment with withholding (off-chain calculation) +struct DividendPayment { + address recipient; + uint256 grossAmount; + uint256 withholdingRate; // e.g., 3000 = 30% + uint256 netAmount; // grossAmount - withholding +} + +// RTA calculates withholding based on: +// - Investor tax status (US vs. non-US) +// - Security type (equity vs. debt) +// - Tax treaty benefits +// - Dividend type (ordinary vs. qualified) +``` + +#### Tax Reporting + +Annual tax reporting handled entirely off-chain: + +**1099 Series (U.S. Recipients):** +- **1099-DIV**: Dividend distributions +- **1099-B**: Proceeds from broker transactions +- **1099-INT**: Interest payments +- **1099-MISC**: Other income + +**1042-S (Non-U.S. Recipients):** +- Foreign person's U.S. source income +- Withholding tax applied +- Treaty benefits claimed + +Reporting references maintained via document management: +```solidity +// Annual tax reporting references +rtaContract.setDocument( + "1099_FORMS_2024", + "encrypted://tax-reports/2024/1099", + block.timestamp +); + +rtaContract.setDocument( + "1042S_FORMS_2024", + "encrypted://tax-reports/2024/1042s", + block.timestamp +); +``` + +#### Implementation Pattern + +```solidity +// Off-chain tax compliance system interfaces with on-chain token +interface ITaxCompliance { + // All functions are off-chain, shown here for documentation + + function collectW9(address investor) external; + function collectW8(address investor, W8Type formType) external; + function calculateWithholding(address investor, uint256 amount) external view returns (uint256); + function generate1099(address investor, uint256 year) external; + function generate1042S(address investor, uint256 year) external; + + // On-chain reference only + function updateTaxDocumentHash(string memory docType, string memory uri) external; +} +``` + +#### Key Principles + +1. **No On-Chain Tax Logic**: All tax calculations and withholding happen off-chain +2. **Document References Only**: Use Document Management Standard (1643) to store encrypted references to tax documents +3. **Privacy Protection**: Never store TINs, SSNs, or tax rates on-chain +4. **Jurisdictional Flexibility**: RTAs handle varying tax requirements per jurisdiction +5. **Audit Trail**: Maintain complete off-chain records for tax authority audits + +#### Operational Workflow + +1. **Onboarding**: Collect W-9/W-8 during KYC process +2. **Distributions**: Calculate withholding off-chain before payment +3. **Trading**: Track cost basis off-chain for 1099-B reporting +4. **Year-End**: Generate tax forms and provide to investors +5. **Compliance**: File with IRS/tax authorities as required + +This approach ensures full tax compliance while keeping the token standard simple and avoiding the complexity of on-chain tax calculations. Tax obligations remain where they belong - in the operational layer managed by the regulated RTA. + +### ATS Adapter Pattern (Non-Normative) + +While ERC-1450 explicitly excludes DEX trading due to compliance requirements, regulated Alternative Trading Systems (ATSs) and other SEC-registered venues can integrate with ERC-1450 tokens to provide compliant secondary market liquidity. + +#### Monitoring Transfer Events for Liquidity Discovery + +Regulated trading venues can monitor existing ERC-1450 events to facilitate compliant trading: + +```solidity +// Existing events that ATSs can monitor +event TransferRequested( + address indexed from, + address indexed to, + uint256 value, + uint256 fee +); + +event TransferApproved( + address indexed from, + address indexed to, + uint256 value +); + +event TransferRejected( + address indexed from, + address indexed to, + uint256 value, + uint16 reason +); +``` + +#### ATS Integration Patterns + +**Pattern 1: Order Book Visibility** +```solidity +// ATS monitors TransferRequested events to understand market interest +// Can display pending transfer requests as "indications of interest" +// Note: Actual execution still requires RTA approval +``` + +**Pattern 2: Pre-Matched Trade Submission** +```solidity +// ATS matches buyers and sellers off-chain +// Submits transfer via registered broker +function submitMatchedTrade( + address buyer, + address seller, + uint256 shares +) external { + require(registeredBrokers[msg.sender], "Must be registered broker"); + + // Route through standard transfer request + token.requestTransferWithFee(seller, buyer, shares, brokerFee); +} +``` + +**Pattern 3: Failed Transfer Analysis** +```solidity +// ATS monitors TransferRejected events with reason codes +// Uses this data to: +// - Pre-filter non-compliant trades +// - Understand liquidity constraints +// - Improve matching algorithms + +if (reasonCode == REASON_RECIPIENT_NOT_VERIFIED) { + // Don't match with this buyer until KYC complete +} else if (reasonCode == REASON_INSUFFICIENT_BALANCE) { + // Seller doesn't have shares available +} +``` + +#### Compliant Venue Integration + +Regulated venues can facilitate liquidity while maintaining full compliance: + +1. **Pre-Trade Compliance** + - Verify all parties are KYC/AML approved + - Check transfer restrictions before matching + - Ensure accreditation requirements are met + +2. **Trade Execution** + - Submit transfers through registered broker accounts + - Include appropriate fees in transfer requests + - Maintain audit trail for regulatory review + +3. **Post-Trade Settlement** + - Monitor TransferApproved events for confirmation + - Handle failed transfers based on reason codes + - Report trades to regulatory systems (CAT, TRACE, etc.) + +#### Example ATS Adapter Implementation + +```solidity +contract ATSAdapter { + IERC1450 public token; + address public rtaAddress; + + // Track pending orders + struct Order { + address trader; + bool isBuy; + uint256 shares; + uint256 price; + bool isActive; + } + + mapping(uint256 => Order) public orders; + + // Submit matched trades to RTA + function executeTrade( + uint256 buyOrderId, + uint256 sellOrderId, + uint256 shares + ) external onlyATS { + Order memory buyOrder = orders[buyOrderId]; + Order memory sellOrder = orders[sellOrderId]; + + require(buyOrder.isBuy && !sellOrder.isBuy, "Invalid order types"); + require(buyOrder.isActive && sellOrder.isActive, "Orders not active"); + + // Pre-verify compliance + require(token.isKYCVerified(buyOrder.trader), "Buyer not KYC'd"); + require(token.isKYCVerified(sellOrder.trader), "Seller not KYC'd"); + + // Submit transfer through registered broker + token.requestTransferWithFee( + sellOrder.trader, + buyOrder.trader, + shares, + calculateFee(shares) + ); + + // Mark orders as pending execution + orders[buyOrderId].isActive = false; + orders[sellOrderId].isActive = false; + } + + // Monitor rejection reasons to update order book + function handleRejection( + address from, + address to, + uint16 reasonCode + ) external { + // Update order book based on rejection reason + if (reasonCode == 10) { // REASON_RECIPIENT_NOT_VERIFIED + // Remove buy orders from this recipient + cancelOrdersForTrader(to); + } + } +} +``` + +#### Benefits of ATS Integration + +1. **Compliant Liquidity**: Provides secondary market without compromising KYC/AML +2. **Price Discovery**: Enables transparent pricing through regulated venues +3. **Regulatory Reporting**: Maintains full audit trail for SEC/FINRA requirements +4. **Investor Protection**: All trades go through regulated entities with oversight +5. **Efficiency**: Reduces settlement risk through pre-verification + +#### Important Considerations + +- **Not DEX Trading**: All transfers still require RTA approval and KYC verification +- **Regulatory Compliance**: ATSs must be SEC-registered and follow all applicable rules +- **No Direct P2P**: Investors cannot trade directly; must go through regulated intermediaries +- **Reason Code Utilization**: ATSs should use rejection reason codes to optimize matching +- **Fee Transparency**: All broker and RTA fees must be clearly disclosed + +This pattern demonstrates how ERC-1450 can support liquid secondary markets through regulated venues while maintaining the strict compliance requirements necessary for security tokens. The existing event structure provides sufficient information for ATSs to facilitate compliant trading without requiring any changes to the core standard. + +## Rationale + +### Why On-Chain If the RTA Gates Everything? + +A critical question: If holders cannot initiate transfers and the RTA controls all operations, why use blockchain instead of a traditional centralized database? The answer lies in the unique benefits blockchain provides even within a regulated, controlled environment: + +#### 1. **Immutable Global Audit Trail** + +Unlike traditional databases where entries can be modified or deleted, blockchain provides: +- **Permanent Record**: Every mint, burn, and transfer is permanently recorded +- **Regulatory Transparency**: SEC, FINRA, and state regulators can independently verify all transactions +- **Court-Admissible Evidence**: Immutable records serve as indisputable evidence in legal proceedings +- **Real-Time Auditing**: Eliminates the need for quarterly reconciliations and manual audits + +#### 2. **Deterministic Settlement and Reconciliation** + +Traditional securities settlement involves multiple intermediaries and T+2 settlement cycles. ERC-1450 enables: +- **Instant Settlement**: Transfers are atomic and final when executed +- **No Failed Trades**: Eliminates settlement risk and the need for NSCC guarantees +- **Automated Reconciliation**: Cap table is always accurate, no manual reconciliation needed +- **Reduced Counterparty Risk**: No need for clearing houses or settlement intermediaries + +#### 3. **Cost Efficiency Through L2 Deployment** + +Deployment on Layer 2 solutions provides dramatic cost savings: +- **Traditional System**: $5-50 per transfer through existing infrastructure +- **L2 Deployment**: $0.01-0.10 per transfer on Base, Arbitrum, or Polygon +- **Batch Operations**: Process hundreds of transfers in a single transaction +- **No Infrastructure Costs**: No need for expensive mainframes and data centers + +#### 4. **Programmatic Composability** + +While direct transfers are disabled, valuable integrations remain: +- **Portfolio Management**: Wallets and portfolio trackers can display holdings +- **Tax Reporting**: Automated tax lot tracking and 1099 generation +- **Regulatory Reporting**: Automated CAT and Blue Sheet reporting +- **Corporate Actions**: Programmable dividends, splits, and voting +- **Compliant Secondary Markets**: Integration with regulated ATSs and exchanges + +#### 5. **Viable On-Chain Integrations** + +Despite transfer restrictions, these blockchain capabilities remain valuable: + +**Read Operations (Always Available)**: +- `balanceOf()`: Check holdings +- `totalSupply()`: View outstanding shares +- `decimals()`, `name()`, `symbol()`: Token metadata (OPTIONAL per [EIP-20](./eip-20.md), SHOULD be provided) +- Event logs: Complete transaction history + +**RTA-Initiated Operations**: +- Automated dividend distributions +- Programmatic share buybacks +- Instant corporate actions (splits, mergers) +- Cross-border settlements without correspondent banking + +**Compliance Integrations**: +- KYC/AML oracle integration +- Accreditation verification services +- Regulatory reporting automation +- Smart contract escrows for M&A + +#### 6. **Future Interoperability** + +Building on blockchain today positions for future innovations: +- **Central Bank Digital Currencies (CBDCs)**: Native integration for settlements +- **Cross-Border Securities**: Eliminate need for ADRs and dual listings +- **24/7 Markets**: Enable round-the-clock trading when regulations permit +- **DeFi Integration**: Future compliant lending and borrowing against securities + +#### 7. **Investor Benefits** + +Even without direct transfers, investors gain: +- **Transparency**: View holdings and transactions in real-time +- **Proof of Ownership**: Cryptographic proof without relying on RTA databases +- **Inheritance**: Simplified estate transfer through smart contracts +- **Global Access**: Hold US securities from anywhere without local custodians + +#### The Centralized Database Comparison + +A traditional centralized database cannot provide: +- **Cryptographic Proof**: No mathematical guarantee of ownership +- **Global Accessibility**: Requires API access and trust +- **Auditability**: Can be modified without trace +- **Interoperability**: Closed system with no composability +- **Cost Efficiency**: Requires expensive infrastructure +- **Innovation Platform**: No programmable extensions + +**Conclusion**: ERC-1450 uses blockchain as a **regulated public infrastructure** rather than a **permissionless payment rail**. The RTA control model satisfies SEC requirements while capturing blockchain's benefits: immutability, transparency, cost efficiency, and programmability. This is not about decentralization—it's about building better market infrastructure. + +### SEC Regulatory Framework for Transfer Agent Operations + +In the United States securities market, the exclusive control model where only the RTA can execute transfers, mints, and burns is based on established regulatory practice: + +**Transfer Agent Exclusive Authority**: Under SEC Rule 17Ad-1 through 17Ad-22, transfer agents are designated as the sole entities responsible for: +- Recording changes in ownership (equivalent to `transferFrom`) +- Issuing securities (equivalent to `mint`) +- Cancelling securities (equivalent to `burnFrom`) +- Maintaining the official register of security holders + +**Regulatory Citations**: +- **17 CFR § 240.17Ad-1**: Defines transfer agent responsibilities for prompt and accurate clearance and settlement +- **17 CFR § 240.17Ad-10**: Requires transfer agents to establish adequate internal accounting controls +- **17 CFR § 240.17Ad-11**: Mandates accurate recordkeeping and reporting systems +- **Section 17A of the Securities Exchange Act of 1934**: Establishes the regulatory framework for transfer agents + +This standard implements these regulatory requirements by assigning exclusive control of transfer operations to the RTA. While this reflects US market practice rather than a universal requirement, similar designated transfer controller models exist in many jurisdictions worldwide. Implementers in other jurisdictions should consult local regulations for specific requirements. + +### Prior Art and Related Standards + +ERC-1450 builds upon lessons learned from previous security token standards: + +[ERC-884 (Delaware General Corporations Law (DGCL) compatible share token)](./eip-884.md) addresses corporate share requirements under Delaware law, focusing on maintaining compliant shareholder registries. While ERC-884 provides important groundwork for regulated securities on blockchain, it explicitly states that broader securities regulation requirements are out of scope. + +Simple Restricted Token Standards provide basic transfer restrictions but do not address critical operational requirements such as recovery mechanisms, court-ordered transfers, or the designated transfer controller model. + +### The Controller of Record Model + +In the United States, SEC regulations under Rule 17Ad mandate that Registered Transfer Agents maintain exclusive authority over share registry and transfer operations - a "controller of record" model that ensures regulatory compliance and investor protection. While other jurisdictions have similar designated controller requirements with different terminology, the SEC's RTA framework is particularly stringent and well-established, making it an ideal foundation for this standard that can be adapted to other regulatory environments. + +This standard implements this controller model on-chain, providing: +- Single point of regulatory accountability +- Clear audit trails for compliance +- Recovery mechanisms for lost assets +- Court-ordered transfer capabilities + +[ERC-3643 (T. rex)](./eip-3643.md) takes a different approach with on-chain identity management and modular compliance rules. While comprehensive, it adds significant complexity through multiple contracts and on-chain identity storage, which raises privacy concerns and gas costs. ERC-3643 is primarily adopted in European markets where regulatory frameworks differ from US SEC requirements. + +### Comparison Matrix: Security Token Standards + +| Feature | [ERC-1450](./eip-1450.md) | [ERC-3643](./eip-3643.md) (T. rex) | Security Token Suite | Standard [ERC-20](./eip-20.md) | +|---------|----------|------------------|----------------|-----------------| +| **Control Model** | RTA-exclusive monopsony | Multiple compliance agents | Flexible controllers | Permissionless | +| **Identity Management** | Off-chain (privacy-preserving) | On-chain identity registry | Mixed (implementation-dependent) | None | +| **US SEC Compliance** | ✅ Native RTA model | ❌ Requires adaptation | ❌ Requires customization | ❌ Non-compliant | +| **Privacy** | ✅ No PII on-chain | ❌ Identity data on-chain | ⚠️ Implementation varies | ✅ No identity required | +| **Gas Efficiency** | ✅ Single contract | ❌ Multiple contracts | ❌ Modular architecture | ✅ Minimal | +| **Operational Complexity** | ✅ Simple RTA operations | ❌ Complex rule engine | ❌ Partition management | ✅ Simple transfers | +| **Recovery Mechanism** | ✅ Via controller transfer; optional time-locked workflow | ⚠️ Implementation-dependent | ⚠️ Via controller operations | ❌ None | +| **Court Orders** | ✅ Native support | ⚠️ Via forced transfers | ✅ Controller operations | ❌ None | +| **Transfer Restrictions** | ✅ RTA-gated only | ✅ Rule-based | ✅ Partition-based | ❌ None | +| **Direct Transfers** | ❌ Disabled (by design) | ⚠️ If rules allow | ⚠️ If authorized | ✅ Always allowed | +| **Broker Integration** | ✅ Native broker model | ❌ Not specified | ⚠️ Implementation varies | ❌ None | +| **Fee Collection** | ✅ Built-in mechanism | ❌ Not specified | ❌ Not specified | ❌ None | +| **Existing RTA Infrastructure** | ✅ Direct integration | ❌ Requires middleware | ❌ Requires adaptation | ❌ Incompatible | +| **Regulatory Reporting** | ✅ Clear audit trail | ✅ On-chain compliance | ⚠️ Varies by module | ❌ None | +| **Market Adoption** | New (2025) | European markets | Limited | Widespread | +| **Implementation Complexity** | Low | High | High | Low | +| **Upgrade Path** | Via RTA Proxy | Contract migrations | Module updates | Immutable | + +**Key Differentiator**: ERC-1450's RTA monopsony model is not a limitation but its core security feature. While other standards offer flexibility, ERC-1450 prioritizes regulatory compliance and operational simplicity through exclusive RTA control—essential for SEC-regulated securities where the transfer agent model is legally mandated. + +### Relationship to Security Token Suites + +Various security token suites provide comprehensive frameworks through multiple complementary standards covering: +- Core security token functionality with transfer restrictions +- Document management for off-chain documents +- Controller operations for forced transfers + +While ERC-1450 appears to overlap with these standards (court-ordered transfers align with controller operations, document references align with document management), we deliberately chose not to extend existing suites for the following reasons: + +1. **Philosophical Difference**: Other security token suites enables flexible, modular compliance where different operators can have different rules. ERC-1450 enforces a single, rigid model where only the RTA has control - essential for SEC compliance and adaptable to similar regulatory models globally. This isn't a limitation—it's the core security feature. + +2. **Regulatory Alignment**: Other security token suites was designed for global markets with varying regulations. ERC-1450 is explicitly designed for US SEC-regulated securities with RTA requirements, while providing a framework that other jurisdictions can adopt for their designated transfer controller models. We prioritize regulatory clarity over flexibility. + +3. **Simplicity Over Modularity**: Other security token suites uses multiple interconnected contracts and complex partition logic. ERC-1450 uses a single contract with clear, restricted operations. This reduces attack surface and audit complexity. + +4. **RTA Exclusivity**: Other security token suites's controller model allows for multiple controllers or changing controllers. ERC-1450's RTA model explicitly prevents this—the RTA cannot be changed without cooperative action, protecting against issuer key compromise. + +5. **Gas Efficiency**: By avoiding modular architecture and partition management, ERC-1450 operations are significantly more gas-efficient, important for retail investors on L2s. + +**Why Not Extend Existing Security Token Standards?** +Extending existing suites would require supporting their controller models, partition systems, and modular architectures—all of which conflict with the designated transfer controller model used in many regulated securities markets. The approaches are fundamentally incompatible. + +### Alignment with Established Security Token Patterns + +ERC-1450 leverages established security token patterns for maximum interoperability: + +**Controller Operations Integration:** +- Implements standard `controllerTransfer` function for forced transfers +- Emits standard `ControllerTransfer` events that existing tools can index +- Uses `operatorData` parameter to specify transfer type while maintaining standard interface + +**Document Management Integration:** +- Defines an optional document management interface aligned with established security token patterns: `setDocument`, `getDocument`, `removeDocument`, `getAllDocuments` +- When implemented, all evidence is stored as document URIs with cryptographic hashes +- Never stores PII directly on-chain, only references +- Emits standard `DocumentUpdated` and `DocumentRemoved` events for audit trails + +**Semantic Clarity Through Data Fields:** +The `operatorData` parameter in `controllerTransfer` encodes: +- Document type (e.g., "COURT_ORDER", "REGULATORY_ACTION", "ESTATE_DISTRIBUTION") +- Document URI for off-chain storage +- Cryptographic hash for integrity verification + +This approach maintains standard function signatures while preserving the semantic precision required for regulatory compliance. + +ERC-1450 specifically addresses US market needs and SEC requirements by: +- Leveraging the existing RTA infrastructure mandated by SEC Rule 17Ad +- Maintaining investor privacy with off-chain identity management +- Providing simple, gas-efficient single contract architecture +- Enabling omnibus custody models used by US broker-dealers +- Supporting fee-based secondary markets with broker registration + +While designed to meet stringent SEC requirements, the standard's controller model can be adapted to other jurisdictions' regulatory frameworks that employ similar designated transfer controller models, making it globally applicable while ensuring US regulatory compliance. + +### Fractional Shares Support + +Unlike earlier proposals that forced `decimals()` to return 0, ERC-1450 allows configurable decimal places set at deployment. This flexibility recognizes that: + +1. **Modern Markets Support Fractions**: Many securities now trade in fractional amounts: + - Mutual funds and ETFs often have fractional shares + - REITs frequently allow fractional ownership + - Modern broker-dealers offer fractional share trading for retail investors + - Dividend reinvestment plans (DRIPs) create fractional shares + +2. **Immutable at Deployment**: The decimal places are set once at contract creation and cannot be changed, ensuring consistency throughout the security's lifecycle. + +3. **RTA Control Maintained**: Whether whole or fractional, all transfers remain under exclusive RTA control, maintaining regulatory compliance. + +This design allows issuers to choose the appropriate divisibility for their specific security type while maintaining the strict RTA control model. + +### Wallet and DEX Integration via ERC-165 + +ERC-1450 implements ERC-165 introspection to prevent broken user experiences in wallets, DEXs, and other tools that expect standard ERC-20 behavior. + +**Detection Flow for Integrators:** + +```solidity +// 1. Check if it's a security token (quickest check) +if (token.isSecurityToken()) { + // This is ERC-1450, disable transfer/approve UI + return handleSecurityToken(); +} + +// 2. Alternative: Check via ERC-165 +bytes4 IERC1450_ID = 0x[computed_interface_id]; +if (token.supportsInterface(IERC1450_ID)) { + // This is ERC-1450, handle accordingly + return handleSecurityToken(); +} + +// 3. For maximum compatibility, also check: +bytes4 IERC20_ID = 0x36372b07; +bool isERC20 = token.supportsInterface(IERC20_ID); +bool isSecure = token.isSecurityToken(); +if (isERC20 && isSecure) { + // Restricted ERC-20 interface detected + showRestrictedTokenUI(); +} +``` + +**Expected Wallet/DEX Behavior:** + +1. **Display**: Show token balances normally (read-only operations work) +2. **Transfers**: Hide or disable transfer/send buttons +3. **Swaps**: Exclude from DEX trading interfaces +4. **Approvals**: Hide or disable approval interfaces +5. **Information**: Display "Security Token - Transfers Restricted" or similar +6. **Secondary Market**: Optionally provide link to compliant secondary market + +This introspection mechanism ensures that: +- Wallets don't show broken transfer interfaces +- DEXs don't attempt to list restricted tokens +- Portfolio trackers can display holdings correctly +- Users understand the token's restricted nature + +**Optional: ERC-1820 Registry** + +For broader discovery, implementations MAY also register with the [ERC-1820 Pseudo-introspection Registry](./eip-1820.md). This allows any address (including externally owned accounts acting as proxies) to publish interface support: + +```solidity +// Optional ERC-1820 registration +bytes32 constant private IERC1450_HASH = keccak256("IERC1450Token"); +registry.setInterfaceImplementer(address(this), IERC1450_HASH, address(this)); +``` + +However, ERC-165 support is sufficient for most use cases and is simpler to implement. + +## Backwards Compatibility + +ERC-1450 implements the ERC-20 interface but with critical behavioral differences. Per the [ERC-20 specification](./eip-20.md#specification), functions MAY return `false` or `revert()` on failure. The specification notes: "Callers MUST handle `false` from `returns (bool)`. Callers MUST NOT assume that `false` is never returned!" + +This standard is ABI-compatible with ERC-20 for reads; state-changing ERC-20 flows are disallowed by design. Contracts MUST implement ERC-165 and expose the `IERC1450` interface ID so clients can detect restricted semantics before offering send/approve UI. Note that ERC-20 itself does not define ERC-165 detection; using ERC-165 for ERC-20 interface detection is acceptable but not universally supported. The `isSecurityToken()` helper function provides an additional discovery mechanism, though ERC-165 and ERC-1820 should be considered the primary discovery methods. + +ERC-1450 makes the following deliberate choices: + +**Read-Only Functions (Fully Compatible):** +* **`function totalSupply() external view returns (uint256)`** - Works normally +* **`function balanceOf(address account) external view returns (uint256)`** - Works normally +* **`function allowance(address owner, address spender) external view returns (uint256)`** - MUST always return `0` + +**Restricted Functions (Modified Behavior):** +* **`function transfer(address to, uint256 amount) external returns (bool)`** and **`function approve(address spender, uint256 amount) external returns (bool)`**: + * **MUST always `revert`** with `ERC1450TransferDisabled` error (never return `false`) + * This is permitted by ERC-20 which allows revert as a failure mode + * Holder-initiated transfers are not legal for regulated securities + +* **`function transferFrom(address from, address to, uint256 amount) external returns (bool)`**: + * **MUST always `revert`** with `ERC1450TransferDisabled` error (never return `false`) + * This standard ERC-20 function is disabled to prevent confusion + * Use `transferFromRegulated()` for actual transfers with regulation tracking + +* **Critical for Integrators**: + * **Implementers MUST expose ERC-165 interface IDs** (`supportsInterface` and `isSecurityToken`) + * This allows clients to detect non-standard ERC-20 behavior before attempting transfers + * Without this detection, wallets and DEXs would mis-assume standard ERC-20 semantics + * The interface detection prevents users from attempting operations that will always fail + +* **`Approval` event**: + * Will never be emitted as `approve()` always reverts + * Implementations MAY omit this event entirely + +## Test Cases + +Test cases are provided in the reference implementation repository (see Reference Implementation section). + +## Reference Implementation + +A production-ready reference implementation is available at `github.com/StartEngine/erc1450-reference`. + +This implementation has completed a comprehensive security audit by **Halborn Security** (December 2025) with all findings addressed. See the repository for full audit report and remediation details. + +The reference implementation includes: +- Full ERC-1450 compliant token contract +- RTA Proxy pattern for enhanced security +- Comprehensive test suite covering all functionality +- Deployment scripts and integration examples +- Gas optimization benchmarks +- Contract versioning with automatic sync from package.json + +Key features demonstrated in the reference implementation: +- Transfer request workflow with fee collection +- Recovery mechanism with timelock security +- Court-ordered transfers and recovery procedures +- Integration with existing ERC-20 infrastructure +- Version tracking for upgrade detection + +### Contract Versioning + +The reference implementation includes automatic version synchronization between the npm package version and the contract `version()` function. This enables: + +1. **Upgrade Detection**: Compare deployed contract version against local artifacts to detect available upgrades +2. **Audit Trail**: Know exactly which version was deployed at any given time +3. **Bytecode Verification**: Match deployed bytecode hash against expected hash for security verification + +```solidity +// Query version from deployed contract +string memory deployedVersion = token.version(); // Returns "1.17.0" + +// Compare with local artifacts to detect upgrades +if (keccak256(bytes(deployedVersion)) != keccak256(bytes(localVersion))) { + // Upgrade available +} +``` + +The version is automatically synced via a pre-commit hook, ensuring the contract version always matches the package version without manual updates. + +## Security Considerations + +### Key Management and Custody + +**Investor Private Key Loss**: +When investors lose access to their private keys, the RTA's exclusive control over transfers enables recovery procedures. Unlike permissionless tokens where lost keys mean permanently lost assets, ERC-1450's RTA can execute court-ordered recovery transfers from lost addresses to new investor-controlled addresses after proper legal verification. + +**Transfer Agent Key Security**: +The RTA MUST implement institutional-grade key management including: +- Hardware Security Modules (HSMs) or secure custody solutions (e.g., Fireblocks) +- Multi-signature requirements for critical operations +- Key rotation procedures through the RTAProxy pattern +- Geographically distributed key shards to prevent single points of failure + +**Issuer Key Compromise**: +The RTAProxy pattern protects against compromised issuer keys by preventing unauthorized RTA changes. Once the RTAProxy is set as the transfer agent, even a compromised issuer cannot redirect token control to an attacker. + +### RTA Control Model Rationale + +**Why the Transfer Controller Has Unilateral Control**: +The design decision to give the designated transfer controller exclusive control over `changeIssuer` (preventing even the issuer from changing the issuer address themselves) is intentional and based on operational requirements: + +1. **Regulatory Independence in US Markets**: In the United States, SEC Rule 17Ad-10 requires transfer agents to establish and maintain adequate internal accounting controls. SEC Rule 17Ad-11 mandates accurate recordkeeping. Transfer agents have fiduciary duties to shareholders that must remain independent from issuer influence. While other jurisdictions may have different requirements, this standard implements the US model which can be adapted for other regulatory frameworks. + +2. **Security Through Regulation**: In US markets, RTAs are heavily regulated entities with: + - SEC registration under Section 17A of the Securities Exchange Act + - Compliance with Rules 17Ad-1 through 17Ad-22 + - Regular FINRA examinations under Rule 17Ad-13 + - Statutory liability for failures + - Professional insurance requirements + - Established business continuity plans + +3. **Issuer Key Vulnerability**: Issuers (often startups) typically lack institutional-grade key management. If an issuer's keys are compromised and they could change the RTA, an attacker could: + - Replace the legitimate RTA with their own address + - Steal all tokens from investors + - Destroy the entire cap table + +4. **Legitimate RTA Changes Are Supported**: The model does support changing RTAs through cooperative action: + - Current RTA and issuer negotiate transition + - Legal agreements are executed off-chain + - Current RTA initiates the technical handoff + - New RTA accepts responsibility + - This mirrors traditional RTA transitions in conventional securities + +**Alternative Models Considered**: +- **Dual-control**: Would violate SEC Rule 17Ad requirements for RTA independence +- **Time-locks**: Could prevent emergency actions required by court orders +- **Multi-sig with issuer**: Reintroduces issuer key compromise risk + +This design prioritizes regulatory compliance and investor protection over decentralization. For fully decentralized governance tokens, other standards like ERC-20 remain more appropriate. + +### Smart Contract Security + +**Reentrancy Protection**: +All state changes MUST occur before external calls. The restricted nature of ERC-1450 (direct value movement disabled, all transfers require RTA execution) naturally limits reentrancy vectors, but implementations should still follow check-effects-interactions patterns. + +**Integer Overflow/Underflow**: +Solidity 0.8.x provides automatic overflow protection. Implementations using earlier versions MUST use SafeMath or equivalent libraries for all arithmetic operations. + +**Authorization Bypasses**: +Critical functions are protected by modifiers (`onlyTransferAgent`). Implementations MUST ensure: +- Modifiers check `msg.sender` against stored RTA address +- No functions exist that bypass RTA authorization +- The `transfer()` and `approve()` functions must ALWAYS revert with appropriate [ERC-6093](./eip-6093.md) errors + +### Regulatory Compliance Risks + +**Unauthorized Transfers**: +The disabled `transfer()` function prevents investors from bypassing KYC/AML requirements. All transfers must go through the RTA, ensuring regulatory compliance for every transaction. + +**Sanctions Screening**: +The RTA MUST maintain updated sanctions lists and check all parties before executing transfers. The exclusive RTA control ensures no transfers can bypass these checks. + +**Jurisdiction Restrictions**: +Securities often have geographic restrictions. The RTA enforces these through off-chain verification before executing any transfer. + +### Off-Chain Infrastructure Security + +**Database Compromise**: +As mentioned in the specification, RTAs maintain off-chain databases of investor information. These systems MUST implement: +- Encryption at rest and in transit +- Regular security audits +- Access controls and audit logging +- Backup and recovery procedures +- Data residency compliance + +**Oracle Risks**: +If the implementation relies on oracles for pricing or other data: +- Multiple oracle sources should be used to prevent manipulation +- Circuit breakers should halt operations on suspicious data +- Time delays for critical operations based on oracle data + +### Denial-of-Service Risks + +**RTA Availability**: +The RTA being the sole transfer authority creates a potential bottleneck. Mitigations include: +- High-availability infrastructure with redundancy +- Service Level Agreements (SLAs) for uptime +- Batch processing capabilities to handle high volumes +- Emergency procedures for RTA unavailability + +**Gas Griefing**: +Batch operations should implement gas limits per operation to prevent one failed transfer from reverting an entire batch. + +### DeFi Integration Risks + +**Incompatibility with DEXs**: +ERC-1450 tokens cannot be traded on standard DEXs due to disabled `transfer()` and `approve()` functions. This is intentional for regulatory compliance. + +**Wrapper Contract Risks**: +Any wrapper contracts that attempt to make ERC-1450 tokens tradeable MUST be carefully audited as they could bypass regulatory controls. The RTA should monitor for and potentially restrict transfers to unauthorized wrapper contracts. + +**Flash Loan Attacks**: +The disabled `transfer()` function prevents flash loan attacks. However, any future extensions should carefully consider flash loan implications. + +### Upgrade and Migration Security + +**Upgrade Authority**: +If the implementation uses upgradeable proxy patterns, upgrade authority MUST be carefully controlled, potentially requiring both RTA and issuer approval. + +**Migration Procedures**: +Token migrations to new contracts should include: +- Snapshot mechanisms to preserve balances +- Time-locked migration periods +- Rollback capabilities in case of issues +- Clear communication to all stakeholders + +### Emergency Response + +**Circuit Breakers**: +Implementations should include emergency pause mechanisms that can be triggered by the RTA in case of: +- Smart contract vulnerabilities discovered +- Regulatory enforcement actions +- Custody provider compromises + +**Incident Response Plan**: +RTAs must maintain documented procedures for: +- Key compromise scenarios +- Smart contract vulnerabilities +- Regulatory interventions +- System outages + +These security considerations are informed by operational experience from SEC-registered transfer agents managing billions in compliant securities offerings. The restricted nature of ERC-1450, while limiting functionality compared to permissionless tokens, provides strong security guarantees essential for regulatory compliance and investor protection. + + +## Copyright + +Copyright and related rights waived via [CC0](../LICENSE.md). diff --git a/doc/ERCSpecification/draft-erc-1643-document.md b/doc/ERCSpecification/draft-erc-1643-document.md index 20cc0737..7b25259a 100644 --- a/doc/ERCSpecification/draft-erc-1643-document.md +++ b/doc/ERCSpecification/draft-erc-1643-document.md @@ -1,158 +1,118 @@ --- -eip: 1643 -title: Document Management for Security Tokens -description: Interface to attach, update, remove, and enumerate legal or operational documents for token contracts. -author: Adam Dossa (@adamdossa), Pablo Ruiz (@pabloruiz55), Fabian Vogelsteller (@frozeman), Stephane Gosselin (@thegostep), Ryan Sauge (@rya-sge) -discussions-to: https://ethereum-magicians.org/t/erc-1643-document-management-standard-erc-1400/27437 + +eip: ERC-1643 +title: Document Management Standard (part of the ERC-1400 Security Token Standards) +author: Adam Dossa (@adamdossa), Pablo Ruiz (@pabloruiz55), Fabian Vogelsteller (@frozeman), Stephane Gosselin (@thegostep) +discussions-to: #1411 status: Draft type: Standards Track category: ERC created: 2018-09-09 ---- +require: None -## Abstract +--- -This ERC defines a standard interface for associating documents with a token contract and for notifying off-chain systems when those documents change. Documents can represent legal agreements, offering materials, disclosures, or other issuer-provided references needed for security token operations. +## Simple Summary -## Motivation +This standard sits under the ERC-1400 (#1411) umbrella set of standards related to security tokens. -Security tokens commonly represent assets with legal rights and obligations that depend on external documents. Wallets, exchanges, custodians, and compliance tools need a predictable way to discover those documents and track updates. +Provides a standard to support attaching documentation to smart contracts, specifically security token contracts in the context of ERC-1400 (#1411). -Without a standard, each implementation exposes different storage and retrieval methods, increasing integration cost and operational risk. A common interface allows ecosystem participants to read and monitor document metadata consistently. +## Abstract -Within security token frameworks, the document management component highlights that security tokens usually have associated documentation such as offering documents and legend details. The ability to set, remove, and retrieve these documents, with events emitted on those actions, allows investors and integrators to remain up to date. +Allows documents to be associated with a smart contract and a standard interface for querying / modifying these contracts, as well as receiving updates (via events) to changes on these documents. -This ERC intentionally does not define an on-chain mechanism for investors to attest they have read or agreed to any document. +Examples of documentation could include offering documents and legends associated with security tokens. -Although originally designed for security tokens built on [ERC-20](./eip-20.md) as part of a broader real-world asset token suite, this interface is not restricted to that context. It can be adopted by any token standard, including [ERC-721](./eip-721.md) non-fungible tokens and [ERC-1155](./eip-1155.md) multi-tokens, as well as by decentralized applications, vaults, and any other on-chain product that requires structured document management. +## Motivation -Historically, this proposal was authored as part of a broader security token standards suite that had not yet been merged in this repository when this proposal was added. +Accelerate the issuance and management of securities on the Ethereum blockchain by specifying a set of standard interfaces through which security tokens can be operated on and interrogated by all relevant parties. -## Specification +Since security tokens and their ownership usually entails rights and obligations either from the investor or the issuer, the ability to associate legal documents with the relevant contracts is important. Doing this in a standardised way allows wallets, exchanges and other ecosystem members to provide a standard view of these documents, and allows investors to subscribe to updates in a standardised manner. -The key words "MUST", "MUST NOT", "REQUIRED", "SHOULD", and "MAY" in this document are to be interpreted as described in RFC 2119 and RFC 8174. +## Requirements -Implementations MUST support querying and subscribing to updates on any relevant documentation for the security. +See ERC-1400 (#1411) for a full set of requirements across the library of standards. -A document entry is identified by a name (`bytes32`) and stores: +The following requirements have been compiled following discussions with parties across the Security Token ecosystem. -- A URI (`string`) pointing to the document location. -- A content hash (`bytes32`) for integrity checks. -- A last-modified timestamp (`uint256`) set when the entry is written. +- MUST support querying and subscribing to updates on any relevant documentation for the security. -### Interface +## Rationale -```solidity -/// @title IERC1643 Document Management -interface IERC1643 { - /// @notice Reverts when `setDocument` is called with `name == bytes32(0)`. - error ERC1643InvalidName(); +Being able to attach documents to a security token allows the issuer, or other authorised entities, to communicate documentation associated with the security to token holders. An attached document can optionally include a hash of its contents in order to provide an immutability guarantee. - /// @notice Reverts when `removeDocument` is called for a missing document. - error ERC1643MissingDocument(); +## Specification - /// @notice Returns metadata for a document identified by `name`. - /// @return uri Document location. - /// @return documentHash Hash of the document contents. - /// @return lastModified Last update timestamp. - function getDocument(bytes32 name) external view returns (string memory uri, bytes32 documentHash, uint256 lastModified); +These functions are used to manage a library of documents associated with the token. These documents can be legal documents, or other reference materials. - /// @notice Creates or updates a document entry. - /// @dev MUST emit `DocumentUpdated` on success. - function setDocument(bytes32 name, string calldata uri, bytes32 documentHash) external; +A document is associated with a short name (represented as a `bytes32`), a modified timestamp, and can optionally have a hash of the document contents associated with it on-chain. - /// @notice Removes an existing document entry. - /// @dev MUST emit `DocumentRemoved` on success. - function removeDocument(bytes32 name) external; +It is referenced via a generic URI that could point to a website or other document portal. - /// @notice Returns all document names currently tracked by the contract. - function getAllDocuments() external view returns (bytes32[] memory documentNames); +### getDocument - /// @notice Emitted when a document is created or updated. - event DocumentUpdated(bytes32 indexed name, string uri, bytes32 documentHash); +Used to return the details of a document with a known name (`bytes32`). Returns the URI associated with the document (`string`), the hash (of the contents) of the document (`bytes32`), and the timestamp at which the document was last modified via `setDocument` (`uint256`). - /// @notice Emitted when a document is removed. - event DocumentRemoved(bytes32 indexed name, string uri, bytes32 documentHash); -} +``` solidity +function getDocument(bytes32 _name) external view returns (string, bytes32, uint256); ``` -### Interface Detection ([ERC-165](./eip-165.md)) +### setDocument -Implementations SHOULD support ERC-165 interface detection. +Used to attach a new document to the contract, or update the URI or hash of an existing attached document. -When ERC-165 is implemented, `supportsInterface` SHOULD return `true` for `type(IERC1643).interfaceId` and for the ERC-165 interface id. +`setDocument` MUST throw if the document is not successfully stored. -### Function Requirements +`setDocument` MUST emit a `DocumentUpdated` event with details of the document being attached or modified. -- `getDocument`: - - MUST return the latest values for the provided document name. - - MUST return empty values when the entry does not exist (`""`, `bytes32(0)`, `0`). - - MUST NOT revert solely because the entry does not exist. - -- `setDocument`: - - MUST create a new entry when `name` is not present. - - MUST overwrite the existing entry when `name` already exists. - - MUST update the stored last-modified timestamp. - - MUST emit `DocumentUpdated` after state changes. - - MUST revert if the update cannot be persisted. - - SHOULD revert when `name == bytes32(0)` to avoid ambiguous/default-key usage. - - `uri` and `documentHash` MAY be empty (`""` and `bytes32(0)`), depending on issuer workflow and document lifecycle stage. - - Implementations MAY decide to reject empty `uri` and/or empty `documentHash` based on policy requirements. - - Implementations SHOULD use the custom error defined in the interface (`ERC1643InvalidName()`) when rejecting `name == bytes32(0)`. - - Implementations MAY use different error names/signatures than those shown in this specification. - -- `removeDocument`: - - MUST remove the entry identified by `name`. - - MUST emit `DocumentRemoved` with the removed metadata. - - MUST revert if removal cannot be completed. - - Implementations SHOULD use the custom error defined in the interface (`ERC1643MissingDocument()`) when the named document does not exist. - - Implementations MAY use different error names/signatures than those shown in this specification. - -- `getAllDocuments`: - - MUST include every document name added by `setDocument` and not removed by `removeDocument`. - - MUST NOT include removed document names. - -## Rationale +``` solidity +function setDocument(bytes32 _name, string _uri, bytes32 _documentHash) external; +``` -The standard uses `bytes32` names to keep keys compact and deterministic, while leaving naming conventions to implementations. A URI-based pointer is used instead of on-chain document storage to avoid high gas costs and to support existing off-chain document systems. +### removeDocument -Including a document hash enables clients to verify that fetched off-chain content matches issuer-published metadata. Emitting update and removal events supports indexing and near-real-time monitoring without repeated full-state polling. +Used to remove an existing document from the contract. -While a human-readable document title cannot always be represented directly in `bytes32` without hashing or canonicalization, `bytes32` remains practical for on-chain identifiers because fixed-size values can be compared directly (`a == b`). By contrast, `string` comparisons generally require hashing (for example, `keccak256(bytes(s))`), which increases contract code size and gas usage when repeated comparisons are needed on-chain, such as locating and removing a document name from an array. +`removeDocument` MUST throw if the document is not successfully removed. -## Backwards Compatibility +`removeDocument` MUST emit a `DocumentRemoved` event with details of the document being attached or modified. -This ERC is additive and does not alter base token transfer semantics. It can be implemented alongside existing token standards and permissioning systems without changing their core behavior. +``` solidity +function removeDocument(bytes32 _name) external; +``` -## Test Cases +### getAllDocuments -Implementations should verify at least the following: +Used to retrieve a full list of documents attached to the smart contract. -- Adding a new document and reading it through `getDocument`. -- Updating an existing document and validating changed URI/hash/timestamp. -- Removing a document and ensuring it is no longer returned by `getAllDocuments`. -- Emission of `DocumentUpdated` on create/update and `DocumentRemoved` on delete. -- Enumeration consistency after multiple add/update/remove operations. +Any document added via `setDocument` and not subsequently removed via the `removeDocument` function MUST be returned. -## Reference Implementation +``` solidity +function getAllDocuments() view returns (bytes32[]); +``` -The interface is provided in [the reference interface](../assets/eip-1643/src/erc-1643/IERC1643.sol). A reusable abstract module implementing the full interface is provided in [the reference module](../assets/eip-1643/src/erc-1643/ERC1643.sol). Example integrations attaching the module to [ERC-20](./eip-20.md) and [ERC-721](./eip-721.md) tokens are provided in [the ERC-20 example](../assets/eip-1643/src/ERC20DocumentToken.sol) and [the ERC-721 example](../assets/eip-1643/src/ERC721DocumentToken.sol). These examples use the OpenZeppelin library and restrict document mutation to the contract owner. They are provided for educational purposes only and have not been audited. +## Interface -The module maintains: +``` solidity +/// @title IERC1643 Document Management (part of the ERC1400 Security Token Standards) +/// @dev See https://github.com/SecurityTokenStandard/EIP-Spec -- Mapping from `bytes32` name to document metadata. -- Array/set for enumeration of active names. -- Index tracking to support O(1) removals from the enumeration set. +interface IERC1643 { -## Security Considerations + // Document Management + function getDocument(bytes32 _name) external view returns (string, bytes32, uint256); + function setDocument(bytes32 _name, string _uri, bytes32 _documentHash) external; + function removeDocument(bytes32 _name) external; + function getAllDocuments() external view returns (bytes32[]); -- Document URIs may reference mutable off-chain content. Consumers are strongly encouraged to verify content using the published `documentHash` and trusted retrieval channels. -- Implementations should protect `setDocument` and `removeDocument` with appropriate authorization, otherwise unauthorized actors can modify legal or operational references. -- Applications should treat event streams as advisory and reconcile against on-chain state when correctness is critical. -- Document names may not always fit cleanly into `bytes32`, especially for long legal titles. Implementations should avoid lossy truncation of human-readable names; using a deterministic hash-based identifier (for example, the document content hash or a hash of a canonical full title) as the `bytes32` name is a safer alternative. -- The custom errors `ERC1643InvalidName()` and `ERC1643MissingDocument()` are defined in this interface but were absent from the original [ERC-1643](./eip-1643.md) proposal text. Older implementations may not define these errors and may instead revert with strings or implementation-specific error patterns. Integrators should not assume all ERC-1643 contracts expose identical revert data. -- ERC-165 interface detection was also not part of the earlier ERC-1643 draft text. Older implementations may not expose `supportsInterface` for ERC-165 or `IERC1643`, so integrators should treat ERC-165 support as optional when interacting with legacy deployments. + // Document Events + event DocumentRemoved(bytes32 indexed _name, string _uri, bytes32 _documentHash); + event DocumentUpdated(bytes32 indexed _name, string _uri, bytes32 _documentHash); -## Copyright +} +``` -Copyright and related rights waived via [CC0](../LICENSE.md). +## References +- [EIP 1400: Security Token Standard With Partitions](https://github.com/ethereum/EIPs/issues/1411) +- [EIP Draft](https://github.com/SecurityTokenStandard/EIP-Spec) \ No newline at end of file diff --git a/doc/ERCSpecification/rework/ERC-8003.md b/doc/ERCSpecification/rework/ERC-8003.md new file mode 100644 index 00000000..15be3965 --- /dev/null +++ b/doc/ERCSpecification/rework/ERC-8003.md @@ -0,0 +1,131 @@ +--- +eip: 8303 +title: Contract Version +description: Interface for exposing a contract implementation version string +author: Ryan Sauge (@rya-sge) +discussions-to: https://ethereum-magicians.org/t/erc-8303-contract-version/28795 +status: Draft +type: Standards Track +category: ERC +created: 2026-02-12 +--- + +## Abstract + +This ERC defines a minimal interface to expose a contract version string through a standardized `version()` view function. The design is based on the version pattern used by [ERC-3643](./eip-3643.md), while remaining token-agnostic and applicable to other smart contract domains, including DeFi applications such as lending protocols. + +## Motivation + +Integrators frequently need a simple, on-chain way to identify which contract implementation they interact with. A standardized version function improves: + +- integration safety (feature gating by version), +- operations (faster incident triage), +- governance and migration tracking (upgrade visibility), +- ecosystem tooling interoperability. + +It is also useful for end-users, developers, and security auditors to identify which version of a codebase is currently used by a deployed contract. + +The same requirement appears in permissioned token systems ([ERC-3643](./eip-3643.md)) and in DeFi systems where contracts evolve over time. + +## Specification + +The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in [RFC 2119](https://www.rfc-editor.org/rfc/rfc2119) and [RFC 8174](https://www.rfc-editor.org/rfc/rfc8174). + +### Interface + +```solidity +interface IERC8303 { + /// @notice Returns the implementation version string. + /// @return The version value (for example "1.0.0"). + function version() external view returns (string memory); +} +``` + +### Required Behavior + +1. **Version read** + - `version()` MUST be a view function. + - `version()` MUST NOT revert under normal operation. + - `version()` MUST return a non-empty string. + +2. **Version meaning** + - Returned values SHOULD be stable and machine-comparable by off-chain tooling. + - Returned values SHOULD follow a Semantic Versioning 2.0.0-like format: `MAJOR.MINOR.PATCH` using decimal integers (for example `1.0.0`, `3.2.1`). + - The canonical recommended pattern is `^[0-9]+\.[0-9]+\.[0-9]+$`. + - Implementations MAY define their own versioning policy, but SHOULD document it publicly. + +3. **Deployment model compatibility** + - This interface is compatible with immutable deployments and proxy-based upgradeable deployments. + - In upgradeable systems, `version()` SHOULD reflect the active implementation seen by users and integrators. + +### [ERC-165](./eip-165.md) + +Implementations SHOULD support [ERC-165](./eip-165.md) interface discovery for this interface. + +If an implementation supports [ERC-165](./eip-165.md), `supportsInterface(type(IERC8303).interfaceId)` MUST return `true`. + +- The interface id for `IERC8303` is `0x54fd4d50`. + +### Compatibility Note for ERC-3643 Integrations + +Integrators MAY treat legacy ERC-3643 token contracts exposing a compatible `version()` function as implementing this ERC even if they do not advertise ERC-165 support. + +## Rationale + +- **Minimal scope**: A single function maximizes adoption and keeps gas/runtime complexity negligible. +- **ERC-3643 alignment**: Reuses a proven pattern already used in regulated token implementations. +- **Token-agnostic design**: The interface applies to token contracts and non-token contracts alike. +- **Optional ERC-165**: ERC-165 support is recommended but not required, lowering the adoption barrier for contracts that do not implement interface discovery. When ERC-165 is supported, advertising this interface is mandatory to ensure consistent detection by integrators. +- **`string` over `bytes32`**: A human-readable string is preferred to a fixed-size bytes32 for legibility in explorers and tooling, at the cost of marginally higher gas for the return value. + +## Backwards Compatibility + +This ERC is fully additive. Contracts already exposing `version()` are naturally compatible if they match the interface signature. + +## Test Cases + +The following test cases apply to any conforming implementation. + +1. `version()` MUST NOT revert. +2. `version()` MUST return a non-empty string. +3. `version()` MUST return the version string declared by the implementation (e.g. `"1.0.0"`). +4. If the contract supports [ERC-165](./eip-165.md), `supportsInterface(0x54fd4d50)` MUST return `true`. +5. If the contract supports [ERC-165](./eip-165.md), `supportsInterface(0xffffffff)` MUST return `false`. + +## Reference Implementation + +Reference implementations are provided in the assets folder: the [interface](../assets/erc-8303/src/IERC8303.sol) and a [base implementation](../assets/erc-8303/src/ERC8303.sol), along with usage examples for [ERC-20](../assets/erc-8303/src/examples/ERC20VersionedExample.sol) and [ERC-721](../assets/erc-8303/src/examples/ERC721VersionedExample.sol) tokens. These examples are provided for educational purposes only and are not audited. + +```solidity +// SPDX-License-Identifier: CC0-1.0 +pragma solidity ^0.8.0; + +import "./IERC8303.sol"; +import "@openzeppelin/contracts/utils/introspection/ERC165.sol"; + +contract ERC8303Example is IERC8303, ERC165 { + function version() external pure override returns (string memory) { + return "1.0.0"; + } + + function supportsInterface(bytes4 interfaceId) + public + view + override + returns (bool) + { + return interfaceId == type(IERC8303).interfaceId + || super.supportsInterface(interfaceId); + } +} +``` + +## Security Considerations + +- `version()` is metadata and must not be used as a sole authorization primitive. +- In upgradeable systems, governance controls remain the trust anchor; version reporting does not prevent malicious upgrades. +- Integrators should combine version checks with other trust signals (governance model, audits, deployment provenance). + +## Copyright + +Copyright and related rights waived via [CC0](../LICENSE.md). diff --git a/doc/ERCSpecification/rework/erc-1404.md b/doc/ERCSpecification/rework/erc-1404.md new file mode 100644 index 00000000..dc8d7a01 --- /dev/null +++ b/doc/ERCSpecification/rework/erc-1404.md @@ -0,0 +1,184 @@ +--- +eip: 1404 +title: Simple Restricted Token +description: An interface for enforcing token transfer restrictions with machine-readable status codes. +author: Ron Gierlach (@rongierlach), James Poole (@pooleja), Mason Borda (@masonicgit), Lawson Baker (@lwsnbaker), Ryan Sauge (@rya-sge) +discussions-to: https://ethereum-magicians.org/t/erc-1404-simple-restricted-token-standard/1405 +status: Draft +type: Standards Track +category: ERC +created: 2018-07-27 +--- + +## Abstract + +Current token standards have provided the community with a platform on which to develop a decentralized economy that is focused on building Ethereum applications for the real world. As these applications mature and face consumer adoption, they begin to interface with corporate governance requirements as well as regulations. They must not only be able to meet corporate and regulatory requirements but must also be able to integrate with technology platforms underpinning their associated businesses. What follows is a simple and extendable standard that seeks to ease the burden of integration for wallets, exchanges, and issuers. + +## Motivation + +Token issuers need a way to restrict transfers of tokens to be compliant with securities laws and other contractual obligations. This is most commonly required for [ERC-20](./eip-20.md) tokens, but the same need applies to any token that moves a single amount between two accounts, such as [ERC-777](./eip-777.md). Current implementations do not address these requirements. + +A few examples: + +- Enforcing Token Lock-Up Periods +- Enforcing Passed AML/KYC Checks +- Private Real-Estate Investment Trusts +- Delaware General Corporations Law Shares + +Furthermore, standards adoption amongst token issuers has the potential to evolve into a dynamic and interoperable landscape of automated compliance. + +The following design gives greater freedom / upgradability to token issuers and simultaneously decreases the burden of integration for developers and exchanges. + +Additionally, this standard provides a pattern by which human-readable messages may be returned when token transfers are reverted. Transparency as to _why_ a token's transfer was reverted is of equal importance to the successful enforcement of the transfer restriction itself. + +## Specification + +The key words "MUST", "MUST NOT", "REQUIRED", "SHOULD", and "MAY" in this document are to be interpreted as described in RFC 2119 and RFC 8174. + +[ERC-1404](./eip-1404.md) defines a transfer-restriction interface consisting of the two methods below. It is designed to be applied to a fungible token whose transfer moves a single `uint256` amount from one address to another — canonically [ERC-20](./eip-20.md), and equally applicable to compatible token interfaces such as [ERC-777](./eip-777.md). + +A token claiming [ERC-1404](./eip-1404.md) compliance MUST implement its underlying token interface (for example, [ERC-20](./eip-20.md)) with all functions, events, and semantics unchanged, and MUST additionally implement the following methods. + +### Methods + +- #### `detectTransferRestriction(address,address,uint256)` + + Returns a restriction code for the proposed transfer of `value` tokens from `from` to `to`, or `0` if the transfer is unrestricted. The restriction logic is defined by the issuer. + + Enforcement MUST be consistent with `detectTransferRestriction` for the same state and inputs: a transfer MUST be rejected whenever `detectTransferRestriction` would return a non-zero code for that transfer, and MUST NOT be rejected for restriction reasons when it would return `0`. + + Implementations MAY satisfy this requirement by invoking `detectTransferRestriction` directly inside the token's transfer entry points (for [ERC-20](./eip-20.md), `transfer` and `transferFrom`; for other token interfaces, the analogous transfer methods, such as [ERC-777](./eip-777.md) `send`) and rejecting on a non-zero code. This is RECOMMENDED, because it guarantees the consistency above by construction. Implementations MAY instead re-evaluate the equivalent conditions in the transfer path — including reverting with typed errors (for example, [ERC-7943](./eip-7943.md)) rather than a restriction code — provided the observable outcome remains consistent with `detectTransferRestriction`. + + When a transfer is rejected, reverting is RECOMMENDED; implementations MAY return `false` instead, consistently with the underlying token's transfer expectations. + + ```solidity + function detectTransferRestriction(address from, address to, uint256 value) external view returns (uint8); + ``` + +- #### `messageForTransferRestriction(uint8)` + + Returns the human-readable message corresponding to `restrictionCode`. + + ```solidity + function messageForTransferRestriction(uint8 restrictionCode) external view returns (string memory); + ``` + +### Additional Specifications + +- Implementations MAY add [ERC-165](./eip-165.md) interface detection support for [ERC-1404](./eip-1404.md). + +- Implementations MAY apply analogous restriction checks to token supply-changing operations (for example, mint and burn). Such checks are optional and are not required for [ERC-1404](./eip-1404.md) compliance. + +- The [ERC-165](./eip-165.md) interface identifier for [ERC-1404](./eip-1404.md) is `0xab84a5c8`. + +- Compliance-related contracts (for example, a rule engine or compliance module) MAY implement the [ERC-1404](./eip-1404.md) restriction interface without implementing any token interface at all. In such cases, this standard only defines the behavior of `detectTransferRestriction` and `messageForTransferRestriction`, and the contract is expected to be consulted by a token or other caller that performs the actual transfer. + +- Restriction checks SHOULD be deterministic for the same state and inputs, so that the reporting and enforcement results remain consistent. Guidance on the data a restriction policy relies on is given in [Security Considerations](#security-considerations) and is non-normative. + +- The string returned by `messageForTransferRestriction` SHOULD NOT be treated as an authorization primitive. + +- Implementations SHOULD define and manage restriction code allocation carefully, because `uint8` limits the available code space to 256 values (`0` to `255`). + +## Rationale + +The standard proposes two functions on top of an underlying token interface (canonically [ERC-20](./eip-20.md)). The rationale for each is described below. + +1. `detectTransferRestriction` - This function encodes the restriction logic of an issuer's token transfers. Some examples of this might include, checking if the token recipient is whitelisted, checking if a sender's tokens are frozen in a lock-up period, etc. Because implementation is up to the issuer, this function serves to standardize the _result_ that transfer enforcement must agree with, rather than mandating a specific call site: an implementation may invoke it directly inside the transfer path or re-evaluate the equivalent conditions, as long as the outcome is consistent (see Specification). Additionally, 3rd parties may publicly call this function to check the expected outcome of a transfer. Because this function returns a `uint8` code rather than a boolean or just reverting, it allows the function caller to know the reason why a transfer might fail and report this to relevant counterparties. +2. `messageForTransferRestriction` - This function is effectively an accessor for the "message", a human-readable explanation as to _why_ a transaction is restricted. By standardizing message look-ups, we empower user interface builders to effectively report errors to users. +3. Optional [ERC-165](./eip-165.md) support - Implementations may expose [ERC-165](./eip-165.md) support for interface discovery. +4. Token-agnostic interface - Although [ERC-1404](./eip-1404.md) was originally designed as an [ERC-20](./eip-20.md) extension, its two methods only assume a transfer that moves a single `uint256` amount between two addresses. The interface is therefore reused unchanged with other compatible token interfaces such as [ERC-777](./eip-777.md), and can be implemented by a standalone compliance contract that is itself not a token. The standard deliberately does not require [ERC-20](./eip-20.md), so that it can describe the restriction behavior independently of the token it is attached to. + +## Backwards Compatibility + +By design [ERC-1404](./eip-1404.md) only adds new methods and leaves the underlying token interface untouched, so it is interface-compatible with [ERC-20](./eip-20.md) (and other compatible token interfaces such as [ERC-777](./eip-777.md)). Transfer restrictions may, however, introduce behavioral differences, because otherwise-valid transfers can be rejected. + +## Test Cases + +The following table-driven cases SHOULD be verified for every implementation. The examples use a whitelist-based policy (code `0` = no restriction, `1` = sender not whitelisted, `2` = recipient not whitelisted) to keep the expected values concrete, but the same structure applies to any issuer-defined policy. + +### `detectTransferRestriction` + +| Scenario | Expected return | +|---|---| +| Both `from` and `to` satisfy the policy | `0` | +| `from` violates the policy | Non-zero restriction code | +| `to` violates the policy; `from` does not | Non-zero restriction code distinct from the sender case | +| Both `from` and `to` violate the policy | Non-zero code; the specific code returned depends on the implementation's evaluation order | + +### `messageForTransferRestriction` + +| Input | Expected output | +|---|---| +| `0` | A deterministic human-readable string indicating no restriction (e.g., `"No restriction"`) | +| Any known restriction code | The corresponding deterministic human-readable message | + +### Transfer enforcement + +| Scenario | Expected behavior for `transfer` and `transferFrom` | +|---|---| +| `detectTransferRestriction` returns `0` | Transfer succeeds | +| `detectTransferRestriction` returns a non-zero code | Transfer reverts (preferred) or returns `false` | + +### ERC-165 interface detection (optional) + +For implementations that expose [ERC-165](./eip-165.md) support: + +| Input to `supportsInterface` | Expected return | +|---|---| +| `0xab84a5c8` ([ERC-1404](./eip-1404.md) interface identifier) | `true` | +| `0x01ffc9a7` ([ERC-165](./eip-165.md) interface identifier) | `true` | +| Any unrecognized selector | `false` | + +A complete Foundry test suite covering all the cases above is available at [`test/ERC1404.t.sol`](../assets/eip-1404/test/ERC1404.t.sol). + +## Reference Implementation + +A complete reference implementation built with Foundry and OpenZeppelin Contracts v5 is provided in the assets folder: + +| File | Description | +|------|-------------| +| [`src/IERC1404.sol`](../assets/eip-1404/src/IERC1404.sol) | Interface — extends `IERC20` with the two [ERC-1404](./eip-1404.md) functions | +| [`src/ERC1404.sol`](../assets/eip-1404/src/ERC1404.sol) | Concrete implementation — whitelist-based, with [ERC-165](./eip-165.md) support | +| [`test/ERC1404.t.sol`](../assets/eip-1404/test/ERC1404.t.sol) | Foundry test suite covering all mandatory behaviors | + +The concrete implementation applies a whitelist policy and defines the following restriction codes. Note that codes `1` and `2` are specific to this implementation; [ERC-1404](./eip-1404.md) does not standardize restriction code values beyond reserving `0` as the "no restriction" sentinel. + +| Code | Constant | Message | +|------|----------|---------| +| `0` | `TRANSFER_OK` | `"No restriction"` | +| `1` | `SENDER_NOT_WHITELISTED` | `"Sender not whitelisted"` | +| `2` | `RECIPIENT_NOT_WHITELISTED` | `"Recipient not whitelisted"` | + +Notable design decisions in this implementation: + +- `transfer` and `transferFrom` revert with a typed `TransferRestricted(uint8 code, string message)` error on non-zero codes, rather than returning `false`. +- `detectTransferRestriction` checks the sender before the recipient, so callers can distinguish the two failure cases with a single view call before submitting a transaction. +- `supportsInterface(0xab84a5c8)` returns `true`, enabling on-chain interface discovery. +- Mint and burn operations apply analogous restriction checks, as permitted by the specification. +- Implementations that also conform to [ERC-7943](./eip-7943.md) will likely revert with that standard's typed errors rather than `TransferRestricted(uint8 code, string message)`. Those errors do not carry a restriction code or human-readable message. + +This example is provided for educational purposes only and has not been audited. Do not use in production without a thorough independent security review. + +## Security Considerations + +- Implementations are expected to encode policy in `detectTransferRestriction`, so mistakes in this logic can block valid transfers or allow restricted transfers. + +- Restriction checks that are not deterministic for the same state and inputs can create inconsistent behavior between the reporting and enforcement paths. + +- Because `detectTransferRestriction` is a `view` function, any result obtained off-chain before a transfer is a prediction: the relevant state may change before the transfer executes, so a transfer predicted to succeed may later be rejected (and vice-versa). Implementations and integrators MUST treat the on-chain enforcement at execution time as authoritative, not an earlier off-chain reading. + +- The following is informational guidance, not a conformance requirement. Because the policy in `detectTransferRestriction` is defined entirely by the issuer, its security depends on the data that policy reads. A policy that branches on values the transacting party can influence within the transfer transaction — for example a spot price read from an automated market maker, which a flash loan can move and restore in a single transaction — can be forced to evaluate as unrestricted at execution time, even though the check runs during the transfer. Issuers are encouraged to base restriction decisions on state they control (such as whitelist or frozen flags) rather than on inputs a counterparty can manipulate. + +- When an implementation enforces transfers by re-evaluating the restriction conditions separately rather than calling `detectTransferRestriction` directly (for example, to revert with typed errors such as [ERC-7943](./eip-7943.md)), the reporting function and the enforcement path become two codepaths that can drift apart. If they diverge, `detectTransferRestriction` may return `0` for a transfer that reverts, or a non-zero code for a transfer that succeeds, defeating the purpose of the machine-readable code for exchanges and user interfaces. Such implementations SHOULD treat the equivalence between `detectTransferRestriction` and the enforcement path as an invariant and verify it under test (for example, asserting over many inputs that a non-zero code holds if and only if the transfer is rejected). + +- Returning machine-readable codes improves integration, but the string returned by `messageForTransferRestriction` remains informational and is not an authorization primitive. + +- Using `uint8` for restriction codes limits the available code space to 256 values (`0` to `255`), which can create ambiguity if code allocation is not managed carefully. + +- [ERC-1404](./eip-1404.md) interface support alone is not evidence that a contract implements a full token interface such as [ERC-20](./eip-20.md) or [ERC-777](./eip-777.md); a compliance-focused contract can expose the [ERC-1404](./eip-1404.md) restriction interface and interface support while implementing no token transfer behavior at all. Callers MUST NOT assume that a contract reporting [ERC-1404](./eip-1404.md) support is itself a transferable token. + +- The original [ERC-1404](./eip-1404.md) text did not require [ERC-165](./eip-165.md) signaling. Therefore, older implementations may still implement [ERC-1404](./eip-1404.md) while not returning `true` for the [ERC-165](./eip-165.md) interface identifier `0xab84a5c8`. + +## Copyright + +Copyright and related rights waived via [CC0](../LICENSE.md). diff --git a/doc/ERCSpecification/rework/erc-1643.md b/doc/ERCSpecification/rework/erc-1643.md new file mode 100644 index 00000000..20cc0737 --- /dev/null +++ b/doc/ERCSpecification/rework/erc-1643.md @@ -0,0 +1,158 @@ +--- +eip: 1643 +title: Document Management for Security Tokens +description: Interface to attach, update, remove, and enumerate legal or operational documents for token contracts. +author: Adam Dossa (@adamdossa), Pablo Ruiz (@pabloruiz55), Fabian Vogelsteller (@frozeman), Stephane Gosselin (@thegostep), Ryan Sauge (@rya-sge) +discussions-to: https://ethereum-magicians.org/t/erc-1643-document-management-standard-erc-1400/27437 +status: Draft +type: Standards Track +category: ERC +created: 2018-09-09 +--- + +## Abstract + +This ERC defines a standard interface for associating documents with a token contract and for notifying off-chain systems when those documents change. Documents can represent legal agreements, offering materials, disclosures, or other issuer-provided references needed for security token operations. + +## Motivation + +Security tokens commonly represent assets with legal rights and obligations that depend on external documents. Wallets, exchanges, custodians, and compliance tools need a predictable way to discover those documents and track updates. + +Without a standard, each implementation exposes different storage and retrieval methods, increasing integration cost and operational risk. A common interface allows ecosystem participants to read and monitor document metadata consistently. + +Within security token frameworks, the document management component highlights that security tokens usually have associated documentation such as offering documents and legend details. The ability to set, remove, and retrieve these documents, with events emitted on those actions, allows investors and integrators to remain up to date. + +This ERC intentionally does not define an on-chain mechanism for investors to attest they have read or agreed to any document. + +Although originally designed for security tokens built on [ERC-20](./eip-20.md) as part of a broader real-world asset token suite, this interface is not restricted to that context. It can be adopted by any token standard, including [ERC-721](./eip-721.md) non-fungible tokens and [ERC-1155](./eip-1155.md) multi-tokens, as well as by decentralized applications, vaults, and any other on-chain product that requires structured document management. + +Historically, this proposal was authored as part of a broader security token standards suite that had not yet been merged in this repository when this proposal was added. + +## Specification + +The key words "MUST", "MUST NOT", "REQUIRED", "SHOULD", and "MAY" in this document are to be interpreted as described in RFC 2119 and RFC 8174. + +Implementations MUST support querying and subscribing to updates on any relevant documentation for the security. + +A document entry is identified by a name (`bytes32`) and stores: + +- A URI (`string`) pointing to the document location. +- A content hash (`bytes32`) for integrity checks. +- A last-modified timestamp (`uint256`) set when the entry is written. + +### Interface + +```solidity +/// @title IERC1643 Document Management +interface IERC1643 { + /// @notice Reverts when `setDocument` is called with `name == bytes32(0)`. + error ERC1643InvalidName(); + + /// @notice Reverts when `removeDocument` is called for a missing document. + error ERC1643MissingDocument(); + + /// @notice Returns metadata for a document identified by `name`. + /// @return uri Document location. + /// @return documentHash Hash of the document contents. + /// @return lastModified Last update timestamp. + function getDocument(bytes32 name) external view returns (string memory uri, bytes32 documentHash, uint256 lastModified); + + /// @notice Creates or updates a document entry. + /// @dev MUST emit `DocumentUpdated` on success. + function setDocument(bytes32 name, string calldata uri, bytes32 documentHash) external; + + /// @notice Removes an existing document entry. + /// @dev MUST emit `DocumentRemoved` on success. + function removeDocument(bytes32 name) external; + + /// @notice Returns all document names currently tracked by the contract. + function getAllDocuments() external view returns (bytes32[] memory documentNames); + + /// @notice Emitted when a document is created or updated. + event DocumentUpdated(bytes32 indexed name, string uri, bytes32 documentHash); + + /// @notice Emitted when a document is removed. + event DocumentRemoved(bytes32 indexed name, string uri, bytes32 documentHash); +} +``` + +### Interface Detection ([ERC-165](./eip-165.md)) + +Implementations SHOULD support ERC-165 interface detection. + +When ERC-165 is implemented, `supportsInterface` SHOULD return `true` for `type(IERC1643).interfaceId` and for the ERC-165 interface id. + +### Function Requirements + +- `getDocument`: + - MUST return the latest values for the provided document name. + - MUST return empty values when the entry does not exist (`""`, `bytes32(0)`, `0`). + - MUST NOT revert solely because the entry does not exist. + +- `setDocument`: + - MUST create a new entry when `name` is not present. + - MUST overwrite the existing entry when `name` already exists. + - MUST update the stored last-modified timestamp. + - MUST emit `DocumentUpdated` after state changes. + - MUST revert if the update cannot be persisted. + - SHOULD revert when `name == bytes32(0)` to avoid ambiguous/default-key usage. + - `uri` and `documentHash` MAY be empty (`""` and `bytes32(0)`), depending on issuer workflow and document lifecycle stage. + - Implementations MAY decide to reject empty `uri` and/or empty `documentHash` based on policy requirements. + - Implementations SHOULD use the custom error defined in the interface (`ERC1643InvalidName()`) when rejecting `name == bytes32(0)`. + - Implementations MAY use different error names/signatures than those shown in this specification. + +- `removeDocument`: + - MUST remove the entry identified by `name`. + - MUST emit `DocumentRemoved` with the removed metadata. + - MUST revert if removal cannot be completed. + - Implementations SHOULD use the custom error defined in the interface (`ERC1643MissingDocument()`) when the named document does not exist. + - Implementations MAY use different error names/signatures than those shown in this specification. + +- `getAllDocuments`: + - MUST include every document name added by `setDocument` and not removed by `removeDocument`. + - MUST NOT include removed document names. + +## Rationale + +The standard uses `bytes32` names to keep keys compact and deterministic, while leaving naming conventions to implementations. A URI-based pointer is used instead of on-chain document storage to avoid high gas costs and to support existing off-chain document systems. + +Including a document hash enables clients to verify that fetched off-chain content matches issuer-published metadata. Emitting update and removal events supports indexing and near-real-time monitoring without repeated full-state polling. + +While a human-readable document title cannot always be represented directly in `bytes32` without hashing or canonicalization, `bytes32` remains practical for on-chain identifiers because fixed-size values can be compared directly (`a == b`). By contrast, `string` comparisons generally require hashing (for example, `keccak256(bytes(s))`), which increases contract code size and gas usage when repeated comparisons are needed on-chain, such as locating and removing a document name from an array. + +## Backwards Compatibility + +This ERC is additive and does not alter base token transfer semantics. It can be implemented alongside existing token standards and permissioning systems without changing their core behavior. + +## Test Cases + +Implementations should verify at least the following: + +- Adding a new document and reading it through `getDocument`. +- Updating an existing document and validating changed URI/hash/timestamp. +- Removing a document and ensuring it is no longer returned by `getAllDocuments`. +- Emission of `DocumentUpdated` on create/update and `DocumentRemoved` on delete. +- Enumeration consistency after multiple add/update/remove operations. + +## Reference Implementation + +The interface is provided in [the reference interface](../assets/eip-1643/src/erc-1643/IERC1643.sol). A reusable abstract module implementing the full interface is provided in [the reference module](../assets/eip-1643/src/erc-1643/ERC1643.sol). Example integrations attaching the module to [ERC-20](./eip-20.md) and [ERC-721](./eip-721.md) tokens are provided in [the ERC-20 example](../assets/eip-1643/src/ERC20DocumentToken.sol) and [the ERC-721 example](../assets/eip-1643/src/ERC721DocumentToken.sol). These examples use the OpenZeppelin library and restrict document mutation to the contract owner. They are provided for educational purposes only and have not been audited. + +The module maintains: + +- Mapping from `bytes32` name to document metadata. +- Array/set for enumeration of active names. +- Index tracking to support O(1) removals from the enumeration set. + +## Security Considerations + +- Document URIs may reference mutable off-chain content. Consumers are strongly encouraged to verify content using the published `documentHash` and trusted retrieval channels. +- Implementations should protect `setDocument` and `removeDocument` with appropriate authorization, otherwise unauthorized actors can modify legal or operational references. +- Applications should treat event streams as advisory and reconcile against on-chain state when correctness is critical. +- Document names may not always fit cleanly into `bytes32`, especially for long legal titles. Implementations should avoid lossy truncation of human-readable names; using a deterministic hash-based identifier (for example, the document content hash or a hash of a canonical full title) as the `bytes32` name is a safer alternative. +- The custom errors `ERC1643InvalidName()` and `ERC1643MissingDocument()` are defined in this interface but were absent from the original [ERC-1643](./eip-1643.md) proposal text. Older implementations may not define these errors and may instead revert with strings or implementation-specific error patterns. Integrators should not assume all ERC-1643 contracts expose identical revert data. +- ERC-165 interface detection was also not part of the earlier ERC-1643 draft text. Older implementations may not expose `supportsInterface` for ERC-165 or `IERC1643`, so integrators should treat ERC-165 support as optional when interacting with legacy deployments. + +## Copyright + +Copyright and related rights waived via [CC0](../LICENSE.md). diff --git a/doc/README.md b/doc/README.md index 1cac701b..8ddfc975 100644 --- a/doc/README.md +++ b/doc/README.md @@ -105,7 +105,7 @@ The CMTAT was initially designed for the digitalization of company shares. For S #### Digitalization of structured products - In early 2024, [UBS](https://www.ubs.com/global/en/investment-bank/tokenize.html) executed a pilot transaction with OSL, a licensed professional investor in Hong Kong, to issue a tokenized warrant on Ethereum using the CMTAT smart contract framework. The tokenization arrangement for this warrant utilizes the CMTAT codebase to represent the warrant smart contract, which forms part of the tokenized register of holders. See [ubs.com - UBS expands its digital asset capabilities by launching Hong Kong’s first-ever tokenized warrant on the Ethereum network [ubs.com]](https://www.ubs.com/global/en/media/display-page-ndp/en-20240207-tokenized-warrant.html). -- [Credit Suisse, Pictet and Vontobel (2022)](https://cmta.ch/news-articles/trading-and-settlement-in-digital-securities) issued tokenized investment products that were traded on [BX Swiss](https://www.bxswiss.com/news/20221213-BX-Swiss-legt-das-technische-Fundament-fuer-die-Zukunft-des-regulierten-Handels-mit-tokenisierten-Wertpapieren) as part of a proof-of-concept leveraging the CMTAT. +- [Credit Suisse, Pictet and Vontobel (2022)](https://cmta.ch/news-articles/trading-and-settlement-in-digital-securities) issued tokenized investment products that were traded on [BX Swiss](https://www.bxswiss.com/news/20221213-BX-Swiss-legt-das-technische-Fundament-fuer-die-Zukunft-des-regulierten-Handels-mit-tokenisierten-Wertpapieren) as part of a proof-of-concept leveraging the CMTAT and another smart contract called [DvP contract](https://github.com/CMTA/DVP) that replicates the delivery-vs-payment functionality of traditional settlement systems. #### Digitalization of artwork @@ -1578,7 +1578,8 @@ Here is the list of the different versions available for each CMTAT version. | CMTAT version | RuleEngine | | ----------------------- | ------------------------------------------------------------ | -| CMTAT v3.0.0 | [RuleEngine v3.0.0-rc0](https://github.com/CMTA/RuleEngine/releases/tag/v3.0.0-rc0)
(unaudited) | +| CMTAT v3.3.0* | [RuleEngine v3.0.0-rc4](https://github.com/CMTA/RuleEngine/releases/tag/v3.0.0-rc4) (unaudited) | +| CMTAT v3.0.+ | [RuleEngine v3.0.0-rc0](https://github.com/CMTA/RuleEngine/releases/tag/v3.0.0-rc0)
(unaudited) | | CMTAT 2.5.0 (unaudited) | RuleEngine >= [v2.0.3](https://github.com/CMTA/RuleEngine/releases/tag/v2.0.3) (unaudited) | | CMTAT 2.4.0 (unaudited) | RuleEngine >=v2.0.0
Last version: [v2.0.2](https://github.com/CMTA/RuleEngine/releases/tag/v2.0.2)(unaudited) | | CMTAT 2.3.0 | [RuleEngine v1.0.2](https://github.com/CMTA/RuleEngine/releases/tag/v1.0.2) | @@ -1587,6 +1588,8 @@ Here is the list of the different versions available for each CMTAT version. This contract acts as a controller and can call different contract rules to apply rules on each transfer. +*CMTAT v3.3.0 transmits the minter and burn operators to the RuleEngine through the spender parameter. + ###### Rules Rules have their own dedicated repository: [github.com/CMTA/Rules](https://github.com/CMTA/Rules) and they can be used through a RuleEngine or directly with CMTAT. @@ -1640,7 +1643,7 @@ CMTA provides an implementation of a [SnapshotEngine](https://github.com/CMTA/Sn | CMTAT | SnapshotEngine | | -------------------------------- | ------------------------------------------------------------ | -| CMTAT v3.0.0 | [v0.3.0](https://github.com/CMTA/SnapshotEngine/releases/tag/v0.3.0)
(unaudited) | +| CMTAT v3.0.0+ | [v0.5.0](https://github.com/CMTA/SnapshotEngine/releases/tag/v0.5.0)
(unaudited) | | CMTAT v2.3.0 | SnapshotEngine v0.1.0 (unaudited) | | CMTAT v2.4.0, v2.5.0 (unaudited) | Include inside SnapshotModule (unaudited) | | CMTAT v2.3.0 | Include inside SnapshotModule (unaudited) | @@ -2801,6 +2804,7 @@ These contracts have now their own GitHub project: [CMTAT Factory](https://githu | CMTAT version | CMTAT Factory | | --------------------------------- | ------------------------------------------------------------ | +| CMTAT v3.3.0 | [CMTAT Factory v0.4.0](https://github.com/CMTA/CMTAT-Factory/releases/tag/v0.4.0) | | CMTAT v3.0.0 | CMTAT Factory [v0.2.0](https://github.com/CMTA/CMTATFactory/releases/tag/v0.2.0) (unaudited) | | CMTAT v2.5.0 / v2.5.1 (unaudited) | Available within CMTAT
see contracts/deployment
(unaudited) | | CMTAT 2.3.0 (audited) | Not available | @@ -2941,114 +2945,7 @@ A detailed maintainer feedback is available in [SequentReport-feedback.md](./sec ### Tools -> More details are available in the file [USAGE.md](./USAGE.md) - -#### [Aderyn](https://github.com/Cyfrin/aderyn) - -Here are the reports produced by [Aderyn](https://github.com/Cyfrin/aderyn): - -| Version | File | -| ------- | ------------------------------------------------------------ | -| v3.3.0 | [v3.3.0-aderyn-report.md](./security/tools/aderyn/v3.3.0-aderyn-report.md)
[v3.3.0-aderyn-feedback.md](./security/tools/aderyn/v3.3.0-aderyn-feedback.md) | -| v3.0.0 | [v3.0.0-aderyn-report.md](./security/tools/aderyn/archive/v3.0.0-aderyn-report.md) | - -Summary (v3.3.0): - -| Category | Tool Severity | Count | CMTAT Maintainer Assessment | Status | -| ------- | ------------- | ----- | --------------------------- | ------ | -| H-1..H-2 | High | 2 | Mixed (false positives + design choice) | Reviewed | -| L-1..L-10 | Low | 10 | Mixed (valid, design choices, style/tooling) | Reviewed | - -#### [Slither](https://github.com/crytic/slither) - -Here are the reports produced by [Slither](https://github.com/crytic/slither): - -| Version | File | -| ------- | ------------------------------------------------------------ | -| v3.3.0 | [v3.3.0-slither-report.md](./security/tools/slither/v3.3.0-slither-report.md)
[v3.3.0-slither-feedback.md](./security/tools/slither/v3.3.0-slither-feedback.md) | -| v3.0.0 | [v3.0.0-slither-report.md](./security/tools/slither/archive/v3.0.0-slither-report.md) | -| v2.3.0 | [v2.3.0-slither-report.md](./security/tools/slither/archive/v2.3.0-slither-report.md) | - -Summary (v3.3.0): - -| Detector | Tool Severity | Count | CMTAT Maintainer Assessment | Status | -| ------- | ------------- | ----- | --------------------------- | ------ | -| `uninitialized-local` | Medium | 1 | Under review (potential correctness) | Open | -| `calls-loop` | Low | 28 | Design choice / context dependent | Accepted | -| `assembly` | Informational | 13 | Expected pattern (ERC-7201-style slots) | Accepted | -| `dead-code` | Informational | 2 | Cleanup candidate, no direct security impact | Open | -| `naming-convention` | Informational | 56 | Style-only | Closed | -| `unindexed-event-address` | Informational | 1 | Minor optimization item | Accepted | - -#### [Mythril](https://github.com/Consensys/mythril) - -Here are the reports produced by Mythril - -| Version | File | -| ------- | ------------------------------------------------------------ | -| v3.0.0 | Mythril currently generates a fatal error, impossible to run the tool | -| v2.5.0 | [mythril-report-standalone.md](./security/tools/mythril/v2.5.0/myth_standalone_report.md)
[mythril-report-proxy.md](./security/tools/mythril/v2.5.0/myth_proxy_report.md)
| - -#### [Nethermind Audit Agent](https://auditagent.nethermind.io) - -Here are the reports produced by [Nethermind Audit Agent](https://auditagent.nethermind.io): - -| Version | File | -| ---------- | ------------------------------------------------------------ | -| v3.1.0 | [nethermind-audit-agent/v3.1.0](./security/tools/nethermind-audit-agent/v3.1.0) | -| v3.0.0-rc5 | [nethermind-audit-agent/v3.0.0-rc5](./security/tools/nethermind-audit-agent/v3.0.0-rc5) | - -The v3.1.0 report identified **14 findings** (2 high, 2 medium, 10 low). All findings were reviewed by CMTA maintainers; 7 were assessed as invalid and 7 were acknowledged as design choices. No finding required a code fix. - -| N° | Title | Severity | Validity | -|----|-------|----------|----------| -| 1 | Partial-freeze not enforced on transfer path | High | Invalid | -| 2 | Unprotected `initialize()` allows front-running of proxy initialization | High | Invalid | -| 3 | Missing spender validation in transfer check function | Medium | Design choice | -| 4 | Missing contract validation for RuleEngine address | Medium | Design choice | -| 5 | Transfers ignore pause/deactivation in `CMTATBaseCommon` | Low | Design choice | -| 6 | Transfers to frozen recipients possible in `CMTATBaseCommon.transfer()` | Low | Design choice | -| 7 | Reentrancy window between unfreeze and balance update | Low | Invalid | -| 8 | `canTransfer`/`canTransferFrom` can return `true` when transfer would revert | Low | Design choice | -| 9 | SnapshotEngine hook bypassed in `_update` | Low | Design choice | -| 10 | RuleEngine spender hardcoded to `address(0)` for minter-initiated transfers | Low | Invalid | -| 11 | Forced transfers still enforced by standard validation | Low | Invalid | -| 12 | Inconsistent deactivation handling between `canTransfer()` and `detectTransferRestriction()` | Low | Invalid | -| 13 | `approve` not protected by pause modifier | Low | Design choice | -| 14 | ERC2771 forwarder set via constructor in upgradeable deployments | Low | Invalid | - -A detailed response to each finding is available in [CMTAT_AuditAgent_Report_Comment_v3.1.0.md](./security/tools/nethermind-audit-agent/v3.1.0/CMTAT_AuditAgent_Report_Comment_v3.1.0.md). - -#### [Wake Arena](https://ackee.xyz) (Ackee Blockchain Security) - -Here are the reports produced by [Wake Arena](https://ackee.xyz), an automated AI vulnerability analysis tool developed by Ackee Blockchain Security: - -| Version | File | -| ------------ | ------------------------------------------------------------ | -| v3.2.0-rc2 | [Wake Arena Report - CMTA: CMTAT-v3.2.0-rc2](./security/tools/ackee-wake-arena/Wake Arena Report - CMTA_ CMTAT-v3.2.0-rc2.pdf) | - -> Ackee Blockchain Security, Wake Arena AI Report \| CMTA: CMTAT, February 10, 2026 12:24 UTC. - -The report (v3.2.0-rc2, February 10, 2026) identified **6 findings** (0 critical, 0 high, 3 medium, 2 low, 1 info): - -| ID | Title | Impact | Status | -|----|-------|--------|--------| -| M1 | Double invocation of compliance hook in `_minterTransferOverride` | Medium | Fixed | -| M2 | Double invocation of compliance hook in `_burnOverride` | Medium | Fixed | -| M3 | Double invocation of compliance hook in `_mintOverride` | Medium | Fixed | -| L1 | Misleading `Spend` event emitted on `transferFrom` when allowance is infinite | Low | Acknowledged (comment added) | -| L2 | Unmitigated ERC20 `approve` allowance change race condition | Low | Acknowledged – won't fix | -| I1 | Documentation mismatch: `_authorizeSelfBurn` comment referenced wrong role | Info | Fixed | - -A detailed feedback and response to each finding is available in [CMTAT-wake-arena-feedback.md](./security/tools/ackee-wake-arena/CMTAT-wake-arena-feedback.md). - -#### [Sequent](https://www.sequent.inc) (Pre-verification Review) - -Here are the reports produced by Sequent: - -| Version | File | -| ------- | ---- | -| v3.3.0-pre | [sequent-report-CMTAT.pdf](./security/pre-review/sequent-report-CMTAT.pdf)
[SequentReport-feedback.md](./security/pre-review/SequentReport-feedback.md) | +Reports and summaries for all static analysis and automated review tools ([Aderyn](https://github.com/Cyfrin/aderyn), [Slither](https://github.com/crytic/slither), Mythril, [Nethermind Audit Agent](https://auditagent.nethermind.io), Wake Arena) are collected in [security/AUDIT.md](./security/AUDIT.md). ### Test @@ -3204,6 +3101,10 @@ Specifications to deploy CMTAT-compliant tokens on Solana are available in the r A second unofficial version is available in the community section. +- [CMTAT-FIX](https://github.com/CMTA/CMTAT-FIX) + +Add FIX Asset Descriptors to CMTAT. See also [ERC-FIX](https://www.erc-fix.com/spec) + ### Unofficial Implementations #### Aztec (Noir) @@ -3224,19 +3125,17 @@ A version for [Starknet](https://www.starknet.io/) written in Cairo is under dev ### Community projects -- [swapnilraj - fix-engine](https://github.com/swapnilraj/CMTAT/tree/swp/fix-engine) - -Add FIX Asset Descriptors to CMTAT. See also [ERC-FIX](https://www.erc-fix.com/spec) - - [siva-sub - cmtat-icma-tokenized-bonds](https://github.com/siva-sub/cmtat-icma-tokenized-bonds) Integration of CMTATv3.0 framework with ICMA Bond Data Taxonomy v1.2 ### Guideline -If you create a version for another blockchain, feel free to use this summary tab to build a correspondence table between CMTAT framework, CMTAT Solidity version and your implementation. +If you create a version for another blockchain, you can find more details here: [CMTAT-equivalency-assessment](https://github.com/CMTA/CMTAT-equivalency-assessment) + +### CMTAT framework -#### CMTAT framework +This section presents the implementation details between CMTAT framework and the present implementation. In the below table, the CMTAT framework required features are mapped to Solidity features. diff --git a/doc/technical/erc-1450-improvement.md b/doc/technical/erc-1450-improvement.md new file mode 100644 index 00000000..3e254f1e --- /dev/null +++ b/doc/technical/erc-1450-improvement.md @@ -0,0 +1,198 @@ +# ERC-1450 — Possible Improvements & ERC-1643 Compatibility + +> Review of [`erc-1450.md`](./../ERCSpecification/erc-1450.md) (Last Call, `requires: 20, 165, 6093`). +> Companion to [`erc-1450-integration.md`](./erc-1450-integration.md). + +This document (1) collects suggested improvements to the ERC-1450 specification from a +CMTAT-implementer's perspective, and (2) answers whether ERC-1450 is compatible with the +**updated** ERC-1643 ([`update/erc-1643.md`](./../ERCSpecification/update/erc-1643.md)). + +--- + +## Part 1 — Suggested improvements + +### 1. The interface is monolithic — split it + +`IERC1450` bundles, in a single interface, everything: ERC-20, an ERC-1643 document subset, +ERC-1644 controller operations, a fee engine, a broker registry, a transfer-request state +machine, recovery workflow, per-lot regulation tracking, account freezing, and optional +EIP-3668 pre-checks. The spec *says* many parts are OPTIONAL, but they all live in one +`IERC1450` type, so `type(IERC1450).interfaceId` cannot distinguish a minimal implementation +from a full one. + +**Recommendation:** decompose into a required core plus discoverable extension interfaces, +each with its own ERC-165 id — e.g. `IERC1450Core`, `IERC1450Fees`, `IERC1450Broker`, +`IERC1450Requests`, `IERC1450Regulation`, and *reuse* `IERC1643` / `IERC1644` rather than +re-declaring them. This mirrors CMTAT's modular design and the way the updated ERC-1643 was +extracted into a standalone, independently-detectable interface. + +### 2. `mint` / `burnFrom` signatures collide with de-facto standards + +- `mint(address,uint256,uint16,uint256)` shadows the ubiquitous `mint(address,uint256)`; generic tooling/indexers that expect the 2-arg form will mis-decode. +- `burnFrom(address,uint256)` has the **exact selector** of OpenZeppelin `ERC20Burnable.burnFrom`, whose semantics are *allowance-based holder burn* — here it means *RTA strategic burn*. Same selector, opposite trust model. This is a real footgun for integrators. + +**Recommendation:** rename the regulation-aware entrypoints (`mintRegulated`, +`burnRegulated`) and, if a plain `mint`/`burnFrom` is kept, give it standard semantics. + +### 3. Three near-identical burn functions + +`burnFrom`, `burnFromRegulated`, and `burnFromRegulation` differ only in how a lot is +selected. This is confusing and error-prone (wrong overload = wrong lot burned). + +**Recommendation:** collapse to one `burnRegulated(from, amount, lotSelector)` where the +selector is a struct/enum (FIFO / LIFO / explicit `(regulationType, issuanceDate)`). + +### 4. On-chain lot tracking contradicts the off-chain-authority premise + +`getHolderRegulations` is specified as MUST-store-on-chain (spec line ~473), yet the same +document repeatedly states the RTA holds the *authoritative* cap table off-chain and that +on-chain document anchoring is "an optional transparency enhancement, not a compliance +requirement." Mandatory per-holder `TokenBatch[]` arrays are gas-heavy (unbounded array +growth, O(n) lot scans) and duplicate the RTA's own books. + +**Recommendation:** make the on-chain lot **store** OPTIONAL; require only the +`TokensMinted` / `RegulatedTransfer` / `TokensBurned` **events** (which already "allow +reconstruction of the entire cap table from events"). Implementers who want lot views can +index events off-chain, matching the ERC's own stated audit-trail model. + +### 5. Transfer-request lifecycle: idempotency & state-machine gaps + +- `requestTransferWithFee` returns a `requestId` but the spec never says **how it is generated**, while simultaneously demanding idempotency ("each requestId is unique and can only be executed ONCE"). Non-deterministic ids (counter) and caller-supplied retries can't both hold. +- `updateRequestStatus(requestId, newStatus)` lets the RTA set an arbitrary status; the "MUST validate state transitions" wording is not encoded in the type, so nothing prevents `Executed → Requested`. + +**Recommendation:** define `requestId = keccak256(from, to, amount, nonce, chainid)` (deterministic, replay-safe, cross-chain-unique), and specify the legal transition matrix explicitly (which transitions each function may perform). + +### 6. Reusing ERC-6093 errors with *changed meaning* is misleading + +The spec keeps `OwnableUnauthorizedAccount` / `OwnableInvalidOwner` "for tooling +compatibility" but redefines "Owner" to mean "Issuer" with inverted control (only the RTA +can change it). Tools that special-case Ownable errors will now be **actively wrong**. + +**Recommendation:** use dedicated `ERC1450NotRTA` / `ERC1450InvalidIssuer` errors. Borrowing +a name while inverting its semantics is worse than a fresh error. + +### 7. `allowance` MUST return 0 — but fees rely on allowance + +`allowance` is forced to `0` and `approve` reverts on the security token, yet +`requestTransferWithFee` requires the caller to have `approve`d the **fee token**. The +security-token `approve` being disabled is fine, but the spec should state loudly that all +fee approvals are on the *fee* ERC-20, and that hard-zeroing `allowance()` can break naïve +aggregators that probe allowance before any interaction. + +### 8. `preCheckCompliance` is `view` yet reverts with `OffchainLookup` *and* returns `bool` + +A function typed `returns (bool)` that is expected to `revert OffchainLookup(...)` has a +contradictory contract: callers can't tell if `false` means "non-compliant" or "call the +callback." EIP-3668 lookups also shouldn't be `view` in a way that implies a pure boolean. + +**Recommendation:** pick one shape — either a pure advisory `bool` view, or a CCIP-Read +function whose only success path is the callback. Don't overload both onto one signature. + +### 9. Interface-ID derivation is not reproducible + +The spec hard-codes `IERC1450_INTERFACE_ID = 0xaf175dee` but derives it from a truncated XOR +list ending in `/* ^ ... */`. No reader can independently recompute `0xaf175dee`. + +**Recommendation:** publish the complete, ordered selector list (or, better, split +interfaces per §1 so each id is small and verifiable). + +### 10. Fee custody lives inside the security token + +`withdrawFees` implies the security-token contract *holds* collected fee-token balances, +mixing fee treasury with the register of ownership. That enlarges the security token's +attack surface and complicates accounting/audits. + +**Recommendation:** route fees to a separate collector contract; keep the security token +free of unrelated ERC-20 custody. + +### 11. `batch*` operations are all-or-nothing with no result map + +`batchMint` / `batchTransferFrom` / `batchBurnFrom` revert entirely if any single item +fails. For large corporate actions (dividends over thousands of holders) one bad row aborts +the whole batch and wastes gas. + +**Recommendation:** offer a variant that returns a success bitmap / per-item status instead +of reverting, letting the RTA reconcile failures out-of-band. + +### 12. `regulationType` codes are non-normative → no cross-platform meaning + +`uint16` regulation codes are "suggested, implementations may vary," so `0x0006` means Reg CF +in one deployment and anything elsewhere. Any cross-issuer tooling that keys off +`regulationType` is unreliable. + +**Recommendation:** either normativize a registry (ideally an external, versioned registry +contract) or replace the raw code with a URI/DID pointer resolved off-chain. + +--- + +## Part 2 — ERC-1643 compatibility + +**Question:** Is ERC-1450 compatible with the *updated* ERC-1643 +([`update/erc-1643.md`](./../ERCSpecification/update/erc-1643.md))? + +**Answer: Yes — at the ABI/selector level the ERC-1450 document subset is a strict, wire-compatible subset of the updated ERC-1643.** A single contract can satisfy both, and CMTAT's existing `DocumentERC1643Module` already does. A handful of `SHOULD`-level semantics from the updated ERC-1643 are not mentioned by ERC-1450 and should be adopted for full conformance. + +### 2.1 Selector-level match (compatible) + +| Element | ERC-1450 (embedded) | Updated ERC-1643 | Compatible? | +|---|---|---|---| +| `setDocument(bytes32,string,bytes32)` | ✓ (`_name,_uri,_documentHash`) | ✓ (`name,uri,documentHash`) | **Yes** — param names don't affect the selector | +| `getDocument(bytes32)` → `(string,bytes32,uint256)` | returns `(documentUri, documentHash, timestamp)` | returns `(uri, documentHash, lastModified)` | **Yes** — identical selector *and* identical ABI return encoding | +| `removeDocument(bytes32)` | ✓ | ✓ | **Yes** | +| `getAllDocuments()` → `bytes32[]` | ✓ | ✓ | **Yes** | +| `event DocumentUpdated(bytes32 indexed,string,bytes32)` | ✓ | ✓ | **Yes** — identical topic0 | +| `event DocumentRemoved(bytes32 indexed,string,bytes32)` | ✓ | ✓ | **Yes** — identical topic0 | + +So an ERC-1450 implementation's document functions are indistinguishable, on the wire, from +an updated-ERC-1643 implementation. Note ERC-1450 exposes only the 4 core functions — it +does not add anything that *conflicts* with ERC-1643. + +### 2.2 CMTAT note — struct return is still ABI-identical + +CMTAT's `IERC1643.getDocument` returns a `Document` **struct** `(string uri, bytes32 +documentHash, uint256 lastModified)` rather than three separate values. In Solidity ABI +encoding a returned struct and three separate return values of the same types encode +**identically**, so CMTAT's document module is wire-compatible with *both* the updated +ERC-1643 tuple form and the ERC-1450 embedded form. No adapter needed. + +### 2.3 Gaps to close for *full* conformance (not incompatibilities) + +The updated ERC-1643 tightened several `SHOULD`/`MUST` semantics that ERC-1450 simply +doesn't mention. None conflict; ERC-1450 should adopt them: + +1. **Custom errors.** Updated ERC-1643 defines `ERC1643InvalidName()` (reject + `name == bytes32(0)` in `setDocument`) and `ERC1643MissingDocument()` (revert when + `removeDocument` targets a missing entry). ERC-1450 says nothing, so a naïve + implementation may accept the zero key or silently no-op on removal. +2. **`getDocument` MUST NOT revert on a missing entry** and MUST return empties + (`""`, `bytes32(0)`, `0`). ERC-1450 leaves this unspecified. +3. **ERC-165 detection.** Updated ERC-1643 says `supportsInterface` SHOULD return `true` + for `type(IERC1643).interfaceId`. ERC-1450's ERC-165 section only mandates the IERC1450 + id (and forbids the ERC-20 id) — it should *also* advertise `IERC1643` when the document + extension is implemented. This is additive and does not clash with the "MUST NOT return + true for ERC-20" rule. +4. **Timestamp semantics** already agree (last-modified, set on write); only the field name + differs (`timestamp` vs `lastModified`) — cosmetic. + +### 2.4 Recommendation + +Because the updated ERC-1643 is now a standalone, reusable interface, ERC-1450 should stop +**re-declaring** the document functions inline and instead: + +- add `1643` to its `requires:` list and `import`/inherit `IERC1643`, and +- adopt the ERC-1643 error set + no-revert-on-missing `getDocument` semantics + optional + `IERC1643` ERC-165 id. + +This removes drift risk (two copies of the same interface diverging over time) and lets +CMTAT — which already ships a conforming ERC-1643 module — back ERC-1450's document +requirement verbatim. + +### 2.5 Compatibility summary + +| Verdict | Detail | +|---|---| +| **ABI / selectors / events** | ✅ Fully compatible — ERC-1450 doc subset ⊆ updated ERC-1643 | +| **CMTAT `DocumentERC1643Module`** | ✅ Usable as-is (struct return is ABI-identical) | +| **Error semantics** | ⚠ ERC-1450 should adopt `ERC1643InvalidName` / `ERC1643MissingDocument` | +| **Missing-entry `getDocument`** | ⚠ ERC-1450 should specify no-revert + empty return | +| **ERC-165 `IERC1643` id** | ⚠ ERC-1450 should advertise it (additive; no conflict) | diff --git a/doc/technical/erc-1450-integration.md b/doc/technical/erc-1450-integration.md new file mode 100644 index 00000000..fdb3d481 --- /dev/null +++ b/doc/technical/erc-1450-integration.md @@ -0,0 +1,186 @@ +# ERC-1450 (RTA-Controlled Security Token) — CMTAT Integration + +> [ERC specification](./../ERCSpecification/erc-1450.md) — *RTA-Controlled Security Token* +> +> Status: Last Call (last-call-deadline 2026-07-14) · `requires: 20, 165, 6093` + +## Overview + +ERC-1450 describes a **US-securities-law** token model where a single **Registered +Transfer Agent (RTA)** holds *exclusive* authority over every token movement. Holders +can never move value themselves: `transfer()` and `approve()` always revert, and the +only value paths are RTA-executed `transferFromRegulated` / `controllerTransfer`, or a +holder/broker-*requested* transfer (`requestTransferWithFee`) that still needs RTA +execution. + +CMTAT and ERC-1450 target the same problem (compliant tokenized securities) from two +different philosophies: + +| Axis | ERC-1450 | CMTAT | +|---|---|---| +| Authority | Single **RTA** controls everything | **Role-based** (`MINTER_ROLE`, `BURNER_ROLE`, `ENFORCER_ROLE`, `DEFAULT_ADMIN_ROLE`, …) | +| Peer transfers | **Disabled** (`transfer`/`approve` revert) | **Allowed but gated** by `ValidationModule` / RuleEngine | +| Forced transfer | `controllerTransfer` (ERC-1644) | `forcedTransfer` (ERC-7943 / ERC-3643) in `ERC20EnforcementModule` | +| Compliance engine | Off-chain RTA + optional CCIP pre-check | On-chain RuleEngine / Allowlist | +| Documents | Embedded ERC-1643 subset | `DocumentERC1643Module` / `DocumentEngineModule` | +| Lot / regulation tracking | On-chain batches (`regulationType`, `issuanceDate`) | Not tracked on-chain | +| Fees / broker / request lifecycle | In-token | Absent | + +**Bottom line:** CMTAT already provides the *enforcement* half of ERC-1450 (forced +transfer, freezing, pause, documents, snapshots) as reusable modules. What ERC-1450 adds +on top — the *RTA-exclusive* transfer policy, the on-chain **transfer-request lifecycle**, +the **fee/broker** system, and **per-lot regulation tracking** — is new surface that must +be built as a dedicated module. This document proposes a concrete integration. + +--- + +## 1. Mapping ERC-1450 onto existing CMTAT modules + +Most of the ERC-1450 *enforcement* surface maps directly onto shipping CMTAT modules. + +| ERC-1450 requirement | CMTAT module / mechanism | Notes | +|---|---|---| +| `mint(to, amount, regulationType, issuanceDate)` | `ERC20MintModule` (`MINTER_ROLE`) | Base `mint`/`mintBatch` exist; regulation args need an extension (see §2). | +| `batchMint(...)` | `ERC20MintModule.mintBatch` | Same array semantics. | +| `burnFrom(from, amount)` | `ERC20BurnModule` (`BURNER_ROLE`) | ⚠ name collides with OZ `ERC20Burnable.burnFrom` (allowance-based) — see improvement doc. | +| `burnFromRegulated` / `burnFromRegulation` | `ERC20BurnModule` + regulation extension | Lot selection is new. | +| `transferFromRegulated(from,to,amount,…)` | `ERC20EnforcementModule.forcedTransfer` | RTA-executed transfer = CMTAT forced transfer. | +| `controllerTransfer(...)` (ERC-1644) | `ERC20EnforcementModule.forcedTransfer` | CMTAT emits ERC-7943 `ForcedTransfer`; add ERC-1644 `ControllerTransfer` alias if strict conformance is required. | +| `transfer` / `approve` MUST revert | `ValidationModule` / RuleEngine returning "no peer transfer" | CMTAT default *permits* gated transfers, so an RTA-controlled variant must additionally override `transfer`/`approve` to revert (see §3). | +| `setAccountFrozen` / `isAccountFrozen` | `EnforcementModule` (`ENFORCER_ROLE`) | Address-level freeze. | +| Partial token freeze | `ERC20EnforcementModule` (`setFrozenTokens` / `getFrozenTokens`) | Finer than ERC-1450 requires. | +| Regulatory halt | `PauseModule` (`PAUSER_ROLE`) | Global pause + optional deactivation. | +| `setDocument` / `getDocument` / `removeDocument` / `getAllDocuments` | `DocumentERC1643Module` (`DOCUMENT_ROLE`) **or** `DocumentEngineModule` (`DOCUMENT_ENGINE_ROLE`) | ERC-1643 — **ABI-compatible**, see §4 and the improvement doc. | +| Record dates / snapshots | `SnapshotEngineModule` (`SNAPSHOOTER_ROLE`) | Backs dividends, voting, quorum. | +| `changeIssuer` | RBAC admin transfer (`AccessControlDefaultAdminRules`) | "Issuer" ≈ CMTAT `DEFAULT_ADMIN_ROLE` holder. | +| `isKYCVerified` | RuleEngine / `ValidationModuleAllowlist` (`ALLOWLIST_ROLE`) | On-chain allowlist or external KYC oracle. | +| `version()` | `VersionModule` | Already present. | +| `isSecurityToken()` | new `pure` returning `true` | Trivial add. | +| Recovery workflow | `forcedTransfer` + `DocumentEngineModule` evidence anchoring | Structured/time-locked flow is new (see §2). | +| RTAProxy (REQUIRED multisig) | External Safe / multisig holding the CMTAT roles | CMTAT does not need a bespoke proxy; a Safe granted the admin + operational roles satisfies the "RTA multisig" requirement. | + +### Role model: how one "RTA" maps to CMTAT roles + +ERC-1450 concentrates authority in one RTA address (an `RTAProxy` multisig). In CMTAT the +equivalent is a **single multisig (Safe) granted the full operational role set**: + +``` +RTAProxy (Safe, M-of-N) ─ granted ─▶ DEFAULT_ADMIN_ROLE (changeIssuer, role admin) + MINTER_ROLE (mint / batchMint) + BURNER_ROLE (burnFrom*) + ENFORCER_ROLE (forcedTransfer, freeze) + PAUSER_ROLE (regulatory halt) + DOCUMENT_ROLE (ERC-1643 documents) + SNAPSHOOTER_ROLE (record dates) +``` + +This preserves ERC-1450's "one controller" semantics while keeping CMTAT's ability to +*delegate* individual roles later (e.g. hand `DOCUMENT_ROLE` to the issuer, which ERC-1450 +explicitly allows: "the issuer maintains rights to update corporate documents"). + +--- + +## 2. New surface CMTAT must add + +Four ERC-1450 capabilities have **no CMTAT equivalent** and would be delivered as one new +optional module (proposed: `ERC1450RequestModule`) plus a small mint/burn extension. Keep +module numbering consistent with the dependency ordering rule in `CLAUDE.md` — this module +depends on core + enforcement + document layers, so it sits above `ValidationModule`. + +1. **Transfer-request lifecycle** — `requestTransferWithFee`, `processTransferRequest`, + `rejectTransferRequest`, `updateRequestStatus`, `getRequestStatus`, the + `RequestStatus` state machine, and the `TransferRequested` / `RequestStatusChanged` / + `TransferExecuted` / `TransferRejected` / `TransferExpired` events. + *Execution* delegates to `ERC20EnforcementModule.forcedTransfer`. + +2. **Fee system** — `setFeeToken`, `setFeeParameters`, `getTransferFee`, `withdrawFees`, + `requestTransferWithPermit`. Fee token is an external ERC-20 (e.g. USDC); collection uses + `safeTransferFrom` on the *fee token* (not the security token). + +3. **Broker registry** — `setBrokerStatus`, `isRegisteredBroker`, gating who may call + `requestTransferWithFee` on behalf of a holder. Maps naturally to a new + `BROKER_ROLE` under CMTAT AccessControl. + +4. **Per-lot regulation tracking** — `getHolderRegulations`, `getRegulationSupply`, + `getDetailedBatchInfo`, plus the `regulationType` / `issuanceDate` parameters threaded + through mint/burn/transfer and the `TokensMinted` / `TokensBurned` / `RegulatedTransfer` + events. Storage pattern: `mapping(address => TokenBatch[])` as in the ERC's + non-normative guidance. + +> ⚠ On-chain lot tracking is heavyweight and, per the ERC itself, the RTA already keeps the +> authoritative cap table off-chain. Treat item (4) as **optional** — see the improvement +> doc for why it may be better emitted as events only rather than stored. + +--- + +## 3. Proposed deployment variant + +Rather than fork the core, add an RTA-controlled deployment variant that composes existing +modules and the new request module: + +``` +CMTATBaseCommon (ERC20 + Mint + Burn + ERC20Enforcement) + ↓ +CMTATBaseDocument (ERC-1643) + ↓ +CMTATBaseAccessControl (RBAC → the "RTA" role bundle) + ↓ +CMTATBaseRuleEngine (RuleEngine that REJECTS all peer transfers) + ↓ +ERC1450RequestModule (new) (request lifecycle + fee + broker + regulation) + ↓ +CMTATRTAControlled (deployment) → overrides transfer()/approve() to revert, + isSecurityToken()=true, + supportsInterface(IERC1450)=true +``` + +The **RTA-exclusive** property is achieved two ways in combination: +- A RuleEngine (or a dedicated `ValidationModuleRTA`) that denies every non-forced transfer, and +- Overriding `transfer` / `approve` / `transferFrom` to revert with `ERC1450TransferDisabled()`, so the token is behaviorally (not just policy-) locked. + +### ERC-165 note (behavioral, not just ABI) + +ERC-1450 requires `supportsInterface`: +- MUST return `true` for `0xaf175dee` (IERC1450) and `0x01ffc9a7` (ERC-165), +- **MUST return `false` for `0x36372b07` (ERC-20)** — because `transfer`/`approve` revert. + +This conflicts with a normal CMTAT deployment, which *is* behaviorally ERC-20. The +RTA-controlled variant must therefore deliberately **not** advertise the ERC-20 interface +ID. This is the single biggest semantic divergence and must be a conscious variant choice, +not a default. + +--- + +## 4. ERC-1643 documents — reuse CMTAT's module as-is + +ERC-1450 embeds an ERC-1643 document subset (`setDocument`, `getDocument`, +`removeDocument`, `getAllDocuments`, `DocumentUpdated`, `DocumentRemoved`). CMTAT's +`DocumentERC1643Module` already implements exactly this interface and is **ABI/selector +compatible** with the ERC-1450 subset (CMTAT returns a `Document` struct from +`getDocument`, which encodes identically to the ERC-1450 `(string, bytes32, uint256)` +tuple). So the CMTAT document module backs ERC-1450's document functions with **no change**. + +The full compatibility analysis (ERC-1450's document subset vs. the *updated* ERC-1643) is +in [`erc-1450-improvement.md`](./erc-1450-improvement.md#erc-1643-compatibility) — short +version: **compatible**, with a few `SHOULD`-level error/no-revert semantics to adopt. + +--- + +## 5. Integration checklist + +- [ ] Deploy an `RTAProxy` Safe; grant it the RTA role bundle (§1). +- [ ] Deploy `CMTATRTAControlled` variant with `transfer`/`approve` reverting. +- [ ] Configure a RuleEngine (or `ValidationModuleRTA`) that denies peer transfers. +- [ ] Wire `transferFromRegulated` / `controllerTransfer` → `forcedTransfer`. +- [ ] Add `ERC1450RequestModule` for request lifecycle + fee + broker (+ optional regulation lots). +- [ ] Reuse `DocumentERC1643Module` for documents; `SnapshotEngineModule` for record dates. +- [ ] Set `supportsInterface`: `true` for IERC1450, `false` for ERC-20. +- [ ] Map allowlist/KYC to `ValidationModuleAllowlist` or an external KYC RuleEngine. + +--- + +## Related documents + +- [`ruleengine-integration.md`](./ruleengine-integration.md) — validation / spender semantics +- [`erc-7943-uRWA-integration.md`](./erc-7943-uRWA-integration.md) — forced transfer & freezing +- [`erc-1450-improvement.md`](./erc-1450-improvement.md) — spec critique + ERC-1643 compatibility diff --git a/doc/technical/erc-7943-uRWA-integration.md b/doc/technical/erc-7943-uRWA-integration.md index 4ec80c06..88cdb8ef 100644 --- a/doc/technical/erc-7943-uRWA-integration.md +++ b/doc/technical/erc-7943-uRWA-integration.md @@ -87,25 +87,6 @@ transfer(to, 100) └─ Transfer executes ``` -## Operator-as-Spender Semantics (RuleEngine) - -In CMTAT RuleEngine-integrated deployments, the effective operator is propagated as `spender` in spender-aware compliance hooks. - -- `transferFrom`: spender is the delegated caller. -- `burnFrom` / `crosschainBurn`: spender is the operator (`_msgSender()`), with `to == address(0)`. -- `mint` / `crosschainMint`: spender is the operator (`_msgSender()`), with `from == address(0)`. -- `minterTransfer` (ERC-3643 `batchTransfer` path): spender is the operator (`_msgSender()`), with `from != address(0)` and `to != address(0)`. - -RuleEngine implementations should explicitly support these mint/burn operator cases where `operator == spender`. - -For `minterTransfer`, spender restrictions apply in the same way as standard operator transfer checks. RuleEngine logic must not classify this path as mint based on `from == address(0)`, because `minterTransfer` is a transfer path. - -`burnFrom` remains an access-controlled operation (role-gated) and is not modeled as a classic `transferFrom` compliance case. At hook level, `burn` and `burnFrom` share burn semantics (`to == address(0)`) and cannot be distinguished solely from `(spender, from, to, value)`. If policy needs to differentiate `burnFrom`, it should target the operator addresses that hold the role allowing `burnFrom`. - -If a RuleEngine policy is meant to apply only to traditional `transferFrom` spender restrictions, it should explicitly exclude these operator mint/burn paths: -- apply spender-only rule only when `from != address(0)` (exclude mint), -- and `to != address(0)` (exclude burn). - ## Error Semantics | Error | When emitted | @@ -137,6 +118,8 @@ The following function is not part of ERC-7943. It is included by CMTAT through |---|---|---|---| | `canTransferFrom(spender, from, to, value)` | ERC-7551 | Check if a delegated transfer is allowed | `ValidationModuleCore` | +For how `spender` is populated across all operation types (mint, burn, `transferFrom`, `minterTransfer`) when a RuleEngine is configured, see [ruleengine-integration.md — Spender / Operator Semantics](./ruleengine-integration.md#spender--operator-semantics). + ## ERC-165 CMTAT returns `true` for `supportsInterface(0x3edbb4c4)` (the fungible ERC-7943 interface ID). diff --git a/doc/technical/ruleengine-integration.md b/doc/technical/ruleengine-integration.md index 2aa27d29..b0544517 100644 --- a/doc/technical/ruleengine-integration.md +++ b/doc/technical/ruleengine-integration.md @@ -130,5 +130,5 @@ Use deployment summary in [doc/SUMMARY.md](../SUMMARY.md) and deployment tables ## Cross-References - Main RuleEngine section: [doc/README.md](../README.md) -- ERC-7943 operator semantics: [erc-7943-uRWA-integration.md](./erc-7943-uRWA-integration.md) +- ERC-7943 integration: [erc-7943-uRWA-integration.md](./erc-7943-uRWA-integration.md) - ERC-3643 mapping: [erc-3643-implementation.md](./erc-3643-implementation.md) diff --git a/doc/technical/snapshot.md b/doc/technical/snapshot.md index 972489d2..60df830d 100644 --- a/doc/technical/snapshot.md +++ b/doc/technical/snapshot.md @@ -58,8 +58,7 @@ It is also possible to extend CMTAT directly to include snapshot logic without a | CMTAT version | SnapshotEngine | |---|---| -| CMTAT v3.3.0 | Testing against v0.3.0-compatible engines in progress | -| CMTAT v3.0.0 | [v0.3.0](https://github.com/CMTA/SnapshotEngine/releases/tag/v0.3.0) (unaudited) | +| CMTAT v3.0.0+ | [v0.5.0](https://github.com/CMTA/SnapshotEngine/releases/tag/v0.5.0) (unaudited) | | CMTAT v2.3.0 | SnapshotEngine v0.1.0 (unaudited) | | CMTAT v2.4.0, v2.5.0 | Included inside SnapshotModule (unaudited) | | CMTAT v1.0.0 | Included inside SnapshotModule (audited) | diff --git a/hardhat.config.js b/hardhat.config.js index 71098ede..95eb7fa9 100644 --- a/hardhat.config.js +++ b/hardhat.config.js @@ -12,7 +12,14 @@ if (reportGas) { module.exports = { networks: { hardhat: { - blockGasLimit: 30000000 + blockGasLimit: 30000000, + // Test-only: relaxes the runtime EIP-170 (24 KB) deploy check so oversized + // test *mocks* (e.g. CMTATUpgradeableERC1363MsgDataMock, which extends the + // full ERC1363 token and adds a getMsgData() helper) can be deployed. + // Production contracts are still guarded: contractSizer.strict below throws + // at compile time for any non-Mock contract over the limit, and real + // networks enforce EIP-170 regardless of this flag. + allowUnlimitedContractSize: true } }, solidity: { diff --git a/test/common/AllowlistModuleCommon.js b/test/common/AllowlistModuleCommon.js index 4d885949..b780df06 100644 --- a/test/common/AllowlistModuleCommon.js +++ b/test/common/AllowlistModuleCommon.js @@ -1,14 +1,13 @@ const { ALLOWLIST_ROLE, ZERO_ADDRESS } = require('../utils') const { expect } = require('chai') -const REASON_FREEZE_STRING = 'testAllowlist' -const REASON_FREEZE_EVENT = ethers.toUtf8Bytes(REASON_FREEZE_STRING) -const reasonFreeze = ethers.Typed.bytes(REASON_FREEZE_EVENT) -const REASON_FREEZE_EMPTY = ethers.Typed.bytes(ethers.toUtf8Bytes('')) +const REASON_ALLOWLIST_STRING = 'testAllowlist' +const REASON_ALLOWLIST_EVENT = ethers.toUtf8Bytes(REASON_ALLOWLIST_STRING) +const reasonAllowlist = ethers.Typed.bytes(REASON_ALLOWLIST_EVENT) const REASON_STRING = 'testUnfreeze' -const REASON_UNFREEZE_EVENT = ethers.toUtf8Bytes(REASON_STRING) -const reasonUnfreeze = ethers.Typed.bytes(REASON_UNFREEZE_EVENT) +const REASON_UNALLOWLIST_EVENT = ethers.toUtf8Bytes(REASON_STRING) +const reasonUnallowlist = ethers.Typed.bytes(REASON_UNALLOWLIST_EVENT) const REASON_EMPTY = ethers.Typed.bytes(ethers.toUtf8Bytes('')) const REASON_EMPTY_EVENT = ethers.toUtf8Bytes('') function AllowlistModuleCommon () { @@ -39,10 +38,10 @@ function AllowlistModuleCommon () { // Act this.logs = await this.cmtat .connect(sender) - .setAddressAllowlist(this.address1, true, reasonFreeze) + .setAddressAllowlist(this.address1, true, reasonAllowlist) this.logs2 = await this.cmtat .connect(sender) - .setAddressAllowlist(this.address2, true, reasonFreeze) + .setAddressAllowlist(this.address2, true, reasonAllowlist) // Assert expect( @@ -56,13 +55,13 @@ function AllowlistModuleCommon () { expect(await this.cmtat.canReceive(this.address1)).to.equal(true) expect(await this.cmtat.canReceive(this.address2)).to.equal(true) - // emits a Freeze event + // emits an allowlist event await expect(this.logs) .to.emit(this.cmtat, 'AddressAddedToAllowlist') - .withArgs(this.address1, true, sender, REASON_FREEZE_EVENT) + .withArgs(this.address1, true, sender, REASON_ALLOWLIST_EVENT) await expect(this.logs2) .to.emit(this.cmtat, 'AddressAddedToAllowlist') - .withArgs(this.address2, true, sender, REASON_FREEZE_EVENT) + .withArgs(this.address2, true, sender, REASON_ALLOWLIST_EVENT) } async function testAllowlistBatch (sender) { @@ -123,7 +122,7 @@ function AllowlistModuleCommon () { const freeze = [true, true, false] // Arrange - testAllowlistBatch(sender) + await testAllowlistBatch.bind(this)(sender) // Act this.logs = await this.cmtat @@ -165,7 +164,7 @@ function AllowlistModuleCommon () { // Arrange await this.cmtat .connect(sender) - .setAddressAllowlist(this.address1, true, reasonFreeze) + .setAddressAllowlist(this.address1, true, reasonAllowlist) // Arrange - Assert expect(await this.cmtat.isAllowlisted(this.address1)).to.equal(true) @@ -176,7 +175,7 @@ function AllowlistModuleCommon () { // Act this.logs = await this.cmtat .connect(sender) - .setAddressAllowlist(this.address1, false, reasonUnfreeze) + .setAddressAllowlist(this.address1, false, reasonUnallowlist) // Assert expect(await this.cmtat.isAllowlisted(this.address1)).to.equal(false) @@ -188,19 +187,19 @@ function AllowlistModuleCommon () { await expect(this.logs) .to.emit(this.cmtat, 'AddressAddedToAllowlist') - .withArgs(this.address1, false, sender, REASON_UNFREEZE_EVENT) + .withArgs(this.address1, false, sender, REASON_UNALLOWLIST_EVENT) } beforeEach(async function () { await this.cmtat .connect(this.admin) - .setAddressAllowlist(this.address1, true, reasonUnfreeze) + .setAddressAllowlist(this.address1, true, reasonUnallowlist) await this.cmtat .connect(this.admin) - .setAddressAllowlist(ZERO_ADDRESS, true, reasonUnfreeze) + .setAddressAllowlist(ZERO_ADDRESS, true, reasonUnallowlist) await this.cmtat.connect(this.admin).mint(this.address1, 50) await this.cmtat .connect(this.admin) - .setAddressAllowlist(this.address1, false, reasonUnfreeze) + .setAddressAllowlist(this.address1, false, reasonUnallowlist) }) it('testAdminCanAllowlistAddress', async function () { @@ -241,7 +240,7 @@ function AllowlistModuleCommon () { .setAddressAllowlist(this.address1, true, REASON_EMPTY) // Assert expect(await this.cmtat.isAllowlisted(this.address1)).to.equal(true) - // emits a Freeze event + // emits an allowlist event await expect(this.logs) .to.emit(this.cmtat, 'AddressAddedToAllowlist') .withArgs(this.address1, true, this.admin, REASON_EMPTY_EVENT) @@ -303,7 +302,7 @@ function AllowlistModuleCommon () { await expect( this.cmtat .connect(this.address2) - .setAddressAllowlist(this.address1, true, reasonFreeze) + .setAddressAllowlist(this.address1, true, reasonAllowlist) ) .to.be.revertedWithCustomError( this.cmtat, @@ -346,16 +345,16 @@ function AllowlistModuleCommon () { .withArgs(this.address2.address, ALLOWLIST_ROLE) }) - it('testCannotNonEnforcerUnWhitelistAddress', async function () { + it('testCannotNonEnforcerRemoveFromAllowlistAddress', async function () { // Arrange await this.cmtat .connect(this.admin) - .setAddressAllowlist(this.address1, true, reasonFreeze) + .setAddressAllowlist(this.address1, true, reasonAllowlist) // Act await expect( this.cmtat .connect(this.address2) - .setAddressAllowlist(this.address1, false, reasonFreeze) + .setAddressAllowlist(this.address1, false, reasonAllowlist) ) .to.be.revertedWithCustomError( this.cmtat, @@ -376,10 +375,10 @@ function AllowlistModuleCommon () { // Act await this.cmtat .connect(this.admin) - .setAddressAllowlist(this.address2, true, reasonFreeze) + .setAddressAllowlist(this.address2, true, reasonAllowlist) await this.cmtat .connect(this.admin) - .setAddressAllowlist(this.address3, true, reasonFreeze) + .setAddressAllowlist(this.address3, true, reasonAllowlist) // Assert await expect( this.cmtat @@ -395,10 +394,10 @@ function AllowlistModuleCommon () { it('testCanTransferMatrixSenderListedReceiverListed', async function () { await this.cmtat .connect(this.admin) - .setAddressAllowlist(this.address1, true, reasonFreeze) + .setAddressAllowlist(this.address1, true, reasonAllowlist) await this.cmtat .connect(this.admin) - .setAddressAllowlist(this.address2, true, reasonFreeze) + .setAddressAllowlist(this.address2, true, reasonAllowlist) expect(await this.cmtat.canSend(this.address1)).to.equal(true) expect(await this.cmtat.canReceive(this.address2)).to.equal(true) @@ -408,10 +407,10 @@ function AllowlistModuleCommon () { it('testCanTransferMatrixSenderListedReceiverNotListed', async function () { await this.cmtat .connect(this.admin) - .setAddressAllowlist(this.address1, true, reasonFreeze) + .setAddressAllowlist(this.address1, true, reasonAllowlist) await this.cmtat .connect(this.admin) - .setAddressAllowlist(this.address2, false, reasonFreeze) + .setAddressAllowlist(this.address2, false, reasonAllowlist) expect(await this.cmtat.canSend(this.address1)).to.equal(true) expect(await this.cmtat.canReceive(this.address2)).to.equal(false) @@ -421,10 +420,10 @@ function AllowlistModuleCommon () { it('testCanTransferMatrixSenderNotListedReceiverListed', async function () { await this.cmtat .connect(this.admin) - .setAddressAllowlist(this.address1, false, reasonFreeze) + .setAddressAllowlist(this.address1, false, reasonAllowlist) await this.cmtat .connect(this.admin) - .setAddressAllowlist(this.address2, true, reasonFreeze) + .setAddressAllowlist(this.address2, true, reasonAllowlist) expect(await this.cmtat.canSend(this.address1)).to.equal(false) expect(await this.cmtat.canReceive(this.address2)).to.equal(true) @@ -434,10 +433,10 @@ function AllowlistModuleCommon () { it('testCanTransferMatrixSenderNotListedReceiverNotListed', async function () { await this.cmtat .connect(this.admin) - .setAddressAllowlist(this.address1, false, reasonFreeze) + .setAddressAllowlist(this.address1, false, reasonAllowlist) await this.cmtat .connect(this.admin) - .setAddressAllowlist(this.address2, false, reasonFreeze) + .setAddressAllowlist(this.address2, false, reasonAllowlist) expect(await this.cmtat.canSend(this.address1)).to.equal(false) expect(await this.cmtat.canReceive(this.address2)).to.equal(false) @@ -447,10 +446,10 @@ function AllowlistModuleCommon () { it('testZeroValueTransferRequiresSenderAndReceiverAllowlisted', async function () { await this.cmtat .connect(this.admin) - .setAddressAllowlist(this.address1, true, reasonFreeze) + .setAddressAllowlist(this.address1, true, reasonAllowlist) await this.cmtat .connect(this.admin) - .setAddressAllowlist(this.address2, false, reasonFreeze) + .setAddressAllowlist(this.address2, false, reasonAllowlist) expect(await this.cmtat.canTransfer(this.address1, this.address2, 0)).to.equal(false) await expect( @@ -460,7 +459,7 @@ function AllowlistModuleCommon () { await this.cmtat .connect(this.admin) - .setAddressAllowlist(this.address2, true, reasonFreeze) + .setAddressAllowlist(this.address2, true, reasonAllowlist) expect(await this.cmtat.canTransfer(this.address1, this.address2, 0)).to.equal(true) await expect( @@ -472,7 +471,7 @@ function AllowlistModuleCommon () { const AMOUNT_TO_APPROVE = 10n await this.cmtat .connect(this.admin) - .setAddressAllowlist(this.address1, false, reasonFreeze) + .setAddressAllowlist(this.address1, false, reasonAllowlist) await expect( this.cmtat @@ -487,10 +486,10 @@ function AllowlistModuleCommon () { const AMOUNT_TO_APPROVE = 10n await this.cmtat .connect(this.admin) - .setAddressAllowlist(this.address1, true, reasonFreeze) + .setAddressAllowlist(this.address1, true, reasonAllowlist) await this.cmtat .connect(this.admin) - .setAddressAllowlist(this.address2, false, reasonFreeze) + .setAddressAllowlist(this.address2, false, reasonAllowlist) await expect( this.cmtat @@ -505,7 +504,7 @@ function AllowlistModuleCommon () { MINT ////////////////////////////////////////////////////////////// */ - it('testCannotMintToNoWhitelistAddress', async function () { + it('testCannotMintToNoAllowlistAddress', async function () { const AMOUNT_TO_TRANSFER = 10 expect( await this.cmtat.canTransfer( @@ -522,7 +521,7 @@ function AllowlistModuleCommon () { .withArgs(this.address1.address) }) - it('testCannotBatchMintToNoWhitelistAddress', async function () { + it('testCannotBatchMintToNoAllowlistAddress', async function () { const AMOUNT_TO_TRANSFER = 10 const TOKEN_HOLDER = [this.address3, this.address1, this.address2] const TOKEN_SUPPLY_BY_HOLDERS = [10n, 100n, 1000n] @@ -555,8 +554,8 @@ function AllowlistModuleCommon () { await this.cmtat .connect(this.admin) - .setAddressAllowlist(this.address1, true, reasonFreeze) - this.cmtat.connect(this.admin).mint(this.address1, AMOUNT_TO_FREEZE) + .setAddressAllowlist(this.address1, true, reasonAllowlist) + await this.cmtat.connect(this.admin).mint(this.address1, AMOUNT_TO_FREEZE) this.logs = await this.cmtat .connect(this.admin) .freezePartialTokens(this.address1, AMOUNT_TO_FREEZE) @@ -566,7 +565,7 @@ function AllowlistModuleCommon () { // Remove from the allowlist because we test a force transfer await this.cmtat .connect(this.admin) - .setAddressAllowlist(this.address1, false, reasonFreeze) + .setAddressAllowlist(this.address1, false, reasonAllowlist) // Act this.logs = await this.cmtat .connect(this.admin) @@ -574,7 +573,7 @@ function AllowlistModuleCommon () { this.address1, this.address2, AMOUNT_TO_TRANSFER, - reasonFreeze + reasonAllowlist ) // Assert expect(await this.cmtat.getFrozenTokens(this.address1)).to.equal('5') @@ -587,51 +586,68 @@ function AllowlistModuleCommon () { /* ////////////////////////////////////////////////////////////// BURN ////////////////////////////////////////////////////////////// */ - it('testCanBatchBurnIfWhitelistedAddress', async function () { + it('testCanBatchBurnIfAllowlistedAddress', async function () { const TOKEN_BY_HOLDERS_TO_BURN = [10, 100, 1000] const TOKEN_HOLDER = [this.admin, this.address1, this.address2] await this.cmtat .connect(this.admin) - .setAddressAllowlist(this.address1, true, reasonFreeze) + .setAddressAllowlist(this.address1, true, reasonAllowlist) await this.cmtat .connect(this.admin) - .setAddressAllowlist(this.admin, true, reasonFreeze) + .setAddressAllowlist(this.admin, true, reasonAllowlist) await this.cmtat .connect(this.admin) - .setAddressAllowlist(this.address2, true, reasonFreeze) + .setAddressAllowlist(this.address2, true, reasonAllowlist) - this.cmtat + // Balances/supply before the mint+burn round-trip (asserting the delta + // rather than absolute 0 keeps this robust to any pre-existing balance) + const balancesBefore = [] + for (const holder of TOKEN_HOLDER) { + balancesBefore.push(await this.cmtat.balanceOf(holder)) + } + const supplyBefore = await this.cmtat.totalSupply() + + await this.cmtat .connect(this.admin) .batchMint(TOKEN_HOLDER, TOKEN_BY_HOLDERS_TO_BURN) // Act - this.cmtat + await this.cmtat .connect(this.admin) .batchBurn(TOKEN_HOLDER, TOKEN_BY_HOLDERS_TO_BURN) + + // Assert - burning the same amounts just minted returns every holder and + // the total supply to their starting values + for (let i = 0; i < TOKEN_HOLDER.length; ++i) { + expect(await this.cmtat.balanceOf(TOKEN_HOLDER[i])).to.equal( + balancesBefore[i] + ) + } + expect(await this.cmtat.totalSupply()).to.equal(supplyBefore) }) - it('testCannotBatchBurnFromNoWhitelistedAddress', async function () { + it('testCannotBatchBurnFromNoAllowlistedAddress', async function () { const TOKEN_BY_HOLDERS_TO_BURN = [10, 100, 1000] const TOKEN_HOLDER = [this.admin, this.address1, this.address2] await this.cmtat .connect(this.admin) - .setAddressAllowlist(this.address1, true, reasonFreeze) + .setAddressAllowlist(this.address1, true, reasonAllowlist) await this.cmtat .connect(this.admin) - .setAddressAllowlist(this.admin, true, reasonFreeze) + .setAddressAllowlist(this.admin, true, reasonAllowlist) await this.cmtat .connect(this.admin) - .setAddressAllowlist(this.address2, true, reasonFreeze) + .setAddressAllowlist(this.address2, true, reasonAllowlist) - this.cmtat + await this.cmtat .connect(this.admin) .batchMint(TOKEN_HOLDER, TOKEN_BY_HOLDERS_TO_BURN) // Remove from the allowlist await this.cmtat .connect(this.admin) - .setAddressAllowlist(this.address1, false, reasonFreeze) + .setAddressAllowlist(this.address1, false, reasonAllowlist) // Act await expect( @@ -645,12 +661,12 @@ function AllowlistModuleCommon () { ) }) - it('testCanBurnFromWhitelistedAddress', async function () { + it('testCanBurnFromAllowlistedAddress', async function () { const AMOUNT_TO_TRANSFER = 10 await this.cmtat .connect(this.admin) - .setAddressAllowlist(this.address1, true, reasonFreeze) - this.cmtat.connect(this.admin).mint(this.address1, AMOUNT_TO_TRANSFER) + .setAddressAllowlist(this.address1, true, reasonAllowlist) + await this.cmtat.connect(this.admin).mint(this.address1, AMOUNT_TO_TRANSFER) expect( await this.cmtat.canTransfer( this.address1, @@ -673,12 +689,12 @@ function AllowlistModuleCommon () { .burn(this.address1, AMOUNT_TO_TRANSFER) }) - it('testCannotBurnFromUnWhitelistedAddress', async function () { + it('testCannotBurnFromRemoveFromAllowlistedAddress', async function () { const AMOUNT_TO_TRANSFER = 10 await this.cmtat .connect(this.admin) - .setAddressAllowlist(this.address1, true, reasonFreeze) - this.cmtat.connect(this.admin).mint(this.address1, AMOUNT_TO_TRANSFER) + .setAddressAllowlist(this.address1, true, reasonAllowlist) + await this.cmtat.connect(this.admin).mint(this.address1, AMOUNT_TO_TRANSFER) expect( await this.cmtat.canTransfer( this.address1, @@ -690,7 +706,7 @@ function AllowlistModuleCommon () { // Remove from the whitelist await this.cmtat .connect(this.admin) - .setAddressAllowlist(this.address1, false, reasonFreeze) + .setAddressAllowlist(this.address1, false, reasonAllowlist) expect( await this.cmtat.canTransfer( @@ -712,7 +728,7 @@ function AllowlistModuleCommon () { Batch transfer ////////////////////////////////////////////////////////////// */ - it('testCannotBeBatchTransferIfToIsNotWhitelisted', async function () { + it('testCannotBeBatchTransferIfToIsNotAllowlisted', async function () { const accounts = [this.address1, this.address3, this.admin] const allowlist = [true, true, true] this.logs = await this.cmtat @@ -744,13 +760,13 @@ function AllowlistModuleCommon () { // Arrange await this.cmtat .connect(this.admin) - .setAddressAllowlist(this.address3, true, reasonFreeze) + .setAddressAllowlist(this.address3, true, reasonAllowlist) await this.cmtat .connect(this.admin) - .setAddressAllowlist(this.address2, true, reasonFreeze) + .setAddressAllowlist(this.address2, true, reasonAllowlist) await this.cmtat .connect(this.admin) - .setAddressAllowlist(this.address1, true, reasonFreeze) + .setAddressAllowlist(this.address1, true, reasonAllowlist) // Define allowance once owner and spender are allowlisted await this.cmtat .connect(this.address3) @@ -777,10 +793,10 @@ function AllowlistModuleCommon () { // Arrange await this.cmtat .connect(this.admin) - .setAddressAllowlist(this.address3, true, reasonFreeze) + .setAddressAllowlist(this.address3, true, reasonAllowlist) await this.cmtat .connect(this.admin) - .setAddressAllowlist(this.address1, true, reasonFreeze) + .setAddressAllowlist(this.address1, true, reasonAllowlist) // Define allowance once owner and spender are allowlisted await this.cmtat .connect(this.address3) @@ -803,20 +819,20 @@ function AllowlistModuleCommon () { // Arrange await this.cmtat .connect(this.admin) - .setAddressAllowlist(this.address3, true, reasonFreeze) + .setAddressAllowlist(this.address3, true, reasonAllowlist) await this.cmtat .connect(this.admin) - .setAddressAllowlist(this.address2, true, reasonFreeze) + .setAddressAllowlist(this.address2, true, reasonAllowlist) await this.cmtat .connect(this.admin) - .setAddressAllowlist(this.address1, true, reasonFreeze) + .setAddressAllowlist(this.address1, true, reasonAllowlist) // Define allowance while spender is still allowlisted await this.cmtat .connect(this.address3) .approve(this.address1, AMOUNT_TO_TRANSFER) await this.cmtat .connect(this.admin) - .setAddressAllowlist(this.address1, false, reasonFreeze) + .setAddressAllowlist(this.address1, false, reasonAllowlist) // Act expect( await this.cmtat.canTransferFrom( @@ -858,7 +874,7 @@ function AllowlistModuleCommon () { ) }) - it('testCannotBatchAllowlistIfAccountsSIsEmpty', async function () { + it('testCannotBatchAllowlistIfAccountsIsEmpty', async function () { const accounts = [] const allowlist = [false, false] await expect( diff --git a/test/common/AuthorizationModule/AuthorizationModuleCommon.js b/test/common/AuthorizationModule/AuthorizationModuleCommon.js index 37703008..283b48aa 100644 --- a/test/common/AuthorizationModule/AuthorizationModuleCommon.js +++ b/test/common/AuthorizationModule/AuthorizationModuleCommon.js @@ -2,7 +2,6 @@ const { expect } = require('chai') const { PAUSER_ROLE, DEFAULT_ADMIN_ROLE, - ZERO_ADDRESS } = require('../../utils') function AuthorizationModuleCommon () { context('Authorization', function () { diff --git a/test/common/CCIPModuleCommon.js b/test/common/CCIPModuleCommon.js index 50d270fa..a5753e17 100644 --- a/test/common/CCIPModuleCommon.js +++ b/test/common/CCIPModuleCommon.js @@ -1,6 +1,5 @@ const { expect } = require('chai') const { DEFAULT_ADMIN_ROLE, ZERO_ADDRESS } = require('../utils') -const { TERMS } = require('../deploymentUtils') function CCIPModuleCommon () { context('Set CCIP Admin', function () { @@ -40,7 +39,7 @@ function CCIPModuleCommon () { .withArgs(this.address1.address, DEFAULT_ADMIN_ROLE) }) - it('testCannotSetAdinWithSameValue>', async function () { + it('testCannotSetAdminWithSameValue', async function () { // Act await expect( this.cmtat.connect(this.admin).setCCIPAdmin(ZERO_ADDRESS) diff --git a/test/common/CMTATIntegrationCommon.js b/test/common/CMTATIntegrationCommon.js index 2422ec28..64293627 100644 --- a/test/common/CMTATIntegrationCommon.js +++ b/test/common/CMTATIntegrationCommon.js @@ -1,7 +1,8 @@ const { expect } = require('chai') const { ZERO_ADDRESS, IERC165_INTERFACEID, IERC721_INTERFACEID,IACCESSCONTROL_INTERFACEID, - IERC5679_INTERFACEID, IERC7943_INTERFACEID, ICMTATDEACTIVATE_INTERFACEID } = require('../utils') + IERC5679_INTERFACEID, IERC7943_INTERFACEID, ICMTATDEACTIVATE_INTERFACEID, + REJECTED_CODE_BASE_TRANSFER_REJECTED_DEACTIVATED } = require('../utils') const VALUE1 = 20n const VALUE2 = 50n function CMTATIntegrationCommon () { @@ -17,8 +18,6 @@ function CMTATIntegrationCommon () { expect(await this.cmtat.supportsInterface(IERC7943_INTERFACEID)).to.equal(true) expect(await this.cmtat.supportsInterface(ICMTATDEACTIVATE_INTERFACEID)).to.equal(true) expect(await this.cmtat.supportsInterface('0xffffffff')).to.equal(false) - - }) it('testCMTATIntegration', async function () { @@ -32,21 +31,6 @@ function CMTATIntegrationCommon () { .to.emit(this.cmtat, 'Name') .withArgs(NEW_NAME, NEW_NAME) - /* ============ Extra information =========== */ - it('testAdminCanUpdateInformation', async function () { - // Arrange - Assert - expect(await this.cmtat.information()).to.equal('CMTAT_info') - // Act - this.logs = await this.cmtat - .connect(this.admin) - .setInformation('new info available') - // Assert - expect(await this.cmtat.information()).to.equal('new info available') - await expect(this.logs) - .to.emit(this.cmtat, 'Information') - .withArgs('new info available') - }) - /* ============ ERC 7551 information =========== */ if (this.erc7551) { const NEW_METADATA = 'https://example.com/metadata2' @@ -101,111 +85,115 @@ function CMTATIntegrationCommon () { await expect(this.logs) .to.emit(this.cmtat, 'CrosschainMint') .withArgs(this.address1, VALUE2, this.admin) + }) - /* ============ Pause module ============ */ + /* ============ Extra information =========== */ + it('testAdminCanUpdateInformation', async function () { + // Arrange - Assert + expect(await this.cmtat.information()).to.equal('CMTAT_info') + // Act + this.logs = await this.cmtat + .connect(this.admin) + .setInformation('new info available') + // Assert + expect(await this.cmtat.information()).to.equal('new info available') + await expect(this.logs) + .to.emit(this.cmtat, 'Information') + .withArgs('new info available') + }) - it('testCanDeactivatedByAdminIfContractIsPaused', async function () { - const AMOUNT_TO_TRANSFER = 10n - // Arrange - await this.cmtat.connect(this.admin).pause() + /* ============ Pause module ============ */ + it('testCanDeactivatedByAdminIfContractIsPaused', async function () { + const AMOUNT_TO_TRANSFER = 10n + // Arrange + await this.cmtat.connect(this.admin).pause() - // Act - this.logs = await this.cmtat.connect(this.admin).deactivateContract() + // Act + this.logs = await this.cmtat.connect(this.admin).deactivateContract() + // Assert + expect(await this.cmtat.deactivated()).to.equal(true) + // Contract is in pause state + expect(await this.cmtat.paused()).to.equal(true) + // emits a Deactivated event + await expect(this.logs) + .to.emit(this.cmtat, 'Deactivated') + .withArgs(this.admin) + // Transfer is reverted because contract is in paused state + if (!this.generic) { + await expect( + this.cmtat + .connect(this.address1) + .transfer(this.address2, AMOUNT_TO_TRANSFER) + ) + .to.be.revertedWithCustomError(this.cmtat, 'EnforcedPause') + } + + if (!this.generic) { + expect( + await this.cmtat.canTransfer( + this.address1, + this.address2, + AMOUNT_TO_TRANSFER + ) + ).to.equal(false) + + expect( + await this.cmtat.canTransferFrom( + this.address3, + this.address1, + this.address2, + AMOUNT_TO_TRANSFER + ) + ).to.equal(false) + } + + if (!this.erc1404 && !this.generic) { // Assert - expect(await this.cmtat.deactivated()).to.equal(true) - // Contract is in pause state - expect(await this.cmtat.paused()).to.equal(true) - // emits a Deactivated event - await expect(this.logs) - .to.emit(this.cmtat, 'Deactivated') - .withArgs(this.admin) - // Transfer is reverted because contract is in paused state - if (!this.generic) { - await expect( - this.cmtat - .connect(this.address1) - .transfer(this.address2, AMOUNT_TO_TRANSFER) + expect( + await this.cmtat.detectTransferRestrictionFrom( + this.address3, + this.address1, + this.address2, + AMOUNT_TO_TRANSFER + ) + ).to.equal(REJECTED_CODE_BASE_TRANSFER_REJECTED_DEACTIVATED) + expect( + await this.cmtat.detectTransferRestriction( + this.address1, + this.address2, + AMOUNT_TO_TRANSFER + ) + ).to.equal(REJECTED_CODE_BASE_TRANSFER_REJECTED_DEACTIVATED) + // Assert + expect( + await this.cmtat.detectTransferRestrictionFrom( + this.address1, + this.address3, + this.address2, + AMOUNT_TO_TRANSFER ) - .to.be.revertedWithCustomError(this.cmtat, 'CMTAT_InvalidTransfer') - .withArgs( - this.address1.address, - this.address2.address, - AMOUNT_TO_TRANSFER - ) - } - - if (!this.generic) { - expect( - await this.cmtat.canTransfer( - this.address1, - this.address2, - AMOUNT_TO_TRANSFER - ) - ).to.equal(false) - - expect( - await this.cmtat.canTransferFrom( - this.address3, - this.address1, - this.address2, - AMOUNT_TO_TRANSFER - ) - ).to.equal(false) - } - - if (!this.erc1404 && !this.generic) { - // Assert - expect( - await this.cmtat.detectTransferRestrictionFrom( - this.address3, - this.address1, - this.address2, - AMOUNT_TO_TRANSFER - ) - ).to.equal(REJECTED_CODE_BASE_TRANSFER_REJECTED_DEACTIVATED) - expect( - await this.cmtat.detectTransferRestriction( - this.address1, - this.address2, - AMOUNT_TO_TRANSFER - ) - ).to.equal(REJECTED_CODE_BASE_TRANSFER_REJECTED_DEACTIVATED) - // Assert - expect( - await this.cmtat.detectTransferRestrictionFrom( - this.address1, - this.address3, - this.address2, - AMOUNT_TO_TRANSFER - ) - ).to.equal(REJECTED_CODE_BASE_TRANSFER_REJECTED_DEACTIVATED) - expect( - await this.cmtat.messageForTransferRestriction( - REJECTED_CODE_BASE_TRANSFER_REJECTED_DEACTIVATED - ) - ).to.equal('ContractDeactivated') - await expect( - this.cmtat - .connect(this.address3) - .transferFrom(this.address1, this.address2, AMOUNT_TO_TRANSFER) + ).to.equal(REJECTED_CODE_BASE_TRANSFER_REJECTED_DEACTIVATED) + expect( + await this.cmtat.messageForTransferRestriction( + REJECTED_CODE_BASE_TRANSFER_REJECTED_DEACTIVATED ) - .to.be.revertedWithCustomError(this.cmtat, 'CMTAT_InvalidTransfer') - .withArgs( - this.address1.address, - this.address2.address, - AMOUNT_TO_TRANSFER - ) - } - - // Unpause is reverted + ).to.equal('ContractDeactivated') await expect( - this.cmtat.connect(this.admin).unpause() - ).to.be.revertedWithCustomError( - this.cmtat, - 'CMTAT_PauseModule_ContractIsDeactivated' + this.cmtat + .connect(this.address3) + .transferFrom(this.address1, this.address2, AMOUNT_TO_TRANSFER) ) - }) + .to.be.revertedWithCustomError(this.cmtat, 'EnforcedPause') + } + + // Unpause is reverted + await expect( + this.cmtat.connect(this.admin).unpause() + ).to.be.revertedWithCustomError( + this.cmtat, + 'CMTAT_PauseModule_ContractIsDeactivated' + ) }) }) } diff --git a/test/common/DebtModule/DebtEngineModuleCommon.js b/test/common/DebtModule/DebtEngineModuleCommon.js index c372dd46..d50cba93 100644 --- a/test/common/DebtModule/DebtEngineModuleCommon.js +++ b/test/common/DebtModule/DebtEngineModuleCommon.js @@ -12,13 +12,13 @@ function DebtEngineModuleCommon () { .connect(this.admin) .setDebtEngine(this.debtEngineMock.target) } - debtIdentifier = { + const debtIdentifier = { issuerName: 'CMTA', issuerDescription: 'Capital Market', guarantor: 'Guarantor A', debtHolder: 'debtHolder A' } - debtInstrument = { + const debtInstrument = { interestRate: 500, // Example: 5.00% parValue: 1000000, // Example: 1,000,000 minimumDenomination: 200, @@ -46,11 +46,13 @@ function DebtEngineModuleCommon () { }) it('testCanReturnTheRightAddressIfSet', async function () { - if (this.definedAtDeployment) { - expect(this.debtEngineMock.target).to.equal( - await this.cmtat.debtEngine() - ) + // Only meaningful when the debt engine is wired at deployment + if (!this.definedAtDeployment) { + this.skip() } + expect(this.debtEngineMock.target).to.equal( + await this.cmtat.debtEngine() + ) }) it('testCanSetAndGetDebtCorrectly', async function () { diff --git a/test/common/DebtModule/DebtModuleCommon.js b/test/common/DebtModule/DebtModuleCommon.js index a60e0865..fb3ff414 100644 --- a/test/common/DebtModule/DebtModuleCommon.js +++ b/test/common/DebtModule/DebtModuleCommon.js @@ -1,17 +1,17 @@ const { expect } = require('chai') -const { ZERO_ADDRESS, DEBT_ROLE } = require('../../utils') +const { DEBT_ROLE } = require('../../utils') function DebtModuleCommon () { context('Debt Module test', function () { let debtBase, creditEvents beforeEach(async function () { - debtIdentifier = { + const debtIdentifier = { issuerName: 'CMTA', issuerDescription: 'Capital Market', guarantor: 'Guarantor A', debtHolder: 'debtHolder A' } - debtInstrument = { + const debtInstrument = { interestRate: 500, // Example: 5.00% parValue: 1000000, // Example: 1,000,000 minimumDenomination: 200, diff --git a/test/common/DocumentModule/DocumentModuleCommon.js b/test/common/DocumentModule/DocumentModuleCommon.js index 3e50204e..29fd2949 100644 --- a/test/common/DocumentModule/DocumentModuleCommon.js +++ b/test/common/DocumentModule/DocumentModuleCommon.js @@ -21,10 +21,13 @@ function DocumentModuleCommon () { } }) it('testCanReturnTheRightAddressIfSet', async function () { - if (this.cmtat.interface.hasFunction('documentEngine()') && this.definedAtDeployment) { - const documentEngine = await this.cmtat.documentEngine() - expect(this.documentEngineMock.target).to.equal(documentEngine) + // Only meaningful when the contract exposes documentEngine() and it is + // wired at deployment + if (!(this.cmtat.interface.hasFunction('documentEngine()') && this.definedAtDeployment)) { + this.skip() } + const documentEngine = await this.cmtat.documentEngine() + expect(this.documentEngineMock.target).to.equal(documentEngine) }) it('testCanSetAndGetADocument', async function () { const name = ethers.encodeBytes32String('doc1') diff --git a/test/common/ERC20BaseModuleCommon.js b/test/common/ERC20BaseModuleCommon.js index 767857ac..b3005743 100644 --- a/test/common/ERC20BaseModuleCommon.js +++ b/test/common/ERC20BaseModuleCommon.js @@ -1,9 +1,5 @@ const { expect } = require('chai') -const { ERC20ENFORCER_ROLE, DEFAULT_ADMIN_ROLE } = require('../utils') -const REASON_STRING = 'Bad guy' -const REASON_EVENT = ethers.toUtf8Bytes(REASON_STRING) -const REASON = ethers.Typed.bytes(REASON_EVENT) -const REASON_EMPTY = ethers.Typed.bytes(ethers.toUtf8Bytes('')) +const { DEFAULT_ADMIN_ROLE } = require('../utils') function ERC20BaseModuleCommon () { context('Token structure', function () { it('testHasTheDefinedName', async function () { @@ -82,7 +78,7 @@ function ERC20BaseModuleCommon () { .to.emit(this.cmtat, 'Symbol') .withArgs(NEW_SYMBOL, NEW_SYMBOL) }) - it('testCannotNonAdminUpdateName', async function () { + it('testCannotNonAdminUpdateSymbol', async function () { // Act await expect(this.cmtat.connect(this.address1).setSymbol('New Symbol')) .to.be.revertedWithCustomError( @@ -116,8 +112,10 @@ function ERC20BaseModuleCommon () { // Assert const ADDRESSES = [this.address1, this.address2, this.address3] let result = await this.cmtat.batchBalanceOf(ADDRESSES) + expect(result[0].length).to.equal(ADDRESSES.length) expect(result[0][0]).to.equal(TOKEN_AMOUNTS[0]) expect(result[0][1]).to.equal(TOKEN_AMOUNTS[1]) + expect(result[0][2]).to.equal(TOKEN_AMOUNTS[2]) expect(result[1]).to.equal(TOKEN_INITIAL_SUPPLY) const ADDRESSES2 = [] @@ -375,10 +373,14 @@ function ERC20BaseModuleCommon () { it('testTransferFromOneAccountToAnother', async function () { const AMOUNT_TO_TRANSFER = 11n - // Act - this.logs = await this.cmtat + // Arrange - address1 approves address3 to spend on its behalf + await this.cmtat .connect(this.address1) - .transfer(this.address2, AMOUNT_TO_TRANSFER) + .approve(this.address3, AMOUNT_TO_TRANSFER) + // Act - address3 moves address1's tokens to address2 via transferFrom + this.logs = await this.cmtat + .connect(this.address3) + .transferFrom(this.address1, this.address2, AMOUNT_TO_TRANSFER) // Assert expect(await this.cmtat.balanceOf(this.address1)).to.equal( TOKEN_AMOUNTS[0] - AMOUNT_TO_TRANSFER @@ -390,21 +392,29 @@ function ERC20BaseModuleCommon () { TOKEN_AMOUNTS[2] ) expect(await this.cmtat.totalSupply()).to.equal(TOKEN_INITIAL_SUPPLY) + // the spender's allowance is consumed + expect( + await this.cmtat.allowance(this.address1, this.address3) + ).to.equal(0n) // emits a Transfer event await expect(this.logs) .to.emit(this.cmtat, 'Transfer') .withArgs(this.address1, this.address2, AMOUNT_TO_TRANSFER) }) - // ADDRESS1 -> ADDRESS2 + // ADDRESS1 -> ADDRESS2 (via ADDRESS3 as spender) it('testCannotTransferMoreTokensThanOwn', async function () { const ADDRESS1_BALANCE = await this.cmtat.balanceOf(this.address1) const AMOUNT_TO_TRANSFER = 50n + // Arrange - approve enough so the balance check (not the allowance) is hit + await this.cmtat + .connect(this.address1) + .approve(this.address3, AMOUNT_TO_TRANSFER) // Act await expect( this.cmtat - .connect(this.address1) - .transfer(this.address2, AMOUNT_TO_TRANSFER) + .connect(this.address3) + .transferFrom(this.address1, this.address2, AMOUNT_TO_TRANSFER) ) .to.be.revertedWithCustomError(this.cmtat, 'ERC20InsufficientBalance') .withArgs(this.address1.address, ADDRESS1_BALANCE, AMOUNT_TO_TRANSFER) diff --git a/test/common/ERC20BurnModuleCommon.js b/test/common/ERC20BurnModuleCommon.js index 89c404cc..a9dacc15 100644 --- a/test/common/ERC20BurnModuleCommon.js +++ b/test/common/ERC20BurnModuleCommon.js @@ -1,11 +1,9 @@ const { BURNER_ROLE, - BURNER_FROM_ROLE, MINTER_ROLE, ZERO_ADDRESS } = require('../utils') const { expect } = require('chai') -// const REASON = 'BURN_TEST' const REASON_STRING = 'BURN_TEST' const REASON_EVENT = ethers.toUtf8Bytes(REASON_STRING) const REASON = ethers.Typed.bytes(REASON_EVENT) @@ -13,7 +11,6 @@ const REASON_EMPTY = ethers.Typed.bytes(ethers.toUtf8Bytes('')) function ERC20BurnModuleCommon () { context('burn', function () { const INITIAL_SUPPLY = 50n - const INITIAL_SUPPLY_TYPED = ethers.Typed.uint256(50) const VALUE1 = 20n const VALUE_TYPED = ethers.Typed.uint256(20) const DIFFERENCE = INITIAL_SUPPLY - VALUE1 @@ -185,7 +182,6 @@ function ERC20BurnModuleCommon () { .setAddressFrozen(this.address1, true) // Act - const VALUE = 20 const VALUE_TYPED = ethers.Typed.uint256(20) await expect( this.cmtat.connect(this.admin).burn(this.address1, VALUE_TYPED) @@ -196,7 +192,7 @@ function ERC20BurnModuleCommon () { it('testBurnPropagatesSpenderToRuleEngine', async function () { if (!this.cmtat.setRuleEngine) { - return + this.skip() } this.ruleEngineMock = await ethers.deployContract('RuleEngineMock', [this.admin]) @@ -213,7 +209,7 @@ function ERC20BurnModuleCommon () { it('testBurnWithRuleEngineAuthorizedSpenderCanBurn', async function () { if (!this.cmtat.setRuleEngine) { - return + this.skip() } this.ruleEngineMock = await ethers.deployContract('RuleEngineMock', [this.admin]) @@ -229,7 +225,6 @@ function ERC20BurnModuleCommon () { context('burnAndMint', function () { const INITIAL_SUPPLY = 50n - const VALUE1 = 20n const REASON_STRING_LOCAL = 'recovery' const REASON_EVENT_LOCAL = ethers.toUtf8Bytes(REASON_STRING_LOCAL) const REASON = ethers.Typed.bytes(REASON_EVENT_LOCAL) @@ -470,10 +465,10 @@ function ERC20BurnModuleCommon () { } beforeEach(async function () { - const TOKEN_HOLDER = [this.admin, this.address1, this.address2]; - ({ logs: this.logs1 } = await this.cmtat + const TOKEN_HOLDER = [this.admin, this.address1, this.address2] + await this.cmtat .connect(this.admin) - .batchMint(TOKEN_HOLDER, TOKEN_SUPPLY_BY_HOLDERS)) + .batchMint(TOKEN_HOLDER, TOKEN_SUPPLY_BY_HOLDERS) expect(await this.cmtat.totalSupply()).to.equal(INITIAL_SUPPLY) }) @@ -492,7 +487,6 @@ function ERC20BurnModuleCommon () { }) it('testCanBeBurntBatchByBurnerRoleWithoutReason', async function () { - const TOKEN_HOLDER = [this.admin, this.address1, this.address2] // Arrange await this.cmtat .connect(this.admin) @@ -504,7 +498,6 @@ function ERC20BurnModuleCommon () { }) it('testCanBeBurntBatchByBurnerRole', async function () { - const TOKEN_HOLDER = [this.admin, this.address1, this.address2] // Arrange await this.cmtat .connect(this.admin) @@ -624,7 +617,7 @@ function ERC20BurnModuleCommon () { const TOKEN_HOLDER = [this.admin, this.address1, this.address2] const TOKEN_SUPPLY_BY_HOLDERS = [10n, 100n, 1000n] - this.cmtat + await this.cmtat .connect(this.admin) .batchMint(TOKEN_HOLDER, TOKEN_SUPPLY_BY_HOLDERS) // Arrange @@ -643,7 +636,7 @@ function ERC20BurnModuleCommon () { const TOKEN_HOLDER = [this.address1, this.admin, this.address2] const TOKEN_SUPPLY_BY_HOLDERS = [10n, 100n, 1000n] - this.cmtat + await this.cmtat .connect(this.admin) .batchMint(TOKEN_HOLDER, TOKEN_SUPPLY_BY_HOLDERS) @@ -681,7 +674,7 @@ function ERC20BurnModuleCommon () { it('testBatchBurnPropagatesSpenderToRuleEngine', async function () { if (!this.cmtat.setRuleEngine) { - return + this.skip() } const TOKEN_HOLDER = [this.admin, this.address1, this.address2] @@ -700,7 +693,7 @@ function ERC20BurnModuleCommon () { it('testBatchBurnWithRuleEngineAuthorizedSpenderCanBurn', async function () { if (!this.cmtat.setRuleEngine) { - return + this.skip() } const TOKEN_HOLDER = [this.admin, this.address1, this.address2] diff --git a/test/common/ERC20CrossChainModuleCommon.js b/test/common/ERC20CrossChainModuleCommon.js index 62b9e7b2..96f6c00c 100644 --- a/test/common/ERC20CrossChainModuleCommon.js +++ b/test/common/ERC20CrossChainModuleCommon.js @@ -5,13 +5,6 @@ const { ZERO_ADDRESS } = require('../utils') const { expect } = require('chai') -// const REASON = 'BURN_TEST' -const REASON_STRING = 'CrosschainBurn' -const REASON_EVENT = ethers.toUtf8Bytes(REASON_STRING) -const REASON_MINT_EVENT = ethers.toUtf8Bytes('CrosschainMint') -const REASON = ethers.Typed.bytes(REASON_EVENT) -const REASON_EMPTY = ethers.Typed.bytes(ethers.toUtf8Bytes('')) -const REASON_EMPTY_EVENT = ethers.toUtf8Bytes('') function ERC20CrossChainModuleCommon () { context('CrosschainBurn', function () { const INITIAL_SUPPLY = 50 @@ -163,11 +156,9 @@ function ERC20CrossChainModuleCommon () { context('burn sender tokens', function () { const INITIAL_SUPPLY = 50n - const INITIAL_SUPPLY_TYPED = ethers.Typed.uint256(50) const VALUE1 = 20n const VALUE_TYPED = ethers.Typed.uint256(20) const DIFFERENCE = INITIAL_SUPPLY - VALUE1 - const DIFFERENCE_TYPED = ethers.Typed.uint256(30) async function testBurn (sender) { // Act // Burn 20 @@ -191,7 +182,9 @@ function ERC20CrossChainModuleCommon () { // Assert // Emits a Transfer event - await expect(this.logs).to.emit(this.cmtat, 'Transfer') + await expect(this.logs) + .to.emit(this.cmtat, 'Transfer') + .withArgs(sender, ZERO_ADDRESS, DIFFERENCE) // Emits a Burn event await expect(this.logs) .to.emit(this.cmtat, 'BurnFrom') @@ -227,14 +220,6 @@ function ERC20CrossChainModuleCommon () { 'AccessControlUnauthorizedAccount' ) .withArgs(this.address2.address, BURNER_SELF_ROLE) - - // Without reason - await expect(this.cmtat.connect(this.address2).burn(20n)) - .to.be.revertedWithCustomError( - this.cmtat, - 'AccessControlUnauthorizedAccount' - ) - .withArgs(this.address2.address, BURNER_SELF_ROLE) }) /* ////////////////////////////////////////////////////////////// @@ -258,7 +243,7 @@ function ERC20CrossChainModuleCommon () { COMPLIANCE ////////////////////////////////////////////////////////////// */ - it('testCannotBeMBurnIfContractIsDeactivated', async function () { + it('testCannotBeBurnIfContractIsDeactivated', async function () { // Arrange await this.cmtat.connect(this.admin).pause() await this.cmtat.connect(this.admin).deactivateContract() @@ -268,7 +253,7 @@ function ERC20CrossChainModuleCommon () { ).to.be.revertedWithCustomError(this.cmtat, 'EnforcedPause') }) - it('testCanBeBurnEvenIfContractIsPaused', async function () { + it('testCannotBeBurnIfContractIsPaused', async function () { await this.cmtat .connect(this.admin) .grantRole(BURNER_SELF_ROLE, this.address1) @@ -287,7 +272,6 @@ function ERC20CrossChainModuleCommon () { .connect(this.admin) .grantRole(BURNER_SELF_ROLE, this.address1) // Act - const VALUE = 20 const VALUE_TYPED = ethers.Typed.uint256(20) await expect(this.cmtat.connect(this.address1).burn(VALUE_TYPED)) .to.be.revertedWithCustomError(this.cmtat, 'ERC7943CannotSend') @@ -297,7 +281,6 @@ function ERC20CrossChainModuleCommon () { context('burnFrom', function () { const INITIAL_SUPPLY = 50n - const VALUE1 = 20n beforeEach(async function () { await this.cmtat.connect(this.admin).mint(this.address1, INITIAL_SUPPLY) @@ -397,7 +380,7 @@ function ERC20CrossChainModuleCommon () { it('testBurnFromPropagatesSpenderToRuleEngine', async function () { if (!this.cmtat.setRuleEngine) { - return + this.skip() } this.ruleEngineMock = await ethers.deployContract('RuleEngineMock', [this.admin]) @@ -415,7 +398,7 @@ function ERC20CrossChainModuleCommon () { it('testBurnFromWithRuleEngineAuthorizedSpenderCanBurn', async function () { if (!this.cmtat.setRuleEngine) { - return + this.skip() } this.ruleEngineMock = await ethers.deployContract('RuleEngineMock', [this.admin]) @@ -541,7 +524,7 @@ function ERC20CrossChainModuleCommon () { it('testCrosschainMintPropagatesSpenderToRuleEngine', async function () { if (!this.cmtat.setRuleEngine) { - return + this.skip() } this.ruleEngineMock = await ethers.deployContract('RuleEngineMock', [this.admin]) @@ -558,7 +541,7 @@ function ERC20CrossChainModuleCommon () { it('testCrosschainMintWithRuleEngineAuthorizedSpenderCanMint', async function () { if (!this.cmtat.setRuleEngine) { - return + this.skip() } this.ruleEngineMock = await ethers.deployContract('RuleEngineMock', [this.admin]) @@ -578,24 +561,7 @@ function ERC20CrossChainModuleCommon () { it('testCrosschainBurnWithRuleEngineUnauthorizedSpenderReverts', async function () { if (!this.cmtat.setRuleEngine) { - return - } - - this.ruleEngineMock = await ethers.deployContract('RuleEngineMock', [this.admin]) - await this.cmtat.connect(this.admin).setRuleEngine(this.ruleEngineMock) - await this.cmtat.connect(this.admin).grantRole(CROSS_CHAIN_ROLE, this.address2) - - await expect( - this.cmtat.connect(this.address2).crosschainBurn(this.address1, 10n) - ).to.be.revertedWithCustomError( - this.ruleEngineMock, - 'RuleEngine_InvalidTransfer' - ).withArgs(this.address1, ZERO_ADDRESS, 10n) - }) - - it('testCrosschainBurnPropagatesSpenderToRuleEngine', async function () { - if (!this.cmtat.setRuleEngine) { - return + this.skip() } this.ruleEngineMock = await ethers.deployContract('RuleEngineMock', [this.admin]) @@ -612,7 +578,7 @@ function ERC20CrossChainModuleCommon () { it('testCrosschainBurnWithRuleEngineAuthorizedSpenderCanBurn', async function () { if (!this.cmtat.setRuleEngine) { - return + this.skip() } this.ruleEngineMock = await ethers.deployContract('RuleEngineMock', [this.admin]) diff --git a/test/common/ERC20EnforcementModuleCommon.js b/test/common/ERC20EnforcementModuleCommon.js index 4b07ff7e..f39f4178 100644 --- a/test/common/ERC20EnforcementModuleCommon.js +++ b/test/common/ERC20EnforcementModuleCommon.js @@ -6,16 +6,8 @@ const { } = require('../utils') const { expect } = require('chai') -const REASON_FREEZE_STRING = 'testFreeze' -const REASON_FREEZE_EVENT = ethers.toUtf8Bytes(REASON_FREEZE_STRING) -const reasonFreeze = ethers.Typed.bytes(REASON_FREEZE_EVENT) -const REASON_FREEZE_EMPTY = ethers.Typed.bytes(ethers.toUtf8Bytes('')) const REASON_STRING = 'testUnfreeze' -const REASON_UNFREEZE_EVENT = ethers.toUtf8Bytes(REASON_STRING) -const reasonUnfreeze = ethers.Typed.bytes(REASON_UNFREEZE_EVENT) -const REASON_EMPTY = ethers.Typed.bytes(ethers.toUtf8Bytes('')) -const REASON_EMPTY_EVENT = ethers.toUtf8Bytes('') const REASON_EVENT = ethers.toUtf8Bytes(REASON_STRING) const REASON = ethers.Typed.bytes(REASON_EVENT) @@ -25,15 +17,6 @@ const UNFREEZE_AMOUNT = 10 const INITIAL_BALANCE = 50 -function hasFunction (contract, signature) { - try { - contract.interface.getFunction(signature) - return true - } catch { - return false - } -} - function supportsReasonedEnforcement (ctx) { return !!ctx.erc7551 } @@ -1345,83 +1328,6 @@ function ERC20EnforcementModuleCommon () { this.address1, AMOUNT_TO_TRANSFER, INITIAL_BALANCE - FREEZE_AMOUNT ) }) - - it('testCannotTransferFromTokenIfActiveBalanceIsNotEnough', async function () { - const AMOUNT_TO_TRANSFER = INITIAL_BALANCE - FREEZE_AMOUNT + 1 - // Arrange - // Define allowance - await this.cmtat - .connect(this.address1) - .approve(this.address3, AMOUNT_TO_TRANSFER) - // Act - await this.cmtat - .connect(this.admin) - .freezePartialTokens(this.address1, FREEZE_AMOUNT) - - // Assert - expect( - await this.cmtat.canTransfer( - this.address1, - this.address2, - AMOUNT_TO_TRANSFER - ) - ).to.equal(false) - - expect( - await this.cmtat.canTransferFrom( - this.address3, - this.address1, - this.address2, - AMOUNT_TO_TRANSFER - ) - ).to.equal(false) - - if (!this.erc1404) { - expect( - await this.cmtat.detectTransferRestriction( - this.address1, - this.address2, - AMOUNT_TO_TRANSFER - ) - ).to.equal( - REJECTED_CODE_BASE_TRANSFER_REJECTED_FROM_INSUFFICIENT_ACTIVE_BALANCE - ) - expect( - await this.cmtat.detectTransferRestrictionFrom( - this.address3, - this.address1, - this.address2, - AMOUNT_TO_TRANSFER - ) - ).to.equal( - REJECTED_CODE_BASE_TRANSFER_REJECTED_FROM_INSUFFICIENT_ACTIVE_BALANCE - ) - expect( - await this.cmtat.messageForTransferRestriction( - REJECTED_CODE_BASE_TRANSFER_REJECTED_FROM_INSUFFICIENT_ACTIVE_BALANCE - ) - ).to.equal('AddrFrom:insufficientActiveBalance') - expect( - await this.cmtat.detectTransferRestrictionFrom( - this.address3, - this.address1, - this.address2, - AMOUNT_TO_TRANSFER - ) - ).to.equal( - REJECTED_CODE_BASE_TRANSFER_REJECTED_FROM_INSUFFICIENT_ACTIVE_BALANCE - ) - } - - await expect( - this.cmtat - .connect(this.address3) - .transferFrom(this.address1, this.address2, AMOUNT_TO_TRANSFER) - ).to.be.revertedWithCustomError( - this.cmtat, 'ERC7943InsufficientUnfrozenBalance').withArgs( - this.address1, AMOUNT_TO_TRANSFER, INITIAL_BALANCE - FREEZE_AMOUNT - ) - }) }) context('Set Address Frozen', function () { @@ -1643,84 +1549,6 @@ function ERC20EnforcementModuleCommon () { this.address1, AMOUNT_TO_TRANSFER, INITIAL_BALANCE - FREEZE_AMOUNT ) }) - - it('testCannotTransferFromTokenIfActiveBalanceIsNotEnough', async function () { - const AMOUNT_TO_TRANSFER = INITIAL_BALANCE - FREEZE_AMOUNT + 1 - // Arrange - // Define allowance - await this.cmtat - .connect(this.address1) - .approve(this.address3, AMOUNT_TO_TRANSFER) - // Act - await this.cmtat - .connect(this.admin) - .setFrozenTokens(this.address1, FREEZE_AMOUNT) - - // Assert - expect( - await this.cmtat.canTransfer( - this.address1, - this.address2, - AMOUNT_TO_TRANSFER - ) - ).to.equal(false) - - expect( - await this.cmtat.canTransferFrom( - this.address3, - this.address1, - this.address2, - AMOUNT_TO_TRANSFER - ) - ).to.equal(false) - - if (!this.erc1404) { - expect( - await this.cmtat.detectTransferRestriction( - this.address1, - this.address2, - AMOUNT_TO_TRANSFER - ) - ).to.equal( - REJECTED_CODE_BASE_TRANSFER_REJECTED_FROM_INSUFFICIENT_ACTIVE_BALANCE - ) - expect( - await this.cmtat.detectTransferRestrictionFrom( - this.address3, - this.address1, - this.address2, - AMOUNT_TO_TRANSFER - ) - ).to.equal( - REJECTED_CODE_BASE_TRANSFER_REJECTED_FROM_INSUFFICIENT_ACTIVE_BALANCE - ) - expect( - await this.cmtat.messageForTransferRestriction( - REJECTED_CODE_BASE_TRANSFER_REJECTED_FROM_INSUFFICIENT_ACTIVE_BALANCE - ) - ).to.equal('AddrFrom:insufficientActiveBalance') - expect( - await this.cmtat.detectTransferRestrictionFrom( - this.address3, - this.address1, - this.address2, - AMOUNT_TO_TRANSFER - ) - ).to.equal( - REJECTED_CODE_BASE_TRANSFER_REJECTED_FROM_INSUFFICIENT_ACTIVE_BALANCE - ) - } - - await expect( - this.cmtat - .connect(this.address3) - .transferFrom(this.address1, this.address2, AMOUNT_TO_TRANSFER) - ).to.be.revertedWithCustomError( - this.cmtat, - 'ERC7943InsufficientUnfrozenBalance').withArgs( - this.address1, AMOUNT_TO_TRANSFER, INITIAL_BALANCE - FREEZE_AMOUNT - ) - }) }) } module.exports = ERC20EnforcementModuleCommon diff --git a/test/common/ERC20MintModuleCommon.js b/test/common/ERC20MintModuleCommon.js index f3554f27..82edde61 100644 --- a/test/common/ERC20MintModuleCommon.js +++ b/test/common/ERC20MintModuleCommon.js @@ -6,7 +6,6 @@ const VALUE2 = 50n const REASON_STRING = 'MINT_TEST' const REASON_EVENT = ethers.toUtf8Bytes(REASON_STRING) const REASON = ethers.Typed.bytes(REASON_EVENT) -const REASON_EMPTY = ethers.Typed.bytes(ethers.toUtf8Bytes('')) function ERC20MintModuleCommon () { context('Minting', function () { async function testMint (sender) { @@ -179,7 +178,7 @@ function ERC20MintModuleCommon () { it('testMintPropagatesSpenderToRuleEngine', async function () { if (!this.cmtat.setRuleEngine) { - return + this.skip() } this.ruleEngineMock = await ethers.deployContract('RuleEngineMock', [this.admin]) @@ -196,7 +195,7 @@ function ERC20MintModuleCommon () { it('testMintWithRuleEngineAuthorizedSpenderCanMint', async function () { if (!this.cmtat.setRuleEngine) { - return + this.skip() } this.ruleEngineMock = await ethers.deployContract('RuleEngineMock', [this.admin]) @@ -263,7 +262,7 @@ function ERC20MintModuleCommon () { await bindTest(this.admin) }) - it('testCanBeMintBatchdByANewMinter', async function () { + it('testCanBeMintBatchedByANewMinter', async function () { // Arrange await this.cmtat .connect(this.admin) @@ -274,7 +273,7 @@ function ERC20MintModuleCommon () { it('testBatchMintPropagatesSpenderToRuleEngine', async function () { if (!this.cmtat.setRuleEngine) { - return + this.skip() } const TOKEN_HOLDER = [this.admin, this.address1, this.address2] @@ -294,7 +293,7 @@ function ERC20MintModuleCommon () { it('testBatchMintWithRuleEngineAuthorizedSpenderCanMint', async function () { if (!this.cmtat.setRuleEngine) { - return + this.skip() } const TOKEN_HOLDER = [this.admin, this.address1, this.address2] @@ -366,7 +365,7 @@ function ERC20MintModuleCommon () { ) }) - it('testCannotbatchMintIfTOSIsEmpty', async function () { + it('testCannotBatchMintIfTOSIsEmpty', async function () { const TOKEN_HOLDER_INVALID = [] const TOKEN_SUPPLY_BY_HOLDERS = [] await expect( @@ -454,7 +453,7 @@ function ERC20MintModuleCommon () { }) // ADDRESS1 -> ADDRESS2 - it('testCannotbatchTransferMoreTokensThanOwn', async function () { + it('testCannotBatchTransferMoreTokensThanOwn', async function () { const TOKEN_ADDRESS_TOS = [this.address1, this.address2, this.address3] const BALANCE_AFTER_FIRST_TRANSFER = (await this.cmtat.balanceOf(this.admin)) - TOKEN_AMOUNTS[0] @@ -514,7 +513,7 @@ function ERC20MintModuleCommon () { ) }) - it('testCannotbatchTransferIfTOSIsEmpty', async function () { + it('testCannotBatchTransferIfTOSIsEmpty', async function () { const TOKEN_ADDRESS_TOS_INVALID = [] await expect( this.cmtat @@ -587,7 +586,7 @@ function ERC20MintModuleCommon () { it('testBatchTransferPropagatesSpenderToRuleEngine', async function () { if (!this.cmtat.setRuleEngine) { - return + this.skip() } const TOKEN_ADDRESS_TOS = [this.address1, this.address2, this.address3] @@ -604,7 +603,7 @@ function ERC20MintModuleCommon () { it('testBatchTransferWithRuleEngineAuthorizedSpenderCanTransfer', async function () { if (!this.cmtat.setRuleEngine) { - return + this.skip() } const TOKEN_ADDRESS_TOS = [this.address1, this.address2, this.address3] diff --git a/test/common/ERC7551ModuleCommon.js b/test/common/ERC7551ModuleCommon.js index 705fc52d..35e33eec 100644 --- a/test/common/ERC7551ModuleCommon.js +++ b/test/common/ERC7551ModuleCommon.js @@ -1,5 +1,5 @@ const { expect } = require('chai') -const { DEFAULT_ADMIN_ROLE, EXTRA_INFORMATION_ROLE } = require('../utils') +const { EXTRA_INFORMATION_ROLE } = require('../utils') const { TERMS } = require('../deploymentUtils') function ERC7551ModuleCommon () { @@ -44,7 +44,7 @@ function ERC7551ModuleCommon () { }) it('testCannotNonAdminUpdateTerms', async function () { // Arrange - Assert - checkTerms(TERMS) + await checkTerms(this, TERMS) // Act await expect( this.cmtat diff --git a/test/common/EnforcementModuleCommon.js b/test/common/EnforcementModuleCommon.js index 659ab80c..dbdb5026 100644 --- a/test/common/EnforcementModuleCommon.js +++ b/test/common/EnforcementModuleCommon.js @@ -10,7 +10,6 @@ const { expect } = require('chai') const REASON_FREEZE_STRING = 'testFreeze' const REASON_FREEZE_EVENT = ethers.toUtf8Bytes(REASON_FREEZE_STRING) const reasonFreeze = ethers.Typed.bytes(REASON_FREEZE_EVENT) -const REASON_FREEZE_EMPTY = ethers.Typed.bytes(ethers.toUtf8Bytes('')) const REASON_STRING = 'testUnfreeze' const REASON_UNFREEZE_EVENT = ethers.toUtf8Bytes(REASON_STRING) @@ -83,7 +82,7 @@ function EnforcementModuleCommon () { const freeze = [false, false, true] // Arrange - testFreezeBatch(sender) + await testFreezeBatch.bind(this)(sender) // Act this.logs = await this.cmtat @@ -429,7 +428,7 @@ function EnforcementModuleCommon () { ) }) - it('testCannotBatchFrozenIfAccountsSIsEmpty', async function () { + it('testCannotBatchFrozenIfAccountsIsEmpty', async function () { const accounts = [] const freeze = [false, false] await expect( diff --git a/test/common/ExtraInfoModuleCommon.js b/test/common/ExtraInfoModuleCommon.js index 54215734..931a5786 100644 --- a/test/common/ExtraInfoModuleCommon.js +++ b/test/common/ExtraInfoModuleCommon.js @@ -1,5 +1,5 @@ const { expect } = require('chai') -const { DEFAULT_ADMIN_ROLE, EXTRA_INFORMATION_ROLE } = require('../utils') +const { EXTRA_INFORMATION_ROLE } = require('../utils') const { TERMS } = require('../deploymentUtils') function ExtraInfoModuleCommon () { @@ -61,7 +61,6 @@ function ExtraInfoModuleCommon () { this.logs = await this.cmtat.connect(this.admin).setTerms(NEW_TERMS) // Assert - // let blockTimestamp = (await ethers.provider.getBlock('latest')).timestamp; await checkTerms(this, NEW_TERMS) const blockTimestamp = ( await ethers.provider.getBlock(this.logs.blockNumber) @@ -76,7 +75,7 @@ function ExtraInfoModuleCommon () { }) it('testCannotNonAdminUpdateTerms', async function () { // Arrange - Assert - checkTerms(TERMS) + await checkTerms(this, TERMS) // Act await expect(this.cmtat.connect(this.address1).setTerms(TERMS)) .to.be.revertedWithCustomError( @@ -84,8 +83,13 @@ function ExtraInfoModuleCommon () { 'AccessControlUnauthorizedAccount' ) .withArgs(this.address1.address, EXTRA_INFORMATION_ROLE) - // Assert - checkTerms(TERMS) + // Assert - terms are unchanged after the failed update. + // Assert content only (not checkTerms): the reverted setTerms mines a + // block, so checkTerms' `storedTimestamp == block.timestamp` sub-check + // would spuriously fail even though the terms themselves did not change. + const termsAfter = await this.cmtat.terms() + expect(termsAfter[0]).to.equal(TERMS[0]) + expect(termsAfter[1][0]).to.equal(TERMS[1]) }) it('testAdminCanUpdateInformation', async function () { // Arrange - Assert diff --git a/test/common/MetaTxModuleCommon.js b/test/common/MetaTxModuleCommon.js index 2edf9027..eb42ec63 100644 --- a/test/common/MetaTxModuleCommon.js +++ b/test/common/MetaTxModuleCommon.js @@ -1,10 +1,8 @@ const helpers = require('@nomicfoundation/hardhat-network-helpers') const { getDomain, - ForwardRequest } = require('../../lib/openzeppelin-contracts-upgradeable/test/helpers/eip712') const { expect } = require('chai') -const { waffle } = require('hardhat') function MetaTxModuleCommon () { context('Transferring without paying gas', function () { const AMOUNT_TO_TRANSFER = 11n @@ -46,7 +44,7 @@ function MetaTxModuleCommon () { data: this.data, gas: 100000n, deadline: (await helpers.time.latest()) + 60, - nonce: await this.forwarder.nonces(this.address1), + nonce: await this.forwarder.nonces(await signer.getAddress()), ...override } req.signature = await signer.signTypedData( @@ -66,9 +64,7 @@ function MetaTxModuleCommon () { }) it('can send a transfer transaction without paying gas', async function () { - // const provider = await ethers.getDefaultProvider() - // getDefaultProvider uses Infura and Alchemy instead of Hardhat - [signer] = await ethers.getSigners() + const [signer] = await ethers.getSigners() const provider = signer.provider const balanceEtherBefore = await provider.getBalance(this.address1) expect(await this.cmtat.balanceOf(this.address1)).to.equal( diff --git a/test/common/MulticallModuleCommon.js b/test/common/MulticallModuleCommon.js index bfc90b53..271b8f64 100644 --- a/test/common/MulticallModuleCommon.js +++ b/test/common/MulticallModuleCommon.js @@ -38,8 +38,10 @@ function MulticallModuleCommon () { results[1] ) - expect(name).to.equal('CMTA Token') - expect(symbol).to.equal('CMTAT') + // Assert the multicall results match the direct getters rather than + // hardcoding the deployed name/symbol (which duplicates fixture knowledge) + expect(name).to.equal(await this.cmtat.name()) + expect(symbol).to.equal(await this.cmtat.symbol()) }) }) } diff --git a/test/common/PauseModuleCommon.js b/test/common/PauseModuleCommon.js index 181c544e..5e94dc48 100644 --- a/test/common/PauseModuleCommon.js +++ b/test/common/PauseModuleCommon.js @@ -84,9 +84,13 @@ function PauseModuleCommon () { await expect(this.logs) .to.emit(this.cmtat, 'Unpaused') .withArgs(this.admin) - // Transfer works + // Transfer is no longer blocked by the pause gate. address1 has no + // balance (and may not be allowlisted), so the tx can still revert for + // another reason — but it must NOT revert with EnforcedPause anymore. if (!this.generic) { - this.cmtat.connect(this.address1).transfer(this.address2, 10n) + await expect( + this.cmtat.connect(this.address1).transfer(this.address2, 10n) + ).to.not.be.revertedWithCustomError(this.cmtat, 'EnforcedPause') } }) @@ -106,9 +110,13 @@ function PauseModuleCommon () { await expect(this.logs) .to.emit(this.cmtat, 'Unpaused') .withArgs(this.address1) - // Transfer works + // Transfer is no longer blocked by the pause gate. address1 has no + // balance (and may not be allowlisted), so the tx can still revert for + // another reason — but it must NOT revert with EnforcedPause anymore. if (!this.generic) { - this.cmtat.connect(this.address1).transfer(this.address2, 10n) + await expect( + this.cmtat.connect(this.address1).transfer(this.address2, 10n) + ).to.not.be.revertedWithCustomError(this.cmtat, 'EnforcedPause') } }) diff --git a/test/common/SnapshotModuleCommon/SnapshotModuleCommonGetNextSnapshot.js b/test/common/SnapshotModuleCommon/SnapshotModuleCommonGetNextSnapshot.js index 298e5273..3f06f114 100644 --- a/test/common/SnapshotModuleCommon/SnapshotModuleCommonGetNextSnapshot.js +++ b/test/common/SnapshotModuleCommon/SnapshotModuleCommonGetNextSnapshot.js @@ -14,16 +14,18 @@ function SnapshotModuleCommonGetNextSnapshot () { 'SnapshotEngineMock', [this.cmtat.target, this.admin] ) - this.cmtat + await this.cmtat .connect(this.admin) .setSnapshotEngine(this.transferEngineMock) } }) it('testCanReturnTheRightAddressIfSet', async function () { - if (this.definedAtDeployment) { - const transferEngine = await this.cmtat.snapshotEngine() - expect(this.transferEngineMock.target).to.equal(transferEngine) + // Only meaningful when the snapshot engine is wired at deployment + if (!this.definedAtDeployment) { + this.skip() } + const transferEngine = await this.cmtat.snapshotEngine() + expect(this.transferEngineMock.target).to.equal(transferEngine) }) it('testCanGetAllNextSnapshots', async function () { // Arrange @@ -85,8 +87,9 @@ function SnapshotModuleCommonGetNextSnapshot () { await this.transferEngineMock .connect(this.admin) .scheduleSnapshot(this.snapshotTime3) - // We jump into the future - await time.increase(4) + // Jump deterministically past the latest snapshot (avoids relying on the + // 1s-per-tx auto-mine drift to push the snapshots into the past) + await time.increaseTo(this.snapshotTime3 + 1) // Act const snapshots = await this.transferEngineMock.getNextSnapshots() // Assert @@ -101,8 +104,8 @@ function SnapshotModuleCommonGetNextSnapshot () { await this.transferEngineMock .connect(this.admin) .scheduleSnapshot(this.snapshotTime1) - // We jump into the future - await time.increase(3) + // Jump deterministically just past snapshotTime1 (but before snapshotTime2) + await time.increaseTo(this.snapshotTime1 + 1) await this.transferEngineMock .connect(this.admin) .scheduleSnapshot(this.snapshotTime2) diff --git a/test/common/SnapshotModuleCommon/SnapshotModuleCommonRescheduling.js b/test/common/SnapshotModuleCommon/SnapshotModuleCommonRescheduling.js index 2dcfe80b..e98ebed7 100644 --- a/test/common/SnapshotModuleCommon/SnapshotModuleCommonRescheduling.js +++ b/test/common/SnapshotModuleCommon/SnapshotModuleCommonRescheduling.js @@ -16,7 +16,7 @@ function SnapshotModuleCommonRescheduling () { 'SnapshotEngineMock', [this.cmtat.target, this.admin] ) - this.cmtat + await this.cmtat .connect(this.admin) .setSnapshotEngine(this.transferEngineMock) } @@ -153,7 +153,7 @@ function SnapshotModuleCommonRescheduling () { ]) }) - it('reverts when calling from non-owner', async function () { + it('reverts when calling from non-snapshooter', async function () { // Act await expect( this.transferEngineMock @@ -234,7 +234,7 @@ function SnapshotModuleCommonRescheduling () { ) }) - it('reverts if no snapshot exits', async function () { + it('reverts if no snapshot exists', async function () { this.logs = await this.transferEngineMock .connect(this.admin) .unscheduleLastSnapshot(this.snapshotTime) diff --git a/test/common/SnapshotModuleCommon/SnapshotModuleCommonScheduling.js b/test/common/SnapshotModuleCommon/SnapshotModuleCommonScheduling.js index 9d3691cd..8a4f1f3d 100644 --- a/test/common/SnapshotModuleCommon/SnapshotModuleCommonScheduling.js +++ b/test/common/SnapshotModuleCommon/SnapshotModuleCommonScheduling.js @@ -14,7 +14,7 @@ function SnapshotModuleCommonScheduling () { 'SnapshotEngineMock', [this.cmtat.target, this.admin] ) - this.cmtat + await this.cmtat .connect(this.admin) .setSnapshotEngine(this.transferEngineMock) } @@ -125,7 +125,7 @@ function SnapshotModuleCommonScheduling () { 'SnapshotEngineMock', [this.cmtat.target, this.admin] ) - this.cmtat + await this.cmtat .connect(this.admin) .setSnapshotEngine(this.transferEngineMock) } @@ -154,7 +154,7 @@ function SnapshotModuleCommonScheduling () { expect(snapshots[0]).to.equal(THIRD_SNAPSHOT) }) - it('can schedule a snaphot in a random place', async function () { + it('can schedule a snapshot in a random place', async function () { const baseTime = await time.latest() // Arrange const FIRST_SNAPSHOT = baseTime + time.duration.seconds(3600) @@ -235,19 +235,6 @@ function SnapshotModuleCommonScheduling () { this.transferEngineMock, 'CMTAT_SnapshotModule_SnapshotScheduledInThePast' ) - /* await expect( - this.transferEngineMock - .connect(this.admin) - .scheduleSnapshotNotOptimized(SNAPSHOT_TIME) - ) - .to.be.revertedWithCustomError( - this.transferEngineMock, - 'CMTAT_SnapshotModule_SnapshotScheduledInThePast' - ) - .withArgs( - SNAPSHOT_TIME, - (await time.latest()) + time.duration.seconds(1) - ) */ }) it('reverts when trying to schedule a snapshot with the same time twice', async function () { diff --git a/test/common/SnapshotModuleCommon/SnapshotModuleCommonUnschedule.js b/test/common/SnapshotModuleCommon/SnapshotModuleCommonUnschedule.js index c2b1cc19..64d589eb 100644 --- a/test/common/SnapshotModuleCommon/SnapshotModuleCommonUnschedule.js +++ b/test/common/SnapshotModuleCommon/SnapshotModuleCommonUnschedule.js @@ -20,7 +20,7 @@ function SnapshotModuleCommonUnschedule () { 'SnapshotEngineMock', [this.cmtat.target, this.admin] ) - this.cmtat + await this.cmtat .connect(this.admin) .setSnapshotEngine(this.transferEngineMock) } @@ -62,11 +62,13 @@ function SnapshotModuleCommonUnschedule () { .connect(this.admin) .unscheduleSnapshotNotOptimized(this.snapshotTime3) snapshots = await this.transferEngineMock.getNextSnapshots() + // snapshotTime3 was just unscheduled, so it must be gone and + // snapshotTime4/5 remain (previously masked by a swallowed assertion) checkArraySnapshot(snapshots, [ this.snapshotTime1, this.snapshotTime2, - this.snapshotTime3, - this.snapshotTime4 + this.snapshotTime4, + this.snapshotTime5 ]) expect(snapshots.length).to.equal(4) }) @@ -98,7 +100,7 @@ function SnapshotModuleCommonUnschedule () { ) }) - it('can unschedule a snaphot in a random place', async function () { + it('can unschedule a snapshot in a random place', async function () { const RANDOM_SNAPSHOT = this.currentTime + time.duration.seconds(17) await this.transferEngineMock .connect(this.admin) @@ -122,7 +124,7 @@ function SnapshotModuleCommonUnschedule () { checkArraySnapshot(snapshots, [ this.snapshotTime1, this.snapshotTime2, - this.RANDOM_SNAPSHOT, + RANDOM_SNAPSHOT, this.snapshotTime3, this.snapshotTime4, this.snapshotTime5 @@ -143,7 +145,7 @@ function SnapshotModuleCommonUnschedule () { ]) }) - it('can schedule a snaphot after an unschedule', async function () { + it('can schedule a snapshot after an unschedule', async function () { await this.transferEngineMock .connect(this.admin) .scheduleSnapshot(this.snapshotTime1) @@ -180,7 +182,7 @@ function SnapshotModuleCommonUnschedule () { ]) }) - it('reverts when calling from non-admin', async function () { + it('reverts when calling from non-snapshooter', async function () { // Arrange const SNAPSHOT_TIME = this.currentTime + time.duration.seconds(60) this.logs = await this.transferEngineMock @@ -215,7 +217,7 @@ function SnapshotModuleCommonUnschedule () { 'SnapshotEngineMock', [this.cmtat.target, this.admin] ) - this.cmtat + await this.cmtat .connect(this.admin) .setSnapshotEngine(this.transferEngineMock) } @@ -235,7 +237,7 @@ function SnapshotModuleCommonUnschedule () { expect(snapshots.length).to.equal(0) }) - it('reverts when calling from non-admin', async function () { + it('reverts when calling from non-snapshooter', async function () { await expect( this.transferEngineMock .connect(this.address1) diff --git a/test/common/SnapshotModuleCommon/SnapshotModuleSetSnapshotEngineCommon.js b/test/common/SnapshotModuleCommon/SnapshotModuleSetSnapshotEngineCommon.js index 07fbc42a..d1398111 100644 --- a/test/common/SnapshotModuleCommon/SnapshotModuleSetSnapshotEngineCommon.js +++ b/test/common/SnapshotModuleCommon/SnapshotModuleSetSnapshotEngineCommon.js @@ -120,20 +120,28 @@ function SnapshotModuleSetSnapshotEngineCommon () { }) context('SnapshotEngineSetTest', function () { - it('testCanBeSetByAdmin', async function () { + beforeEach(async function () { + // Deployed for every test so testCannotBeSetByNonAdmin no longer + // depends on testCanBeSetByAdmin having run first this.transferEngineMock = await ethers.deployContract( 'SnapshotEngineMock', [ZERO_ADDRESS, this.admin] ) + }) + it('testCanBeSetByAdmin', async function () { // Act this.logs = await this.cmtat .connect(this.admin) .setSnapshotEngine(this.transferEngineMock.target) // Assert - // emits a SnapshotEngineSet event + // emits a SnapshotEngine event await expect(this.logs) .to.emit(this.cmtat, 'SnapshotEngine') .withArgs(this.transferEngineMock.target) + // the engine is effectively stored + expect(await this.cmtat.snapshotEngine()).to.equal( + this.transferEngineMock.target + ) }) it('testCannotBeSetByAdminWithTheSameValue', async function () { diff --git a/test/common/SnapshotModuleCommon/SnapshotModuleUtils/SnapshotModuleUtils.js b/test/common/SnapshotModuleCommon/SnapshotModuleUtils/SnapshotModuleUtils.js index 4b6c14e1..f3c99b36 100644 --- a/test/common/SnapshotModuleCommon/SnapshotModuleUtils/SnapshotModuleUtils.js +++ b/test/common/SnapshotModuleCommon/SnapshotModuleUtils/SnapshotModuleUtils.js @@ -1,13 +1,6 @@ const { expect } = require('chai') -const getUnixTimestamp = () => { - return Math.round(new Date().getTime() / 1000) -} - -const timeout = function (ms) { - return new Promise((resolve) => setTimeout(resolve, ms)) -} -async function checkSnapshot (time, totalSupply, addressese, balances) { +async function checkSnapshot (time, totalSupply, balances) { const addresses = [this.address1, this.address2, this.address3] // Values before the snapshot expect(await this.transferEngineMock.snapshotTotalSupply(time)).to.equal( @@ -39,14 +32,14 @@ async function checkSnapshot (time, totalSupply, addressese, balances) { expect(result2[1][0]).to.equal(totalSupply) } -async function checkArraySnapshot (snapshots, snapshotsValue) { +// Synchronous on purpose: it contains no await. Making it async caused every +// (unawaited) call site to swallow assertion failures as unhandled rejections. +function checkArraySnapshot (snapshots, snapshotsValue) { for (let i = 0; i < snapshots.length; ++i) { expect(snapshots[i]).to.equal(snapshotsValue[i]) } } module.exports = { - getUnixTimestamp, - timeout, checkSnapshot, checkArraySnapshot } diff --git a/test/common/SnapshotModuleCommon/global/SnapshotModuleMultiplePlannedTest.js b/test/common/SnapshotModuleCommon/global/SnapshotModuleMultiplePlannedTest.js index 711b8c46..a5c67598 100644 --- a/test/common/SnapshotModuleCommon/global/SnapshotModuleMultiplePlannedTest.js +++ b/test/common/SnapshotModuleCommon/global/SnapshotModuleMultiplePlannedTest.js @@ -5,7 +5,6 @@ const { ZERO_ADDRESS } = require('../../../utils') function SnapshotModuleMultiplePlannedTest () { // With multiple planned snapshot context('SnapshotMultiplePlannedTest', function () { - const ADDRESSES = [this.address1, this.address2, this.address3] const ADDRESS1_INITIAL_MINT = 31n const ADDRESS2_INITIAL_MINT = 32n const ADDRESS3_INITIAL_MINT = 33n @@ -22,7 +21,7 @@ function SnapshotModuleMultiplePlannedTest () { 'SnapshotEngineMock', [this.cmtat.target, this.admin] ) - this.cmtat + await this.cmtat .connect(this.admin) .setSnapshotEngine(this.transferEngineMock) } @@ -63,7 +62,6 @@ function SnapshotModuleMultiplePlannedTest () { this, await time.latest(), TOTAL_SUPPLY_INITIAL_MINT, - ADDRESSES, [ADDRESS1_INITIAL_MINT, ADDRESS2_INITIAL_MINT, ADDRESS3_INITIAL_MINT] ) @@ -80,7 +78,6 @@ function SnapshotModuleMultiplePlannedTest () { this, this.beforeSnapshotTime, TOTAL_SUPPLY_INITIAL_MINT, - ADDRESSES, [ADDRESS1_INITIAL_MINT, ADDRESS2_INITIAL_MINT, ADDRESS3_INITIAL_MINT] ) // values at the time of the first snapshot @@ -88,7 +85,6 @@ function SnapshotModuleMultiplePlannedTest () { this, this.snapshotTime1, TOTAL_SUPPLY_INITIAL_MINT, - ADDRESSES, [ADDRESS1_INITIAL_MINT, ADDRESS2_INITIAL_MINT, ADDRESS3_INITIAL_MINT] ) // values now @@ -100,7 +96,6 @@ function SnapshotModuleMultiplePlannedTest () { this, await time.latest(), TOTAL_SUPPLY_INITIAL_MINT, - ADDRESSES, [ address1NewTokensBalance, address2NewTokensBalance, @@ -118,7 +113,6 @@ function SnapshotModuleMultiplePlannedTest () { this, await time.latest(), TOTAL_SUPPLY_INITIAL_MINT, - ADDRESSES, [ADDRESS1_INITIAL_MINT, ADDRESS2_INITIAL_MINT, ADDRESS3_INITIAL_MINT] ) @@ -135,7 +129,6 @@ function SnapshotModuleMultiplePlannedTest () { this, this.beforeSnapshotTime, TOTAL_SUPPLY_INITIAL_MINT, - ADDRESSES, [ADDRESS1_INITIAL_MINT, ADDRESS2_INITIAL_MINT, ADDRESS3_INITIAL_MINT] ) // Values at the time of the first snapshot @@ -143,7 +136,6 @@ function SnapshotModuleMultiplePlannedTest () { this, this.snapshotTime1, TOTAL_SUPPLY_INITIAL_MINT, - ADDRESSES, [ADDRESS1_INITIAL_MINT, ADDRESS2_INITIAL_MINT, ADDRESS3_INITIAL_MINT] ) // Values at the time of the second snapshot @@ -151,7 +143,6 @@ function SnapshotModuleMultiplePlannedTest () { this, this.snapshotTime2, TOTAL_SUPPLY_INITIAL_MINT, - ADDRESSES, [ADDRESS1_INITIAL_MINT, ADDRESS2_INITIAL_MINT, ADDRESS3_INITIAL_MINT] ) // values now @@ -163,7 +154,6 @@ function SnapshotModuleMultiplePlannedTest () { this, await time.latest(), TOTAL_SUPPLY_INITIAL_MINT, - ADDRESSES, [ address1NewTokensBalance, address2NewTokensBalance, @@ -182,7 +172,6 @@ function SnapshotModuleMultiplePlannedTest () { this, await time.latest(), TOTAL_SUPPLY_INITIAL_MINT, - ADDRESSES, [ADDRESS1_INITIAL_MINT, ADDRESS2_INITIAL_MINT, ADDRESS3_INITIAL_MINT] ) // Act @@ -198,7 +187,6 @@ function SnapshotModuleMultiplePlannedTest () { this, this.beforeSnapshotTime, TOTAL_SUPPLY_INITIAL_MINT, - ADDRESSES, [ADDRESS1_INITIAL_MINT, ADDRESS2_INITIAL_MINT, ADDRESS3_INITIAL_MINT] ) // Values at the time of the first snapshot @@ -206,7 +194,6 @@ function SnapshotModuleMultiplePlannedTest () { this, this.snapshotTime1, TOTAL_SUPPLY_INITIAL_MINT, - ADDRESSES, [ADDRESS1_INITIAL_MINT, ADDRESS2_INITIAL_MINT, ADDRESS3_INITIAL_MINT] ) // Values at the time of the second snapshot @@ -214,7 +201,6 @@ function SnapshotModuleMultiplePlannedTest () { this, this.snapshotTime2, TOTAL_SUPPLY_INITIAL_MINT, - ADDRESSES, [ADDRESS1_INITIAL_MINT, ADDRESS2_INITIAL_MINT, ADDRESS3_INITIAL_MINT] ) // Values at the time of the third snapshot @@ -222,7 +208,6 @@ function SnapshotModuleMultiplePlannedTest () { this, this.snapshotTime3, TOTAL_SUPPLY_INITIAL_MINT, - ADDRESSES, [ADDRESS1_INITIAL_MINT, ADDRESS2_INITIAL_MINT, ADDRESS3_INITIAL_MINT] ) // Values now @@ -234,7 +219,6 @@ function SnapshotModuleMultiplePlannedTest () { this, await time.latest(), TOTAL_SUPPLY_INITIAL_MINT, - ADDRESSES, [ address1NewTokensBalance, address2NewTokensBalance, @@ -251,7 +235,6 @@ function SnapshotModuleMultiplePlannedTest () { this, await time.latest(), TOTAL_SUPPLY_INITIAL_MINT, - ADDRESSES, [ADDRESS1_INITIAL_MINT, ADDRESS2_INITIAL_MINT, ADDRESS3_INITIAL_MINT] ) @@ -268,7 +251,6 @@ function SnapshotModuleMultiplePlannedTest () { this, this.beforeSnapshotTime, TOTAL_SUPPLY_INITIAL_MINT, - ADDRESSES, [ADDRESS1_INITIAL_MINT, ADDRESS2_INITIAL_MINT, ADDRESS3_INITIAL_MINT] ) // Values at the time of the first snapshot @@ -276,22 +258,20 @@ function SnapshotModuleMultiplePlannedTest () { this, this.snapshotTime1, TOTAL_SUPPLY_INITIAL_MINT, - ADDRESSES, [ADDRESS1_INITIAL_MINT, ADDRESS2_INITIAL_MINT, ADDRESS3_INITIAL_MINT] ) const ADDRESS1_BALANCE_AFTER_ONE_TRANSFER = ADDRESS1_INITIAL_MINT - TRANSFER_AMOUNT_1 - const ADDRESS2_BALANCE_AFTER_TONE_TRANSFER = + const ADDRESS2_BALANCE_AFTER_ONE_TRANSFER = ADDRESS2_INITIAL_MINT + TRANSFER_AMOUNT_1 // Values at the time of the second snapshot await checkSnapshot.call( this, this.snapshotTime2, TOTAL_SUPPLY_INITIAL_MINT, - ADDRESSES, [ ADDRESS1_BALANCE_AFTER_ONE_TRANSFER, - ADDRESS2_BALANCE_AFTER_TONE_TRANSFER, + ADDRESS2_BALANCE_AFTER_ONE_TRANSFER, ADDRESS3_INITIAL_MINT ] ) @@ -300,10 +280,9 @@ function SnapshotModuleMultiplePlannedTest () { this, this.snapshotTime3, TOTAL_SUPPLY_INITIAL_MINT, - ADDRESSES, [ ADDRESS1_BALANCE_AFTER_ONE_TRANSFER, - ADDRESS2_BALANCE_AFTER_TONE_TRANSFER, + ADDRESS2_BALANCE_AFTER_ONE_TRANSFER, ADDRESS3_INITIAL_MINT ] ) @@ -312,10 +291,9 @@ function SnapshotModuleMultiplePlannedTest () { this, await time.latest(), TOTAL_SUPPLY_INITIAL_MINT, - ADDRESSES, [ ADDRESS1_BALANCE_AFTER_ONE_TRANSFER, - ADDRESS2_BALANCE_AFTER_TONE_TRANSFER, + ADDRESS2_BALANCE_AFTER_ONE_TRANSFER, ADDRESS3_INITIAL_MINT ] ) @@ -338,7 +316,6 @@ function SnapshotModuleMultiplePlannedTest () { this, this.beforeSnapshotTime, TOTAL_SUPPLY_INITIAL_MINT, - ADDRESSES, [ADDRESS1_INITIAL_MINT, ADDRESS2_INITIAL_MINT, ADDRESS3_INITIAL_MINT] ) // Values at the time of the first snapshot @@ -346,7 +323,6 @@ function SnapshotModuleMultiplePlannedTest () { this, this.snapshotTime1, TOTAL_SUPPLY_INITIAL_MINT, - ADDRESSES, [ADDRESS1_INITIAL_MINT, ADDRESS2_INITIAL_MINT, ADDRESS3_INITIAL_MINT] ) // Values at the time of the second snapshot @@ -354,10 +330,9 @@ function SnapshotModuleMultiplePlannedTest () { this, this.snapshotTime2, TOTAL_SUPPLY_INITIAL_MINT, - ADDRESSES, [ ADDRESS1_BALANCE_AFTER_ONE_TRANSFER, - ADDRESS2_BALANCE_AFTER_TONE_TRANSFER, + ADDRESS2_BALANCE_AFTER_ONE_TRANSFER, ADDRESS3_INITIAL_MINT ] ) @@ -365,12 +340,11 @@ function SnapshotModuleMultiplePlannedTest () { const ADDRESS1_BALANCE_AFTER_TWO_TRANSFER = ADDRESS1_BALANCE_AFTER_ONE_TRANSFER + TRANSFER_AMOUNT_2 const ADDRESS2_BALANCE_AFTER_TWO_TRANSFER = - ADDRESS2_BALANCE_AFTER_TONE_TRANSFER - TRANSFER_AMOUNT_2 + ADDRESS2_BALANCE_AFTER_ONE_TRANSFER - TRANSFER_AMOUNT_2 await checkSnapshot.call( this, await time.latest(), TOTAL_SUPPLY_INITIAL_MINT, - ADDRESSES, [ ADDRESS1_BALANCE_AFTER_TWO_TRANSFER, ADDRESS2_BALANCE_AFTER_TWO_TRANSFER, @@ -397,15 +371,13 @@ function SnapshotModuleMultiplePlannedTest () { this, this.beforeSnapshotTime, TOTAL_SUPPLY_INITIAL_MINT, - ADDRESSES, [ADDRESS1_INITIAL_MINT, ADDRESS2_INITIAL_MINT, ADDRESS3_INITIAL_MINT] ) // Values at the time of the first snapshot - checkSnapshot.call( + await checkSnapshot.call( this, this.snapshotTime1, TOTAL_SUPPLY_INITIAL_MINT, - ADDRESSES, [ADDRESS1_INITIAL_MINT, ADDRESS2_INITIAL_MINT, ADDRESS3_INITIAL_MINT] ) // Values at the time of the second snapshot @@ -413,10 +385,9 @@ function SnapshotModuleMultiplePlannedTest () { this, this.snapshotTime2, TOTAL_SUPPLY_INITIAL_MINT, - ADDRESSES, [ ADDRESS1_BALANCE_AFTER_ONE_TRANSFER, - ADDRESS2_BALANCE_AFTER_TONE_TRANSFER, + ADDRESS2_BALANCE_AFTER_ONE_TRANSFER, ADDRESS3_INITIAL_MINT ] ) @@ -425,7 +396,6 @@ function SnapshotModuleMultiplePlannedTest () { this, this.snapshotTime3, TOTAL_SUPPLY_INITIAL_MINT, - ADDRESSES, [ ADDRESS1_BALANCE_AFTER_TWO_TRANSFER, ADDRESS2_BALANCE_AFTER_TWO_TRANSFER, @@ -436,12 +406,11 @@ function SnapshotModuleMultiplePlannedTest () { const ADDRESS1_BALANCE_AFTER_THREE_TRANSFER = ADDRESS1_BALANCE_AFTER_ONE_TRANSFER + TRANSFER_AMOUNT_3 const ADDRESS2_BALANCE_AFTER_THREE_TRANSFER = - ADDRESS2_BALANCE_AFTER_TONE_TRANSFER - TRANSFER_AMOUNT_3 + ADDRESS2_BALANCE_AFTER_ONE_TRANSFER - TRANSFER_AMOUNT_3 await checkSnapshot.call( this, await time.latest(), TOTAL_SUPPLY_INITIAL_MINT, - ADDRESSES, [ ADDRESS1_BALANCE_AFTER_THREE_TRANSFER, ADDRESS2_BALANCE_AFTER_THREE_TRANSFER, diff --git a/test/common/SnapshotModuleCommon/global/SnapshotModuleOnePlannedSnapshotTest.js b/test/common/SnapshotModuleCommon/global/SnapshotModuleOnePlannedSnapshotTest.js index ebda8292..ca63d979 100644 --- a/test/common/SnapshotModuleCommon/global/SnapshotModuleOnePlannedSnapshotTest.js +++ b/test/common/SnapshotModuleCommon/global/SnapshotModuleOnePlannedSnapshotTest.js @@ -5,10 +5,8 @@ const { ZERO_ADDRESS } = require('../../../utils') const REASON_STRING = 'BURN_TEST' const REASON_EVENT = ethers.toUtf8Bytes(REASON_STRING) const REASON = ethers.Typed.bytes(REASON_EVENT) -const REASON_EMPTY = ethers.Typed.bytes(ethers.toUtf8Bytes('')) function SnapshotModuleOnePlannedSnapshotTest () { - const ADDRESSES = [this.address1, this.address2, this.address3] const ADDRESS1_INITIAL_MINT = '31' const ADDRESS2_INITIAL_MINT = '32' const ADDRESS3_INITIAL_MINT = '33' @@ -20,7 +18,7 @@ function SnapshotModuleOnePlannedSnapshotTest () { 'SnapshotEngineMock', [this.cmtat.target, this.admin] ) - this.cmtat + await this.cmtat .connect(this.admin) .setSnapshotEngine(this.transferEngineMock) } @@ -50,7 +48,6 @@ function SnapshotModuleOnePlannedSnapshotTest () { this, await time.latest(), TOTAL_SUPPLY_INITIAL_MINT, - ADDRESSES, [ADDRESS1_INITIAL_MINT, ADDRESS2_INITIAL_MINT, ADDRESS3_INITIAL_MINT] ); // Act @@ -61,13 +58,17 @@ function SnapshotModuleOnePlannedSnapshotTest () { // Assert // Values before the snapshot - // await checkSnapshot.call(this, this.beforeSnapshotTime, TOTAL_SUPPLY_INITIAL_MINT, ADDRESSES, [ADDRESS1_INITIAL_MINT, ADDRESS2_INITIAL_MINT, ADDRESS3_INITIAL_MINT]) + await checkSnapshot.call( + this, + this.beforeSnapshotTime, + TOTAL_SUPPLY_INITIAL_MINT, + [ADDRESS1_INITIAL_MINT, ADDRESS2_INITIAL_MINT, ADDRESS3_INITIAL_MINT] + ) // Value at the time of the snapshot await checkSnapshot.call( this, this.snapshotTime, TOTAL_SUPPLY_INITIAL_MINT, - ADDRESSES, [ADDRESS1_INITIAL_MINT, ADDRESS2_INITIAL_MINT, ADDRESS3_INITIAL_MINT] ) // Values now @@ -81,7 +82,6 @@ function SnapshotModuleOnePlannedSnapshotTest () { this, await time.latest(), newTotalSupply, - ADDRESSES, [address1NewTokensBalance, ADDRESS2_INITIAL_MINT, ADDRESS3_INITIAL_MINT] ) const snapshots = await this.transferEngineMock.getNextSnapshots() @@ -95,7 +95,6 @@ function SnapshotModuleOnePlannedSnapshotTest () { this, await time.latest(), TOTAL_SUPPLY_INITIAL_MINT, - ADDRESSES, [ADDRESS1_INITIAL_MINT, ADDRESS2_INITIAL_MINT, ADDRESS3_INITIAL_MINT] ) @@ -109,13 +108,17 @@ function SnapshotModuleOnePlannedSnapshotTest () { // Assert // Values before the snapshot - // await checkSnapshot.call(this, this.beforeSnapshotTime, TOTAL_SUPPLY_INITIAL_MINT, ADDRESSES, [ADDRESS1_INITIAL_MINT, ADDRESS2_INITIAL_MINT, ADDRESS3_INITIAL_MINT]) + await checkSnapshot.call( + this, + this.beforeSnapshotTime, + TOTAL_SUPPLY_INITIAL_MINT, + [ADDRESS1_INITIAL_MINT, ADDRESS2_INITIAL_MINT, ADDRESS3_INITIAL_MINT] + ) // Value at the time of the snapshot await checkSnapshot.call( this, this.snapshotTime, TOTAL_SUPPLY_INITIAL_MINT, - ADDRESSES, [ADDRESS1_INITIAL_MINT, ADDRESS2_INITIAL_MINT, ADDRESS3_INITIAL_MINT] ) // Values now @@ -129,7 +132,6 @@ function SnapshotModuleOnePlannedSnapshotTest () { this, await time.latest(), newTotalSupply, - ADDRESSES, [address1NewTokensBalance, ADDRESS2_INITIAL_MINT, ADDRESS3_INITIAL_MINT] ) const snapshots = await this.transferEngineMock.getNextSnapshots() @@ -143,7 +145,6 @@ function SnapshotModuleOnePlannedSnapshotTest () { this, await time.latest(), TOTAL_SUPPLY_INITIAL_MINT, - ADDRESSES, [ADDRESS1_INITIAL_MINT, ADDRESS2_INITIAL_MINT, ADDRESS3_INITIAL_MINT] ) @@ -158,13 +159,17 @@ function SnapshotModuleOnePlannedSnapshotTest () { // Assert // Values before the snapshot - // await checkSnapshot.call(this, this.beforeSnapshotTime, TOTAL_SUPPLY_INITIAL_MINT, ADDRESSES, [ADDRESS1_INITIAL_MINT, ADDRESS2_INITIAL_MINT, ADDRESS3_INITIAL_MINT]) + await checkSnapshot.call( + this, + this.beforeSnapshotTime, + TOTAL_SUPPLY_INITIAL_MINT, + [ADDRESS1_INITIAL_MINT, ADDRESS2_INITIAL_MINT, ADDRESS3_INITIAL_MINT] + ) // Value at the time of the snapshot await checkSnapshot.call( this, this.snapshotTime, TOTAL_SUPPLY_INITIAL_MINT, - ADDRESSES, [ADDRESS1_INITIAL_MINT, ADDRESS2_INITIAL_MINT, ADDRESS3_INITIAL_MINT] ) // Values now @@ -178,7 +183,6 @@ function SnapshotModuleOnePlannedSnapshotTest () { this, await time.latest(), TOTAL_SUPPLY_INITIAL_MINT, - ADDRESSES, [ address1NewTokensBalance, address2NewTokensBalance, diff --git a/test/common/SnapshotModuleCommon/global/SnapshotModuleZeroPlannedSnapshot.js b/test/common/SnapshotModuleCommon/global/SnapshotModuleZeroPlannedSnapshot.js index 5ac60120..e8339459 100644 --- a/test/common/SnapshotModuleCommon/global/SnapshotModuleZeroPlannedSnapshot.js +++ b/test/common/SnapshotModuleCommon/global/SnapshotModuleZeroPlannedSnapshot.js @@ -1,10 +1,8 @@ const { time } = require('@nomicfoundation/hardhat-network-helpers') -const { expect } = require('chai') const { checkSnapshot } = require('../SnapshotModuleUtils/SnapshotModuleUtils') const { ZERO_ADDRESS } = require('../../../utils') -function SnapshotModuleCommonGlobal () { +function SnapshotModuleZeroPlannedSnapshot () { context('zeroPlannedSnapshotTest', function () { - const ADDRESSES = [this.address1, this.address2, this.address3] const ADDRESS1_INITIAL_MINT = '31' const ADDRESS2_INITIAL_MINT = '32' const ADDRESS3_INITIAL_MINT = '33' @@ -15,7 +13,7 @@ function SnapshotModuleCommonGlobal () { 'SnapshotEngineMock', [this.cmtat.target, this.admin] ) - this.cmtat + await this.cmtat .connect(this.admin) .setSnapshotEngine(this.transferEngineMock) } @@ -31,17 +29,16 @@ function SnapshotModuleCommonGlobal () { }) context('Before any snapshot', function () { - it('testCanGetBalanceAddress&TotalSupply', async function () { + it('testCanGetBalanceAddressAndTotalSupply', async function () { // Act + Assert await checkSnapshot.call( this, await time.latest(), TOTAL_SUPPLY_INITIAL_MINT, - ADDRESSES, [ADDRESS1_INITIAL_MINT, ADDRESS2_INITIAL_MINT, ADDRESS3_INITIAL_MINT] ) }) }) }) } -module.exports = SnapshotModuleCommonGlobal +module.exports = SnapshotModuleZeroPlannedSnapshot diff --git a/test/common/ValidationModule/ValidationModuleCommon.js b/test/common/ValidationModule/ValidationModuleCommon.js index 92490fad..208b8f36 100644 --- a/test/common/ValidationModule/ValidationModuleCommon.js +++ b/test/common/ValidationModule/ValidationModuleCommon.js @@ -24,13 +24,15 @@ function ValidationModuleCommon () { } }) it('testCanReturnTheRightAddressIfSet', async function () { - if (this.definedAtDeployment) { - expect(this.ruleEngineMock.target).to.equal( - await this.cmtat.ruleEngine() - ) + // Only meaningful when the rule engine is wired at deployment + if (!this.definedAtDeployment) { + this.skip() } + expect(this.ruleEngineMock.target).to.equal( + await this.cmtat.ruleEngine() + ) }) - it('testCanCanTransferWithoutRuleEngine', async function () { + it('testCanTransferWithoutRuleEngine', async function () { if (!this.erc1404) { // Arrange await this.cmtat.connect(this.admin).setRuleEngine(ZERO_ADDRESS) @@ -60,14 +62,16 @@ function ValidationModuleCommon () { }) it('testCanReturnMessageValidTransfer', async function () { - if (!this.erc1404) { - // Act + Assert - expect( - await this.cmtat.messageForTransferRestriction( - REJECTED_CODE_BASE_TRANSFER_OK - ) - ).to.equal('NoRestriction') + // This human-readable message is only defined by the non-erc1404 validation + if (this.erc1404) { + this.skip() } + // Act + Assert + expect( + await this.cmtat.messageForTransferRestriction( + REJECTED_CODE_BASE_TRANSFER_OK + ) + ).to.equal('NoRestriction') }) it('testCanDetectTransferRestrictionWithAmountTooHigh', async function () { @@ -92,14 +96,16 @@ function ValidationModuleCommon () { }) it('testCanReturnMessageWithAmountTooHigh', async function () { - if (!this.erc1404) { - // Act + Assert - expect( - await this.cmtat.messageForTransferRestriction( - RULE_MOCK_AMOUNT_MAX_CODE - ) - ).to.equal('Amount too high') + // This human-readable message is only defined by the non-erc1404 validation + if (this.erc1404) { + this.skip() } + // Act + Assert + expect( + await this.cmtat.messageForTransferRestriction( + RULE_MOCK_AMOUNT_MAX_CODE + ) + ).to.equal('Amount too high') }) it('testCanReturnMessageWithUnknownRestrictionCode', async function () { @@ -248,7 +254,7 @@ function ValidationModuleCommon () { ).to.equal(false) }) - it('testCanCanTransferFromWithoutRuleEngine', async function () { + it('testCanTransferFromWithoutRuleEngine', async function () { // Arrange if (!this.erc1404) { await this.cmtat.connect(this.admin).setRuleEngine(ZERO_ADDRESS) @@ -367,7 +373,7 @@ function ValidationModuleCommon () { } }) - it('testCanCanMintWithoutRuleEngine', async function () { + it('testCanMintWithoutRuleEngine', async function () { if (!this.erc1404) { // Arrange await this.cmtat.connect(this.admin).setRuleEngine(ZERO_ADDRESS) @@ -388,7 +394,7 @@ function ValidationModuleCommon () { this.address2, 11 ) - ).to.equal(0) + ).to.equal(REJECTED_CODE_BASE_TRANSFER_OK) expect( await this.cmtat.connect(this.admin).detectTransferRestrictionFrom( this.admin, @@ -396,7 +402,7 @@ function ValidationModuleCommon () { this.address2, 11 ) - ).to.equal(0) + ).to.equal(REJECTED_CODE_BASE_TRANSFER_OK) } expect( @@ -426,14 +432,16 @@ function ValidationModuleCommon () { }) it('testCanReturnMessageWithMintAmountTooHigh', async function () { - if (!this.erc1404) { - // Act + Assert - expect( - await this.cmtat.messageForTransferRestriction( - RULE_MOCK_MINT_RESTRICTION_CODE - ) - ).to.equal('Mint amount too high') + // This human-readable message is only defined by the non-erc1404 validation + if (this.erc1404) { + this.skip() } + // Act + Assert + expect( + await this.cmtat.messageForTransferRestriction( + RULE_MOCK_MINT_RESTRICTION_CODE + ) + ).to.equal('Mint amount too high') }) // this.address1 may transfer tokens to this.address2 diff --git a/test/common/ValidationModule/ValidationModuleCommonCore.js b/test/common/ValidationModule/ValidationModuleCommonCore.js index 035cf1a1..aab8bfbd 100644 --- a/test/common/ValidationModule/ValidationModuleCommonCore.js +++ b/test/common/ValidationModule/ValidationModuleCommonCore.js @@ -1,5 +1,4 @@ const { expect } = require('chai') -const { RULE_MOCK_AMOUNT_MAX, ZERO_ADDRESS } = require('../../utils') function ValidationModuleCommonCore () { // Transferring with Rule Engine set @@ -21,7 +20,7 @@ function ValidationModuleCommonCore () { } }) - it('testCanCanTransferWithoutRuleEngine', async function () { + it('testCanTransferWithoutRuleEngine', async function () { // Act + Assert expect( await this.cmtat.canTransfer(this.address1, this.address2, 10) @@ -62,9 +61,9 @@ function ValidationModuleCommonCore () { ) }) - // reverts if this.address1 transfers more tokens than rule allows + // this.address1 transfers an allowed amount to this.address2 it('testCanTransfer', async function () { - AMOUNT_TO_TRANSFER = 5 + const AMOUNT_TO_TRANSFER = 5n // Act expect( await this.cmtat.canTransfer( @@ -77,6 +76,17 @@ function ValidationModuleCommonCore () { await this.cmtat .connect(this.address1) .transfer(this.address2, AMOUNT_TO_TRANSFER) + + // Assert - balances moved by exactly AMOUNT_TO_TRANSFER + expect(await this.cmtat.balanceOf(this.address1)).to.equal( + this.ADDRESS1_INITIAL_BALANCE - AMOUNT_TO_TRANSFER + ) + expect(await this.cmtat.balanceOf(this.address2)).to.equal( + this.ADDRESS2_INITIAL_BALANCE + AMOUNT_TO_TRANSFER + ) + expect(await this.cmtat.balanceOf(this.address3)).to.equal( + this.ADDRESS3_INITIAL_BALANCE + ) }) }) } diff --git a/test/common/ValidationModule/ValidationModuleSetRuleEngineCommon.js b/test/common/ValidationModule/ValidationModuleSetRuleEngineCommon.js index d78023f9..b0501050 100644 --- a/test/common/ValidationModule/ValidationModuleSetRuleEngineCommon.js +++ b/test/common/ValidationModule/ValidationModuleSetRuleEngineCommon.js @@ -28,13 +28,13 @@ function ValidationModuleSetRuleEngineCommon () { // Act this.logs = await this.cmtat .connect(this.admin) - .setRuleEngine(this.ruleEngine) + .setRuleEngine(this.ruleEngineMock.target) // Assert - // emits a RuleEngineSet event + // emits a RuleEngine event await expect(this.logs) .to.emit(this.cmtat, 'RuleEngine') - .withArgs(this.ruleEngine) - expect(await this.cmtat.ruleEngine()).to.equal(this.ruleEngine) + .withArgs(this.ruleEngineMock.target) + expect(await this.cmtat.ruleEngine()).to.equal(this.ruleEngineMock.target) }) it('testCanNotBeSetByAdminWithTheSameValue', async function () { @@ -52,7 +52,7 @@ function ValidationModuleSetRuleEngineCommon () { it('testCannotBeSetByNonAdmin', async function () { // Act await expect( - this.cmtat.connect(this.address1).setRuleEngine(this.ruleEngine) + this.cmtat.connect(this.address1).setRuleEngine(this.ruleEngineMock.target) ) .to.be.revertedWithCustomError( this.cmtat, @@ -61,7 +61,7 @@ function ValidationModuleSetRuleEngineCommon () { .withArgs(this.address1.address, DEFAULT_ADMIN_ROLE) }) - it('testCanReturnMessageWithNoRuleEngine&UnknownRestrictionCode', async function () { + it('testCanReturnMessageWithNoRuleEngineAndUnknownRestrictionCode', async function () { // Act + Assert expect(await this.cmtat.messageForTransferRestriction(254)).to.equal( 'UnknownCode' diff --git a/test/common/ValidationModule/proxy/ValidationModuleProxyCommon.js b/test/common/ValidationModule/proxy/ValidationModuleProxyCommon.js index e29f0b6c..9746d813 100644 --- a/test/common/ValidationModule/proxy/ValidationModuleProxyCommon.js +++ b/test/common/ValidationModule/proxy/ValidationModuleProxyCommon.js @@ -9,7 +9,6 @@ const { fixture, loadFixture } = require('../../../deploymentUtils') -const { ZERO_ADDRESS } = require('../../../utils') function ValidationModuleProxyCommon () { context('Proxy - ValidationModule', function () { beforeEach(async function () { diff --git a/test/common/VersionModuleCommon.js b/test/common/VersionModuleCommon.js index 62fc096a..a2ef64ca 100644 --- a/test/common/VersionModuleCommon.js +++ b/test/common/VersionModuleCommon.js @@ -1,10 +1,11 @@ const { expect } = require('chai') +const { VERSION } = require('../utils') function VersionModuleCommon () { context('Token structure', function () { it('testHasTheDefinedVersion', async function () { // Act + Assert - expect(await this.cmtat.version()).to.equal('3.2.0') + expect(await this.cmtat.version()).to.equal(VERSION) }) }) } diff --git a/test/deployment/ERC1363/deploymentERC1363Proxy.test.js b/test/deployment/ERC1363/deploymentERC1363Proxy.test.js index c52788e1..ba4cb5cf 100644 --- a/test/deployment/ERC1363/deploymentERC1363Proxy.test.js +++ b/test/deployment/ERC1363/deploymentERC1363Proxy.test.js @@ -10,7 +10,8 @@ const { upgrades } = require('hardhat') const { ZERO_ADDRESS, IERC165_INTERFACEID, IERC721_INTERFACEID,IACCESSCONTROL_INTERFACEID, - IERC1363_INTERFACEID, IERC5679_INTERFACEID + IERC1363_INTERFACEID, IERC5679_INTERFACEID, + IERC1404_INTERFACEID, IERC1404EXTEND_INTERFACEID } = require('../../utils') // Core @@ -53,6 +54,9 @@ describe('CMTAT - ERC1363 Proxy Deployment', function () { false) expect(await this.cmtat.supportsInterface(IERC5679_INTERFACEID)).to.equal(true) expect(await this.cmtat.supportsInterface(IERC1363_INTERFACEID)).to.equal(true) + expect(await this.cmtat.supportsInterface(IERC1404_INTERFACEID)).to.equal(true) + expect(await this.cmtat.supportsInterface(IERC1404EXTEND_INTERFACEID)).to.equal(true) + expect(await this.cmtat.supportsInterface('0xffffffff')).to.equal(false) }) it('testCanSendTokenToReceiverContract', async function () { // Arrange diff --git a/test/deployment/ERC1363/deploymentERC1363ProxyValidation.test.js b/test/deployment/ERC1363/deploymentERC1363ProxyValidation.test.js index 39f5488b..19f57c70 100644 --- a/test/deployment/ERC1363/deploymentERC1363ProxyValidation.test.js +++ b/test/deployment/ERC1363/deploymentERC1363ProxyValidation.test.js @@ -1,4 +1,3 @@ -const { expect } = require('chai') const ValidationModuleProxyCommon = require('../../common/ValidationModule/proxy/ValidationModuleProxyCommon') describe('CMTAT ERC1363 - Proxy - ValidationModule', function () { diff --git a/test/deployment/ERC1363/deploymentERC1363Standalone.test.js b/test/deployment/ERC1363/deploymentERC1363Standalone.test.js index fc30d2a2..f3e247ae 100644 --- a/test/deployment/ERC1363/deploymentERC1363Standalone.test.js +++ b/test/deployment/ERC1363/deploymentERC1363Standalone.test.js @@ -6,7 +6,8 @@ const { } = require('../../deploymentUtils') const { IERC165_INTERFACEID, IERC721_INTERFACEID,IACCESSCONTROL_INTERFACEID, - IERC5679_INTERFACEID, IERC1363_INTERFACEID + IERC5679_INTERFACEID, IERC1363_INTERFACEID, + IERC1404_INTERFACEID, IERC1404EXTEND_INTERFACEID } = require('../../utils') // Core @@ -49,6 +50,8 @@ describe('CMTAT ERC1363 - Standalone', function () { false) expect(await this.cmtat.supportsInterface(IERC5679_INTERFACEID)).to.equal(true) expect(await this.cmtat.supportsInterface(IERC1363_INTERFACEID)).to.equal(true) + expect(await this.cmtat.supportsInterface(IERC1404_INTERFACEID)).to.equal(true) + expect(await this.cmtat.supportsInterface(IERC1404EXTEND_INTERFACEID)).to.equal(true) }) it('testCanSendTokenToReceiverContract', async function () { // Arrange diff --git a/test/deployment/allowlist/deploymentStandaloneAllowlist.test.js b/test/deployment/allowlist/deploymentStandaloneAllowlist.test.js index 4ef2df39..3327a351 100644 --- a/test/deployment/allowlist/deploymentStandaloneAllowlist.test.js +++ b/test/deployment/allowlist/deploymentStandaloneAllowlist.test.js @@ -1,10 +1,8 @@ -const { expect } = require('chai') const { deployCMTATAllowlistStandalone, fixture, loadFixture } = require('../../deploymentUtils') -const { ZERO_ADDRESS } = require('../../utils') // Core const ERC20BaseModuleCommon = require('../../common/ERC20BaseModuleCommon') const ERC20MintModuleCommon = require('../../common/ERC20MintModuleCommon') diff --git a/test/deployment/allowlist/deploymentStandaloneDisableAllowlist.test.js b/test/deployment/allowlist/deploymentStandaloneDisableAllowlist.test.js index 6e417b7b..3fc072e9 100644 --- a/test/deployment/allowlist/deploymentStandaloneDisableAllowlist.test.js +++ b/test/deployment/allowlist/deploymentStandaloneDisableAllowlist.test.js @@ -1,10 +1,8 @@ -const { expect } = require('chai') const { deployCMTATAllowlistStandalone, fixture, loadFixture } = require('../../deploymentUtils') -const { ZERO_ADDRESS } = require('../../utils') const ERC20BaseModuleCommon = require('../../common/ERC20BaseModuleCommon') const ERC20MintModuleCommon = require('../../common/ERC20MintModuleCommon') const ERC20BurnModuleCommon = require('../../common/ERC20BurnModuleCommon') @@ -38,5 +36,5 @@ describe('CMTAT Disable Allowlist- Standalone', function () { // Extensions DocumentModuleCommon() ExtraInfoModuleCommon() - ERC20EnforcementModuleCommon + ERC20EnforcementModuleCommon() }) diff --git a/test/deployment/allowlist/deploymentUpgradeableAllowlist.test.js b/test/deployment/allowlist/deploymentUpgradeableAllowlist.test.js index 37ef44cf..73cffaa0 100644 --- a/test/deployment/allowlist/deploymentUpgradeableAllowlist.test.js +++ b/test/deployment/allowlist/deploymentUpgradeableAllowlist.test.js @@ -1,10 +1,8 @@ -const { expect } = require('chai') const { deployCMTATAllowlistProxy, fixture, loadFixture } = require('../../deploymentUtils') -const { ZERO_ADDRESS } = require('../../utils') const ERC20BaseModuleCommon = require('../../common/ERC20BaseModuleCommon') const ERC20MintModuleCommon = require('../../common/ERC20MintModuleCommon') const ERC20BurnModuleCommon = require('../../common/ERC20BurnModuleCommon') diff --git a/test/deployment/debt/deploymentStandaloneDebt.test.js b/test/deployment/debt/deploymentStandaloneDebt.test.js index 25e7d911..0c6480fc 100644 --- a/test/deployment/debt/deploymentStandaloneDebt.test.js +++ b/test/deployment/debt/deploymentStandaloneDebt.test.js @@ -1,10 +1,8 @@ -const { expect } = require('chai') const { deployCMTATDebtStandalone, fixture, loadFixture } = require('../../deploymentUtils') -const { ZERO_ADDRESS } = require('../../utils') // Core const ERC20BaseModuleCommon = require('../../common/ERC20BaseModuleCommon') const ERC20MintModuleCommon = require('../../common/ERC20MintModuleCommon') diff --git a/test/deployment/debt/deploymentStandaloneDebtSnapshot.test.js b/test/deployment/debt/deploymentStandaloneDebtSnapshot.test.js index 7c69dd7b..e7d36566 100644 --- a/test/deployment/debt/deploymentStandaloneDebtSnapshot.test.js +++ b/test/deployment/debt/deploymentStandaloneDebtSnapshot.test.js @@ -1,10 +1,8 @@ -const { expect } = require('chai') const { deployCMTATDebtStandalone, fixture, loadFixture } = require('../../deploymentUtils') -const { ZERO_ADDRESS } = require('../../utils') // Core const ERC20BaseModuleCommon = require('../../common/ERC20BaseModuleCommon') const ERC20MintModuleCommon = require('../../common/ERC20MintModuleCommon') @@ -27,7 +25,7 @@ const SnapshotModuleMultiplePlannedTest = require('../../common/SnapshotModuleCo const SnapshotModuleOnePlannedSnapshotTest = require('../../common/SnapshotModuleCommon/global/SnapshotModuleOnePlannedSnapshotTest') const SnapshotModuleZeroPlannedSnapshotTest = require('../../common/SnapshotModuleCommon/global/SnapshotModuleZeroPlannedSnapshot') const SnapshotModuleSetSnapshotEngineCommon = require('../../common/SnapshotModuleCommon/SnapshotModuleSetSnapshotEngineCommon') -describe('CMTAT Debt - Standalone', function () { +describe('CMTAT Debt - Standalone Snapshot', function () { beforeEach(async function () { Object.assign(this, await loadFixture(fixture)) this.cmtat = await deployCMTATDebtStandalone( diff --git a/test/deployment/debt/deploymentUpgradeableDebt.test.js b/test/deployment/debt/deploymentUpgradeableDebt.test.js index 1c97246b..05732e18 100644 --- a/test/deployment/debt/deploymentUpgradeableDebt.test.js +++ b/test/deployment/debt/deploymentUpgradeableDebt.test.js @@ -1,10 +1,8 @@ -const { expect } = require('chai') const { deployCMTATDebtProxy, fixture, loadFixture } = require('../../deploymentUtils') -const { ZERO_ADDRESS } = require('../../utils') // Core const ERC20BaseModuleCommon = require('../../common/ERC20BaseModuleCommon') const ERC20MintModuleCommon = require('../../common/ERC20MintModuleCommon') diff --git a/test/deployment/debt/deploymentUpgradeableDebtSnapshot.test.js b/test/deployment/debt/deploymentUpgradeableDebtSnapshot.test.js index 5271172c..b34d8c7e 100644 --- a/test/deployment/debt/deploymentUpgradeableDebtSnapshot.test.js +++ b/test/deployment/debt/deploymentUpgradeableDebtSnapshot.test.js @@ -1,10 +1,8 @@ -const { expect } = require('chai') const { deployCMTATDebtProxy, fixture, loadFixture } = require('../../deploymentUtils') -const { ZERO_ADDRESS } = require('../../utils') // Core const ERC20BaseModuleCommon = require('../../common/ERC20BaseModuleCommon') const ERC20MintModuleCommon = require('../../common/ERC20MintModuleCommon') diff --git a/test/deployment/debtEngine/deploymentStandaloneDebtEngine.test.js b/test/deployment/debtEngine/deploymentStandaloneDebtEngine.test.js index 9cade26b..b372d78b 100644 --- a/test/deployment/debtEngine/deploymentStandaloneDebtEngine.test.js +++ b/test/deployment/debtEngine/deploymentStandaloneDebtEngine.test.js @@ -1,10 +1,8 @@ -const { expect } = require('chai') const { deployCMTATDebtEngineStandalone, fixture, loadFixture } = require('../../deploymentUtils') -const { ZERO_ADDRESS } = require('../../utils') // Core const ERC20BaseModuleCommon = require('../../common/ERC20BaseModuleCommon') const ERC20MintModuleCommon = require('../../common/ERC20MintModuleCommon') diff --git a/test/deployment/debtEngine/deploymentStandaloneDebtEngineSnapshot.test .js b/test/deployment/debtEngine/deploymentStandaloneDebtEngineSnapshot.test.js similarity index 97% rename from test/deployment/debtEngine/deploymentStandaloneDebtEngineSnapshot.test .js rename to test/deployment/debtEngine/deploymentStandaloneDebtEngineSnapshot.test.js index 422b6acf..38f37ef0 100644 --- a/test/deployment/debtEngine/deploymentStandaloneDebtEngineSnapshot.test .js +++ b/test/deployment/debtEngine/deploymentStandaloneDebtEngineSnapshot.test.js @@ -1,10 +1,8 @@ -const { expect } = require('chai') const { deployCMTATDebtEngineStandalone, fixture, loadFixture } = require('../../deploymentUtils') -const { ZERO_ADDRESS } = require('../../utils') // Core const ERC20BaseModuleCommon = require('../../common/ERC20BaseModuleCommon') const ERC20MintModuleCommon = require('../../common/ERC20MintModuleCommon') diff --git a/test/deployment/debtEngine/deploymentUpgradeableDebtEngine.test.js b/test/deployment/debtEngine/deploymentUpgradeableDebtEngine.test.js index c5536211..45f180e1 100644 --- a/test/deployment/debtEngine/deploymentUpgradeableDebtEngine.test.js +++ b/test/deployment/debtEngine/deploymentUpgradeableDebtEngine.test.js @@ -1,10 +1,8 @@ -const { expect } = require('chai') const { deployCMTATDebtEngineProxy, fixture, loadFixture } = require('../../deploymentUtils') -const { ZERO_ADDRESS } = require('../../utils') // Core const ERC20BaseModuleCommon = require('../../common/ERC20BaseModuleCommon') const ERC20MintModuleCommon = require('../../common/ERC20MintModuleCommon') @@ -18,7 +16,6 @@ const ERC20EnforcementModuleCommon = require('../../common/ERC20EnforcementModul const DocumentModuleCommon = require('../../common/DocumentModule/DocumentModuleCommon') const ExtraInfoModuleCommon = require('../../common/ExtraInfoModuleCommon') // debt -const DebtModuleCommon = require('../../common/DebtModule/DebtModuleCommon') const DebtModuleSetDebtEngineCommon = require('../../common/DebtModule/DebtModuleSetDebtEngineCommon') const DebtEngineModuleCommon = require('../../common/DebtModule/DebtEngineModuleCommon') diff --git a/test/deployment/deploymentUpgradeableUUPS.test.js b/test/deployment/deploymentUpgradeableUUPS.test.js index 5302ad14..94936e39 100644 --- a/test/deployment/deploymentUpgradeableUUPS.test.js +++ b/test/deployment/deploymentUpgradeableUUPS.test.js @@ -1,4 +1,3 @@ -const { expect } = require('chai') const { deployCMTATUUPSProxy, fixture, @@ -19,7 +18,6 @@ const ExtraInfoModuleCommon = require('../common/ExtraInfoModuleCommon') const ERC20CrossChainModuleCommon = require('../common/ERC20CrossChainModuleCommon') const CCIPModuleCommon = require('../common/CCIPModuleCommon') -const VALUE = 20n describe('CMTAT UUPS', function () { beforeEach(async function () { Object.assign(this, await loadFixture(fixture)) diff --git a/test/deployment/deploymentUpgradeableUUPSManual.test.js b/test/deployment/deploymentUpgradeableUUPSManual.test.js index 3453e1b1..a174b2b5 100644 --- a/test/deployment/deploymentUpgradeableUUPSManual.test.js +++ b/test/deployment/deploymentUpgradeableUUPSManual.test.js @@ -1,8 +1,6 @@ -const { expect } = require('chai') const { DEPLOYMENT_DECIMAL, TERMS, - deployCMTATUUPSProxy, fixture, loadFixture } = require('../deploymentUtils') @@ -22,7 +20,6 @@ const ExtraInfoModuleCommon = require('../common/ExtraInfoModuleCommon') const ERC20CrossChainModuleCommon = require('../common/ERC20CrossChainModuleCommon') const CCIPModuleCommon = require('../common/CCIPModuleCommon') -const VALUE = 20n describe('CMTAT UUPS - Manual Deployment', function () { beforeEach(async function () { Object.assign(this, await loadFixture(fixture)) diff --git a/test/deployment/erc721mock.test.js b/test/deployment/erc721mock.test.js index 138b6255..198078db 100644 --- a/test/deployment/erc721mock.test.js +++ b/test/deployment/erc721mock.test.js @@ -61,9 +61,13 @@ describe('ERC721MockUpgradeable', function () { it('testFailedTransferIfNotOwned', async function () { await this.cmtat.mint(this.address1, 1) - this.cmtat - .connect(this.address2) - .transferFrom(this.address1, this.address2, 1) + await expect( + this.cmtat + .connect(this.address2) + .transferFrom(this.address1, this.address2, 1) + ) + .to.be.revertedWithCustomError(this.cmtat, 'ERC721InsufficientApproval') + .withArgs(this.address2.address, 1) }) it('testApprovedAndTransfer', async function () { diff --git a/test/deployment/erc7551/deploymentERC7551Standalone.test.js b/test/deployment/erc7551/deploymentERC7551Standalone.test.js index 87ea99ab..e7e25581 100644 --- a/test/deployment/erc7551/deploymentERC7551Standalone.test.js +++ b/test/deployment/erc7551/deploymentERC7551Standalone.test.js @@ -1,4 +1,3 @@ -const { expect } = require('chai') const { deployCMTATERC7551Standalone, fixture, diff --git a/test/deployment/erc7551/deploymentERC7551Upgradeable.test.js b/test/deployment/erc7551/deploymentERC7551Upgradeable.test.js index a21d2bc7..0679d4be 100644 --- a/test/deployment/erc7551/deploymentERC7551Upgradeable.test.js +++ b/test/deployment/erc7551/deploymentERC7551Upgradeable.test.js @@ -1,4 +1,3 @@ -const { expect } = require('chai') const { deployCMTATERC7551Proxy, fixture, @@ -21,7 +20,6 @@ const ERC20CrossChainModuleCommon = require('../../common/ERC20CrossChainModuleC const ERC7551ModuleCommon = require('../../common/ERC7551ModuleCommon') const CCIPModuleCommon = require('../../common/CCIPModuleCommon') -const VALUE = 20n describe('CMTAT - ERC-7551 Proxy Deployment', function () { beforeEach(async function () { Object.assign(this, await loadFixture(fixture)) diff --git a/test/deployment/light/deploymentStandaloneLight.test.js b/test/deployment/light/deploymentStandaloneLight.test.js index 6424bdaa..1843de58 100644 --- a/test/deployment/light/deploymentStandaloneLight.test.js +++ b/test/deployment/light/deploymentStandaloneLight.test.js @@ -112,12 +112,13 @@ describe('CMTAT Core - Standalone', function () { /* ============ ERC165 ============ */ it('testSupportRightInterface', async function () { const erc1363Interface = '0xb0202a11' - // don't really know how to compute this easily // type(IAccessControl).interfaceId const IERC165Interface = '0x01ffc9a7' const IERC721Interface = '0x80ac58cd' const IERC5679 = '0xd0017968' const ICMTATDeactivate = '0xe9cd80b0' + const IERC1404 = '0xab84a5c8' + const IERC1404Extend = '0x78a8de7d' // Assert expect(await this.cmtat.supportsInterface(erc1363Interface)).to.equal( false @@ -128,6 +129,9 @@ describe('CMTAT Core - Standalone', function () { ) expect(await this.cmtat.supportsInterface(IERC5679)).to.equal(true) expect(await this.cmtat.supportsInterface(ICMTATDeactivate)).to.equal(true) + // The light deployment (CMTATBaseCore) does not include the ERC-1404 module + expect(await this.cmtat.supportsInterface(IERC1404)).to.equal(false) + expect(await this.cmtat.supportsInterface(IERC1404Extend)).to.equal(false) expect(await this.cmtat.supportsInterface('0xffffffff')).to.equal(false) }) }) diff --git a/test/deployment/light/deploymentUpgradeableLight.test.js b/test/deployment/light/deploymentUpgradeableLight.test.js index 588a7307..e8c87fad 100644 --- a/test/deployment/light/deploymentUpgradeableLight.test.js +++ b/test/deployment/light/deploymentUpgradeableLight.test.js @@ -11,7 +11,6 @@ const EnforcementModuleCommon = require('../../common/EnforcementModuleCommon') const VersionModuleCommon = require('../../common/VersionModuleCommon') const PauseModuleCommon = require('../../common/PauseModuleCommon') const ValidationModuleCommonCore = require('../../common/ValidationModule/ValidationModuleCommonCore') -const VALUE = 20n describe('CMTAT Core - Upgradeable', function () { beforeEach(async function () { Object.assign(this, await loadFixture(fixture)) @@ -37,12 +36,17 @@ describe('CMTAT Core - Upgradeable', function () { const IERC721Interface = '0x80ac58cd' const IERC5679 = '0xd0017968' const ICMTATDeactivate = '0xe9cd80b0' + const IERC1404 = '0xab84a5c8' + const IERC1404Extend = '0x78a8de7d' expect(await this.cmtat.supportsInterface(erc1363Interface)).to.equal(false) expect(await this.cmtat.supportsInterface(IERC165Interface)).to.equal(true) expect(await this.cmtat.supportsInterface(IERC721Interface)).to.equal(false) expect(await this.cmtat.supportsInterface(IERC5679)).to.equal(true) expect(await this.cmtat.supportsInterface(ICMTATDeactivate)).to.equal(true) + // The light deployment (CMTATBaseCore) does not include the ERC-1404 module + expect(await this.cmtat.supportsInterface(IERC1404)).to.equal(false) + expect(await this.cmtat.supportsInterface(IERC1404Extend)).to.equal(false) expect(await this.cmtat.supportsInterface('0xffffffff')).to.equal(false) }) }) diff --git a/test/deploymentUtils.js b/test/deploymentUtils.js index 65691e62..cfae2e2f 100644 --- a/test/deploymentUtils.js +++ b/test/deploymentUtils.js @@ -369,29 +369,6 @@ async function deployCMTATDebtEngineProxy (_, admin, deployerAddress) { return ETHERS_CMTAT_PROXY } -async function deployCMTATDebtProxy (_, admin, deployerAddress) { - // Ref: https://forum.openzeppelin.com/t/upgrades-hardhat-truffle5/30883/3 - const ETHERS_CMTAT_PROXY_FACTORY = await ethers.getContractFactory( - 'CMTATUpgradeableDebt' - ) - const ETHERS_CMTAT_PROXY = await upgrades.deployProxy( - ETHERS_CMTAT_PROXY_FACTORY, - [ - admin, - ['CMTA Token', 'CMTAT', DEPLOYMENT_DECIMAL], - ['CMTAT_ISIN', TERMS, 'CMTAT_info'], - [ZERO_ADDRESS] - ], - { - initializer: 'initialize', - constructorArgs: [], - from: deployerAddress, - unsafeAllow: ['missing-initializer'] - } - ) - return ETHERS_CMTAT_PROXY -} - async function deployCMTATUUPSProxy (_, admin, deployerAddress) { // Ref: https://forum.openzeppelin.com/t/upgrades-hardhat-truffle5/30883/3 const ETHERS_CMTAT_PROXY_FACTORY = await ethers.getContractFactory( @@ -446,7 +423,6 @@ async function deployCMTATProxyWithParameter ( unsafeAllow: ['missing-initializer'] } ) - // return ETHERS_CMTAT_PROXY.getAddress() return ETHERS_CMTAT_PROXY } diff --git a/test/proxy/general/UpgradeProxy.test.js b/test/proxy/general/UpgradeProxy.test.js index 29ca8981..cdaef162 100644 --- a/test/proxy/general/UpgradeProxy.test.js +++ b/test/proxy/general/UpgradeProxy.test.js @@ -1,4 +1,3 @@ -const { expect } = require('chai') const { ZERO_ADDRESS } = require('../../utils') const UpgradeProxyCommon = require('./UpgradeProxyCommon') const { diff --git a/test/proxy/general/UpgradeProxyCommon.js b/test/proxy/general/UpgradeProxyCommon.js index b5a8f3fa..98ae6672 100644 --- a/test/proxy/general/UpgradeProxyCommon.js +++ b/test/proxy/general/UpgradeProxyCommon.js @@ -1,5 +1,5 @@ const { expect } = require('chai') -const { ethers, upgrades } = require('hardhat') +const { upgrades } = require('hardhat') function UpgradeProxyCommon () { /* * Functions used: balanceOf, totalSupply, mint @@ -61,8 +61,6 @@ function UpgradeProxyCommon () { IMPLEMENTATION_CONTRACT_ADDRESS_V2 ); - ({ logs: this.logs1 } = await CMTAT_PROXY_V2.balanceOf(this.address1)) - expect(await CMTAT_PROXY_V2.balanceOf(this.address1)).to.equal(20); // keep the storage diff --git a/test/proxy/general/UpgradeProxyUUPS.test.js b/test/proxy/general/UpgradeProxyUUPS.test.js index ed9b5a98..880e9c91 100644 --- a/test/proxy/general/UpgradeProxyUUPS.test.js +++ b/test/proxy/general/UpgradeProxyUUPS.test.js @@ -14,7 +14,7 @@ describe('CMTAT with UUPS Proxy', function () { Object.assign(this, await loadFixture(fixture)) // Deploy the implementation contract - CMTAT_PROXY_FACTORY = await ethers.getContractFactory( + const CMTAT_PROXY_FACTORY = await ethers.getContractFactory( 'CMTATUpgradeableUUPS' ) this.CMTAT_PROXY_TestFactory = await ethers.getContractFactory( diff --git a/test/proxy/modules/CMTATIntegration.test.js b/test/proxy/modules/CMTATIntegration.test.js index 4d1ee79e..f1dc7028 100644 --- a/test/proxy/modules/CMTATIntegration.test.js +++ b/test/proxy/modules/CMTATIntegration.test.js @@ -4,8 +4,7 @@ const { fixture, loadFixture } = require('../../deploymentUtils') -const VALUE1 = 20n -describe('Proxy - ERC20BurnModule', function () { +describe('Proxy - CMTATIntegration', function () { beforeEach(async function () { Object.assign(this, await loadFixture(fixture)) this.cmtat = await deployCMTATProxy( diff --git a/test/proxy/modules/ERC20CrossChainModule.test.js b/test/proxy/modules/ERC20CrossChainModule.test.js index 019a2a97..a58e4542 100644 --- a/test/proxy/modules/ERC20CrossChainModule.test.js +++ b/test/proxy/modules/ERC20CrossChainModule.test.js @@ -4,7 +4,7 @@ const { fixture, loadFixture } = require('../../deploymentUtils') -describe('Standard - ERC20CrossChain', function () { +describe('Proxy - ERC20CrossChain', function () { beforeEach(async function () { Object.assign(this, await loadFixture(fixture)) this.cmtat = await deployCMTATProxy( diff --git a/test/proxy/modules/ExtraInfoModule.test.js b/test/proxy/modules/ExtraInfoModule.test.js index f9792a4f..b5e2287d 100644 --- a/test/proxy/modules/ExtraInfoModule.test.js +++ b/test/proxy/modules/ExtraInfoModule.test.js @@ -5,7 +5,7 @@ const { loadFixture } = require('../../deploymentUtils') -describe('Proxy - BaseModule', function () { +describe('Proxy - ExtraInfoModule', function () { beforeEach(async function () { Object.assign(this, await loadFixture(fixture)) this.cmtat = await deployCMTATProxy( diff --git a/test/standard/modules/CCIPModule.test.js b/test/standard/modules/CCIPModule.test.js index ebb93255..7167d8bb 100644 --- a/test/standard/modules/CCIPModule.test.js +++ b/test/standard/modules/CCIPModule.test.js @@ -1,7 +1,6 @@ const CCIPModuleCommon = require('../../common/CCIPModuleCommon') const { deployCMTATStandalone, - DEPLOYMENT_FLAG, fixture, loadFixture } = require('../../deploymentUtils') diff --git a/test/standard/modules/ExtraInfoModule.test.js b/test/standard/modules/ExtraInfoModule.test.js index 3ea5a9fd..4da8ba8e 100644 --- a/test/standard/modules/ExtraInfoModule.test.js +++ b/test/standard/modules/ExtraInfoModule.test.js @@ -1,14 +1,12 @@ const ExtraInfoModuleCommon = require('../../common/ExtraInfoModuleCommon') const { deployCMTATStandalone, - DEPLOYMENT_FLAG, fixture, loadFixture } = require('../../deploymentUtils') describe('Standard - ExtraInfoModule', function () { beforeEach(async function () { Object.assign(this, await loadFixture(fixture)) - this.flag = DEPLOYMENT_FLAG // value used in tests this.cmtat = await deployCMTATStandalone( this._.address, this.admin.address, diff --git a/test/standard/modules/MetaTxModuleAllowlist.test.js b/test/standard/modules/MetaTxModuleAllowlist.test.js index c8dde59a..08156056 100644 --- a/test/standard/modules/MetaTxModuleAllowlist.test.js +++ b/test/standard/modules/MetaTxModuleAllowlist.test.js @@ -5,7 +5,7 @@ const { loadFixture } = require('../../deploymentUtils.js') const { ERC2771ForwarderDomain } = require('../../utils.js') -describe('Standard - MetaTxModule', function () { +describe('Standard - MetaTxModule Allowlist', function () { beforeEach(async function () { Object.assign(this, await loadFixture(fixture)) this.forwarder = await ethers.deployContract('MinimalForwarderMock') diff --git a/test/standard/modules/MetaTxModuleERC1363.test.js b/test/standard/modules/MetaTxModuleERC1363.test.js index b1ad1c3c..c530dab4 100644 --- a/test/standard/modules/MetaTxModuleERC1363.test.js +++ b/test/standard/modules/MetaTxModuleERC1363.test.js @@ -4,8 +4,8 @@ const { fixture, loadFixture } = require('../../deploymentUtils.js') -const { ZERO_ADDRESS, ERC2771ForwarderDomain } = require('../../utils.js') -describe('Standard - MetaTxModule', function () { +const { ERC2771ForwarderDomain } = require('../../utils.js') +describe('Standard - MetaTxModule ERC1363', function () { beforeEach(async function () { Object.assign(this, await loadFixture(fixture)) this.forwarder = await ethers.deployContract('MinimalForwarderMock') diff --git a/test/standard/modules/MetaTxModuleUUPS.test.js b/test/standard/modules/MetaTxModuleUUPS.test.js index 83790184..186900fd 100644 --- a/test/standard/modules/MetaTxModuleUUPS.test.js +++ b/test/standard/modules/MetaTxModuleUUPS.test.js @@ -4,8 +4,8 @@ const { fixture, loadFixture } = require('../../deploymentUtils.js') -const { ZERO_ADDRESS, ERC2771ForwarderDomain } = require('../../utils.js') -describe('Standard - MetaTxModule', function () { +const { ERC2771ForwarderDomain } = require('../../utils.js') +describe('Standard - MetaTxModule UUPS', function () { beforeEach(async function () { Object.assign(this, await loadFixture(fixture)) this.forwarder = await ethers.deployContract('MinimalForwarderMock') diff --git a/test/standard/modules/SnapshotModule/SnapshotModule.test.js b/test/standard/modules/SnapshotModule/SnapshotModule.test.js index d5f792db..005c6786 100644 --- a/test/standard/modules/SnapshotModule/SnapshotModule.test.js +++ b/test/standard/modules/SnapshotModule/SnapshotModule.test.js @@ -12,7 +12,7 @@ const { fixture, loadFixture } = require('../../../deploymentUtils') -describe('Snapshot Deployment - Standalone', function () { +describe('Standard - SnapshotModule', function () { beforeEach(async function () { Object.assign(this, await loadFixture(fixture)) this.cmtat = await deployCMTATSnapshotStandalone( diff --git a/test/standard/modules/ValidationModule/ValidationModuleConstructor.test.js b/test/standard/modules/ValidationModule/ValidationModuleConstructor.test.js index 3b954a0e..0629b816 100644 --- a/test/standard/modules/ValidationModule/ValidationModuleConstructor.test.js +++ b/test/standard/modules/ValidationModule/ValidationModuleConstructor.test.js @@ -7,7 +7,6 @@ const { DEPLOYMENT_DECIMAL } = require('../../../deploymentUtils') -const { ZERO_ADDRESS } = require('../../../utils') describe('Standard - ValidationModule - Constructor', function () { beforeEach(async function () { diff --git a/test/utils.js b/test/utils.js index 93489d3b..de136fab 100644 --- a/test/utils.js +++ b/test/utils.js @@ -1,8 +1,11 @@ module.exports = { + // Expected contract version (see contracts/modules/wrapper/core/VersionModule.sol). + // Bump here together with the contract on a version change. + VERSION: '3.2.0', MINTER_ROLE: '0x9f2df0fed2c77648de5860a4cc508cd0818c85b8b8a1ab4ceeef8d981c8956a6', // keccak256("MINTER_ROLE"); BURNER_SELF_ROLE: - '0x13d9f3ea33477b975af6cd01437366c28412d5bd9b872fa0fc25bd3a160683af', // keccak256("BURNER_SELF_ROLE");, + '0x13d9f3ea33477b975af6cd01437366c28412d5bd9b872fa0fc25bd3a160683af', // keccak256("BURNER_SELF_ROLE"); BURNER_ROLE: '0x3c11d16cbaffd01df69ce1c404f6340ee057498f5f00246190ea54220576a848', // keccak256("BURNER_ROLE"); BURNER_FROM_ROLE: @@ -51,13 +54,14 @@ module.exports = { REJECTED_CODE_BASE_TRANSFER_REJECTED_FROM_INSUFFICIENT_ACTIVE_BALANCE: 6, // ERC-165 interface ID - IERC165_INTERFACEID : '0x01ffc9a7', - IERC721_INTERFACEID : '0x80ac58cd', - IACCESSCONTROL_INTERFACEID : '0x7965db0b', - IRULEENGINE_INTERFACEID : '0x20c49ce7', - IERC1404EXTEND_INTERFACEID : '0x78a8de7d', - IERC5679_INTERFACEID : '0xd0017968', - IERC1363_INTERFACEID : '0xb0202a11', - IERC7943_INTERFACEID: `0x3edbb4c4`, + IERC165_INTERFACEID: '0x01ffc9a7', + IERC721_INTERFACEID: '0x80ac58cd', + IACCESSCONTROL_INTERFACEID: '0x7965db0b', + IRULEENGINE_INTERFACEID: '0x20c49ce7', + IERC1404_INTERFACEID: '0xab84a5c8', + IERC1404EXTEND_INTERFACEID: '0x78a8de7d', + IERC5679_INTERFACEID: '0xd0017968', + IERC1363_INTERFACEID: '0xb0202a11', + IERC7943_INTERFACEID: '0x3edbb4c4', ICMTATDEACTIVATE_INTERFACEID: '0xe9cd80b0' }