From 4a12bd52b895c9657e93f06e8ecd057a39496efc Mon Sep 17 00:00:00 2001 From: Dhruv Gupta Date: Fri, 13 Feb 2026 14:53:04 +0530 Subject: [PATCH 1/7] Append fix interfaces and wrapper --- .../engine/IFixDescriptorEngine.sol | 79 ++++++++++++++++ .../modules/IFixDescriptorEngineModule.sol | 41 ++++++++ .../extensions/FixDescriptorEngineModule.sol | 93 +++++++++++++++++++ 3 files changed, 213 insertions(+) create mode 100644 contracts/interfaces/engine/IFixDescriptorEngine.sol create mode 100644 contracts/interfaces/modules/IFixDescriptorEngineModule.sol create mode 100644 contracts/modules/wrapper/extensions/FixDescriptorEngineModule.sol diff --git a/contracts/interfaces/engine/IFixDescriptorEngine.sol b/contracts/interfaces/engine/IFixDescriptorEngine.sol new file mode 100644 index 00000000..7fac855a --- /dev/null +++ b/contracts/interfaces/engine/IFixDescriptorEngine.sol @@ -0,0 +1,79 @@ +// SPDX-License-Identifier: MPL-2.0 + +pragma solidity ^0.8.20; + +/* ==== FixDescriptorKit === */ +import "@fixdescriptorkit/contracts/src/IFixDescriptor.sol"; + +/** + * @title IFixDescriptorEngine + * @notice Interface for FIX Descriptor Engine contract + * @dev Engine contract that manages FIX descriptor for a single bound token + * Following CMTAT engine pattern similar to ISnapshotEngine + * One engine instance is bound to one token at construction time + */ +interface IFixDescriptorEngine { + /** + * @notice Get the address of the bound token + * @return token The address of the token this engine is bound to + */ + function token() external view returns (address); + + /** + * @notice Get the complete FIX descriptor for the bound token + * @return descriptor The FixDescriptor struct + */ + function getFixDescriptor() external view returns (IFixDescriptor.FixDescriptor memory descriptor); + + /** + * @notice Get the Merkle root commitment for the bound token + * @return root The fixRoot for verification + */ + function getFixRoot() external view returns (bytes32 root); + + /** + * @notice Verify a specific field against the committed descriptor for the bound token + * @param pathSBE SBE-encoded bytes of the field path + * @param value Raw FIX value bytes + * @param proof Merkle proof (sibling hashes) + * @param directions Direction array (true=right child, false=left child) + * @return valid True if the proof is valid + */ + function verifyField( + bytes calldata pathSBE, + bytes calldata value, + bytes32[] calldata proof, + bool[] calldata directions + ) external view returns (bool valid); + + /** + * @notice Get SBE data chunk from SSTORE2 storage for the bound token + * @param start Start offset (in the data, not including STOP byte) + * @param size Number of bytes to read + * @return chunk The requested SBE data + */ + function getFixSBEChunk( + uint256 start, + uint256 size + ) external view returns (bytes memory chunk); + + /** + * @notice Set or update the FIX descriptor for the bound token + * @dev Can only be called by authorized roles + * @param descriptor The complete FixDescriptor struct + */ + function setFixDescriptor(IFixDescriptor.FixDescriptor calldata descriptor) external; + + /** + * @notice Deploy SBE data and set descriptor in one transaction + * @dev Deploys SBE data using SSTORE2 pattern, then sets the descriptor + * This is a convenience function that combines deployment and descriptor setting + * @param sbeData Raw SBE-encoded data to deploy + * @param descriptor Descriptor struct (fixSBEPtr and fixSBELen will be set automatically) + * @return sbePtr Address of the deployed SBE data contract + */ + function setFixDescriptorWithSBE( + bytes memory sbeData, + IFixDescriptor.FixDescriptor memory descriptor + ) external returns (address sbePtr); +} diff --git a/contracts/interfaces/modules/IFixDescriptorEngineModule.sol b/contracts/interfaces/modules/IFixDescriptorEngineModule.sol new file mode 100644 index 00000000..a6b69ba9 --- /dev/null +++ b/contracts/interfaces/modules/IFixDescriptorEngineModule.sol @@ -0,0 +1,41 @@ +// SPDX-License-Identifier: MPL-2.0 + +pragma solidity ^0.8.20; + +/* ==== Engine === */ +import {IFixDescriptorEngine} from "../engine/IFixDescriptorEngine.sol"; + +/** + * @title IFixDescriptorEngineModule + * @notice Minimal interface for integrating a FIX descriptor engine module. + * @dev Provides methods to set and retrieve a FIX descriptor engine used for managing FIX descriptors. + */ +interface IFixDescriptorEngineModule { + /* ============ Events ============ */ + /** + * @notice Emitted when a new FIX descriptor engine is set. + * @param newFixDescriptorEngine The address of the newly assigned FIX descriptor engine contract. + */ + event FixDescriptorEngine(IFixDescriptorEngine indexed newFixDescriptorEngine); + /* ============ Error ============ */ + /** + * @dev Reverts if the new FIX descriptor engine is the same as the current one. + */ + error CMTAT_FixDescriptorModule_SameValue(); + /* ============ Functions ============ */ + /** + * @notice Sets the address of the FIX descriptor engine contract. + * @dev The FIX descriptor engine is responsible for managing FIX descriptors and SBE data. + * Emits a {FixDescriptorEngine} event. + * Reverts with {CMTAT_FixDescriptorModule_SameValue} if the new engine is the same as the current one. + * @param fixDescriptorEngine_ The new FIX descriptor engine contract address to set. + */ + function setFixDescriptorEngine( + IFixDescriptorEngine fixDescriptorEngine_ + ) external; + /** + * @notice Returns the currently set FIX descriptor engine. + * @return The address of the active FIX descriptor engine contract. + */ + function fixDescriptorEngine() external view returns (IFixDescriptorEngine); +} diff --git a/contracts/modules/wrapper/extensions/FixDescriptorEngineModule.sol b/contracts/modules/wrapper/extensions/FixDescriptorEngineModule.sol new file mode 100644 index 00000000..5e339448 --- /dev/null +++ b/contracts/modules/wrapper/extensions/FixDescriptorEngineModule.sol @@ -0,0 +1,93 @@ +// SPDX-License-Identifier: MPL-2.0 + +pragma solidity ^0.8.20; + +/* ==== OpenZeppelin === */ +import {Initializable} from "@openzeppelin/contracts/proxy/utils/Initializable.sol"; +/* ==== Engine === */ +import {IFixDescriptorEngine, IFixDescriptorEngineModule} from "../../../interfaces/modules/IFixDescriptorEngineModule.sol"; + +abstract contract FixDescriptorEngineModule is Initializable, IFixDescriptorEngineModule { + /* ============ State Variables ============ */ + bytes32 public constant DESCRIPTOR_ENGINE_ROLE = keccak256("DESCRIPTOR_ENGINE_ROLE"); + + /* ============ ERC-7201 ============ */ + // keccak256(abi.encode(uint256(keccak256("CMTAT.storage.FixDescriptorEngineModule")) - 1)) & ~bytes32(uint256(0xff)) + bytes32 private constant FixDescriptorEngineModuleStorageLocation = 0xc09aa28957960c2b82e3fad477567fe122f23bca69560181151331ec7041c600; + /* ==== ERC-7201 State Variables === */ + struct FixDescriptorEngineModuleStorage { + IFixDescriptorEngine _fixDescriptorEngine; + } + + /* ============ Modifier ============ */ + modifier onlyDescriptorEngine() { + _authorizeFixDescriptorEngine(); + _; + } + /* ============ Initializer Function ============ */ + /** + * @dev + * + * - The grant to the admin role is done by AccessControlDefaultAdminRules + * - The control of the zero address is done by AccessControlDefaultAdminRules + * + */ + function __FixDescriptorEngineModule_init_unchained(IFixDescriptorEngine fixDescriptorEngine_) + internal virtual onlyInitializing { + if (address(fixDescriptorEngine_) != address (0)) { + FixDescriptorEngineModuleStorage storage $ = _getFixDescriptorEngineModuleStorage(); + _setFixDescriptorEngine($, fixDescriptorEngine_); + } + } + + + /*////////////////////////////////////////////////////////////// + PUBLIC/EXTERNAL FUNCTIONS + //////////////////////////////////////////////////////////////*/ + + /* ============ State Restricted Functions ============ */ + /** + * @inheritdoc IFixDescriptorEngineModule + * @custom:access-control + * - The caller must have the `DESCRIPTOR_ENGINE_ROLE`. + */ + function setFixDescriptorEngine( + IFixDescriptorEngine fixDescriptorEngine_ + ) public virtual override(IFixDescriptorEngineModule) onlyDescriptorEngine { + FixDescriptorEngineModuleStorage storage $ = _getFixDescriptorEngineModuleStorage(); + require($._fixDescriptorEngine != fixDescriptorEngine_, CMTAT_FixDescriptorModule_SameValue()); + _setFixDescriptorEngine($, fixDescriptorEngine_); + } + + + /* ============ View functions ============ */ + + /** + * @inheritdoc IFixDescriptorEngineModule + */ + function fixDescriptorEngine() public view virtual override(IFixDescriptorEngineModule) returns (IFixDescriptorEngine) { + FixDescriptorEngineModuleStorage storage $ = _getFixDescriptorEngineModuleStorage(); + return $._fixDescriptorEngine; + } + /*////////////////////////////////////////////////////////////// + INTERNAL/PRIVATE FUNCTIONS + //////////////////////////////////////////////////////////////*/ + + function _setFixDescriptorEngine( + FixDescriptorEngineModuleStorage storage $, IFixDescriptorEngine fixDescriptorEngine_ + ) internal virtual { + $._fixDescriptorEngine = fixDescriptorEngine_; + emit FixDescriptorEngine(fixDescriptorEngine_); + } + + /* ============ Access Control ============ */ + function _authorizeFixDescriptorEngine() internal virtual; + /* ============ ERC-7201 ============ */ + function _getFixDescriptorEngineModuleStorage() private pure returns (FixDescriptorEngineModuleStorage storage $) { + assembly { + $.slot := FixDescriptorEngineModuleStorageLocation + } + } + + +} From f73c813a37f2568882b1e91ae22d2260334949a2 Mon Sep 17 00:00:00 2001 From: Dhruv Gupta Date: Fri, 13 Feb 2026 15:26:01 +0530 Subject: [PATCH 2/7] add engine tests --- .../allowlist/CMTATStandaloneAllowlist.sol | 12 ++- .../engine/IFixDescriptorEngine.sol | 75 +-------------- .../technical/ICMTATConstructor.sol | 2 + contracts/mocks/FixDescriptorEngineMock.sol | 92 ++++++++++++++++++ .../mocks/library/fix/IFixDescriptor.sol | 70 ++++++++++++++ contracts/modules/0_CMTATBaseCommon.sol | 13 ++- contracts/modules/1_CMTATBaseAllowlist.sol | 18 ++-- contracts/modules/1_CMTATBaseRuleEngine.sol | 2 +- .../FixDescriptorModuleCommon.js | 94 +++++++++++++++++++ ...iptorModuleSetFixDescriptorEngineCommon.js | 52 ++++++++++ test/deploymentUtils.js | 2 + .../FixDescriptorModule.test.js | 19 ++++ .../FixDescriptorModuleConstructor.test.js | 34 +++++++ .../FixDescriptorModule.test.js | 19 ++++ .../FixDescriptorModuleConstructor.test.js | 34 +++++++ test/utils.js | 2 + 16 files changed, 456 insertions(+), 84 deletions(-) create mode 100644 contracts/mocks/FixDescriptorEngineMock.sol create mode 100644 contracts/mocks/library/fix/IFixDescriptor.sol create mode 100644 test/common/FixDescriptorModule/FixDescriptorModuleCommon.js create mode 100644 test/common/FixDescriptorModule/FixDescriptorModuleSetFixDescriptorEngineCommon.js create mode 100644 test/proxy/modules/FixDescriptorModule/FixDescriptorModule.test.js create mode 100644 test/proxy/modules/FixDescriptorModule/FixDescriptorModuleConstructor.test.js create mode 100644 test/standard/modules/FixDescriptorModule/FixDescriptorModule.test.js create mode 100644 test/standard/modules/FixDescriptorModule/FixDescriptorModuleConstructor.test.js diff --git a/contracts/deployment/allowlist/CMTATStandaloneAllowlist.sol b/contracts/deployment/allowlist/CMTATStandaloneAllowlist.sol index 6209baad..646bc198 100644 --- a/contracts/deployment/allowlist/CMTATStandaloneAllowlist.sol +++ b/contracts/deployment/allowlist/CMTATStandaloneAllowlist.sol @@ -3,6 +3,7 @@ pragma solidity ^0.8.20; import {CMTATBaseAllowlist, ISnapshotEngine, IERC1643} from "../../modules/1_CMTATBaseAllowlist.sol"; +import {IFixDescriptorEngine} from "../../interfaces/engine/IFixDescriptorEngine.sol"; import {ERC2771Module, ERC2771ContextUpgradeable} from "../../modules/wrapper/options/ERC2771Module.sol"; import {ICMTATConstructor} from "../../interfaces/technical/ICMTATConstructor.sol"; @@ -16,7 +17,9 @@ contract CMTATStandaloneAllowlist is CMTATBaseAllowlist { * @param admin address of the admin of contract (Access Control) * @param ERC20Attributes_ ERC20 name, symbol and decimals * @param extraInformationAttributes_ tokenId, terms, information - * @param engines_ external contract + * @param snapshotEngine_ external contract + * @param documentEngine_ external contract + * @param fixDescriptorEngine_ external contract */ /// @custom:oz-upgrades-unsafe-allow constructor constructor( @@ -25,8 +28,8 @@ contract CMTATStandaloneAllowlist is CMTATBaseAllowlist { ICMTATConstructor.ERC20Attributes memory ERC20Attributes_, ICMTATConstructor.ExtraInformationAttributes memory extraInformationAttributes_, ISnapshotEngine snapshotEngine_, - IERC1643 documentEngine_ - + IERC1643 documentEngine_, + IFixDescriptorEngine fixDescriptorEngine_ ) ERC2771Module(forwarderIrrevocable){ // Initialize the contract to avoid front-running initialize( @@ -34,7 +37,8 @@ contract CMTATStandaloneAllowlist is CMTATBaseAllowlist { ERC20Attributes_, extraInformationAttributes_, snapshotEngine_, - documentEngine_ + documentEngine_, + fixDescriptorEngine_ ); } } diff --git a/contracts/interfaces/engine/IFixDescriptorEngine.sol b/contracts/interfaces/engine/IFixDescriptorEngine.sol index 7fac855a..a082858c 100644 --- a/contracts/interfaces/engine/IFixDescriptorEngine.sol +++ b/contracts/interfaces/engine/IFixDescriptorEngine.sol @@ -2,78 +2,9 @@ pragma solidity ^0.8.20; -/* ==== FixDescriptorKit === */ -import "@fixdescriptorkit/contracts/src/IFixDescriptor.sol"; - -/** - * @title IFixDescriptorEngine - * @notice Interface for FIX Descriptor Engine contract - * @dev Engine contract that manages FIX descriptor for a single bound token - * Following CMTAT engine pattern similar to ISnapshotEngine - * One engine instance is bound to one token at construction time +/* + * @dev minimum interface to define a FixDescriptorEngine */ interface IFixDescriptorEngine { - /** - * @notice Get the address of the bound token - * @return token The address of the token this engine is bound to - */ - function token() external view returns (address); - - /** - * @notice Get the complete FIX descriptor for the bound token - * @return descriptor The FixDescriptor struct - */ - function getFixDescriptor() external view returns (IFixDescriptor.FixDescriptor memory descriptor); - - /** - * @notice Get the Merkle root commitment for the bound token - * @return root The fixRoot for verification - */ - function getFixRoot() external view returns (bytes32 root); - - /** - * @notice Verify a specific field against the committed descriptor for the bound token - * @param pathSBE SBE-encoded bytes of the field path - * @param value Raw FIX value bytes - * @param proof Merkle proof (sibling hashes) - * @param directions Direction array (true=right child, false=left child) - * @return valid True if the proof is valid - */ - function verifyField( - bytes calldata pathSBE, - bytes calldata value, - bytes32[] calldata proof, - bool[] calldata directions - ) external view returns (bool valid); - - /** - * @notice Get SBE data chunk from SSTORE2 storage for the bound token - * @param start Start offset (in the data, not including STOP byte) - * @param size Number of bytes to read - * @return chunk The requested SBE data - */ - function getFixSBEChunk( - uint256 start, - uint256 size - ) external view returns (bytes memory chunk); - - /** - * @notice Set or update the FIX descriptor for the bound token - * @dev Can only be called by authorized roles - * @param descriptor The complete FixDescriptor struct - */ - function setFixDescriptor(IFixDescriptor.FixDescriptor calldata descriptor) external; - - /** - * @notice Deploy SBE data and set descriptor in one transaction - * @dev Deploys SBE data using SSTORE2 pattern, then sets the descriptor - * This is a convenience function that combines deployment and descriptor setting - * @param sbeData Raw SBE-encoded data to deploy - * @param descriptor Descriptor struct (fixSBEPtr and fixSBELen will be set automatically) - * @return sbePtr Address of the deployed SBE data contract - */ - function setFixDescriptorWithSBE( - bytes memory sbeData, - IFixDescriptor.FixDescriptor memory descriptor - ) external returns (address sbePtr); + // nothing more } diff --git a/contracts/interfaces/technical/ICMTATConstructor.sol b/contracts/interfaces/technical/ICMTATConstructor.sol index e91c3226..97b62e08 100644 --- a/contracts/interfaces/technical/ICMTATConstructor.sol +++ b/contracts/interfaces/technical/ICMTATConstructor.sol @@ -4,6 +4,7 @@ pragma solidity ^0.8.20; import {IRuleEngine} from "../engine/IRuleEngine.sol"; import {ISnapshotEngine} from "../engine/ISnapshotEngine.sol"; import {IDocumentEngine, IERC1643} from "../engine/IDocumentEngine.sol"; +import {IFixDescriptorEngine} from "../engine/IFixDescriptorEngine.sol"; import {IERC1643CMTAT} from "../tokenization/draft-IERC1643CMTAT.sol"; @@ -15,6 +16,7 @@ interface ICMTATConstructor { IRuleEngine ruleEngine; ISnapshotEngine snapshotEngine; IERC1643 documentEngine; + IFixDescriptorEngine fixDescriptorEngine; } struct ERC20Attributes { // token name, diff --git a/contracts/mocks/FixDescriptorEngineMock.sol b/contracts/mocks/FixDescriptorEngineMock.sol new file mode 100644 index 00000000..b93ef2f3 --- /dev/null +++ b/contracts/mocks/FixDescriptorEngineMock.sol @@ -0,0 +1,92 @@ +// SPDX-License-Identifier: MPL-2.0 +pragma solidity ^0.8.20; + +import {IFixDescriptorEngine} from "../interfaces/engine/IFixDescriptorEngine.sol"; +import {IFixDescriptor} from "./library/fix/IFixDescriptor.sol"; + +/* + * @title a FixDescriptorEngine mock for testing, not suitable for production + */ +contract FixDescriptorEngineMock is IFixDescriptorEngine, IFixDescriptor { + address public immutable token; + IFixDescriptor.FixDescriptor private _descriptor; + bool private _initialized; + bool private _verifyFieldResult; // for testing verification results + + constructor(address token_, address admin) { + token = token_; + // admin not used but kept for consistency with SnapshotEngineMock pattern + _verifyFieldResult = true; // default to true for tests + } + + /** + * @notice Set the descriptor for testing + * @param descriptor The FixDescriptor struct to store + */ + function setFixDescriptor(IFixDescriptor.FixDescriptor memory descriptor) external { + bytes32 oldRoot = _descriptor.fixRoot; + _descriptor = descriptor; + _initialized = true; + + if (oldRoot != bytes32(0)) { + emit FixDescriptorUpdated(oldRoot, descriptor.fixRoot, descriptor.fixSBEPtr); + } else { + emit FixDescriptorSet( + descriptor.fixRoot, + descriptor.dictHash, + descriptor.fixSBEPtr, + descriptor.fixSBELen + ); + } + } + + /** + * @notice Set the verifyField result for testing + * @param result The boolean result to return from verifyField + */ + function setVerifyFieldResult(bool result) external { + _verifyFieldResult = result; + } + + /** + * @notice Get the complete FIX descriptor + * @return descriptor The FixDescriptor struct + */ + function getFixDescriptor() external view override returns (IFixDescriptor.FixDescriptor memory descriptor) { + return _descriptor; + } + + /** + * @notice Get the Merkle root commitment + * @return root The fixRoot for verification + */ + function getFixRoot() external view override returns (bytes32 root) { + return _descriptor.fixRoot; + } + + /** + * @notice Verify a specific field against the committed descriptor + * @param pathSBE SBE-encoded bytes of the field path + * @param value Raw FIX value bytes + * @param proof Merkle proof (sibling hashes) + * @param directions Direction array (true=right child, false=left child) + * @return valid True if the proof is valid (returns configured test result) + */ + function verifyField( + bytes calldata pathSBE, + bytes calldata value, + bytes32[] calldata proof, + bool[] calldata directions + ) external view override returns (bool valid) { + // Return configured result for testing + return _verifyFieldResult; + } + + /** + * @notice Get the descriptor engine address + * @return engine Address of the FixDescriptorEngine contract (returns self) + */ + function getDescriptorEngine() external view override returns (address engine) { + return address(this); + } +} diff --git a/contracts/mocks/library/fix/IFixDescriptor.sol b/contracts/mocks/library/fix/IFixDescriptor.sol new file mode 100644 index 00000000..cb0eb052 --- /dev/null +++ b/contracts/mocks/library/fix/IFixDescriptor.sol @@ -0,0 +1,70 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +/** + * @title IFixDescriptor + * @notice Standard interface for assets with embedded FIX descriptors + * @dev Asset contracts (ERC20, ERC721, etc.) implement this to expose their FIX descriptor + * This is a local copy for testing purposes to avoid external package dependencies + */ +interface IFixDescriptor { + /// @notice FIX descriptor structure + struct FixDescriptor { + uint16 fixMajor; // FIX version major (e.g., 4) + uint16 fixMinor; // FIX version minor (e.g., 4) + bytes32 dictHash; // FIX dictionary/Orchestra hash + bytes32 fixRoot; // Merkle root commitment + address fixSBEPtr; // SSTORE2 data contract address + uint32 fixSBELen; // SBE data length + string schemaURI; // Optional SBE schema URI (ipfs:// or https://) + } + + /// @notice Emitted when descriptor is first set + event FixDescriptorSet( + bytes32 indexed fixRoot, + bytes32 indexed dictHash, + address fixSBEPtr, + uint32 fixSBELen + ); + + /// @notice Emitted when descriptor is updated + event FixDescriptorUpdated( + bytes32 indexed oldRoot, + bytes32 indexed newRoot, + address newPtr + ); + + /** + * @notice Get the complete FIX descriptor for this asset + * @return descriptor The FixDescriptor struct + */ + function getFixDescriptor() external view returns (FixDescriptor memory descriptor); + + /** + * @notice Get the Merkle root commitment + * @return root The fixRoot for verification + */ + function getFixRoot() external view returns (bytes32 root); + + /** + * @notice Verify a specific field against the committed descriptor + * @param pathSBE SBE-encoded bytes of the field path + * @param value Raw FIX value bytes + * @param proof Merkle proof (sibling hashes) + * @param directions Direction array (true=right child, false=left child) + * @return valid True if the proof is valid + */ + function verifyField( + bytes calldata pathSBE, + bytes calldata value, + bytes32[] calldata proof, + bool[] calldata directions + ) external view returns (bool valid); + + /** + * @notice Get the descriptor engine address (optional) + * @dev Returns address(0) if token uses embedded storage instead of engine + * @return engine Address of the FixDescriptorEngine contract, or address(0) if not using engine + */ + function getDescriptorEngine() external view returns (address engine); +} diff --git a/contracts/modules/0_CMTATBaseCommon.sol b/contracts/modules/0_CMTATBaseCommon.sol index 6a54d1b3..d92d8c96 100644 --- a/contracts/modules/0_CMTATBaseCommon.sol +++ b/contracts/modules/0_CMTATBaseCommon.sol @@ -16,11 +16,13 @@ import {ExtraInformationModule} from "./wrapper/extensions/ExtraInformationModul import {ERC20EnforcementModule, ERC20EnforcementModuleInternal} from "./wrapper/extensions/ERC20EnforcementModule.sol"; import {DocumentEngineModule, IERC1643} from "./wrapper/extensions/DocumentEngineModule.sol"; import {SnapshotEngineModule} from "./wrapper/extensions/SnapshotEngineModule.sol"; +import {FixDescriptorEngineModule} from "./wrapper/extensions/FixDescriptorEngineModule.sol"; // options import {ERC20BaseModule, ERC20Upgradeable} from "./wrapper/core/ERC20BaseModule.sol"; /* ==== Interface and other library === */ import {ICMTATConstructor} from "../interfaces/technical/ICMTATConstructor.sol"; import {ISnapshotEngine} from "../interfaces/engine/ISnapshotEngine.sol"; +import {IFixDescriptorEngine} from "../interfaces/engine/IFixDescriptorEngine.sol"; import {IBurnMintERC20} from "../interfaces/technical/IMintBurnToken.sol"; import {IERC5679} from "../interfaces/technical/IERC5679.sol"; @@ -34,6 +36,7 @@ abstract contract CMTATBaseCommon is SnapshotEngineModule, ERC20EnforcementModule, DocumentEngineModule, + FixDescriptorEngineModule, ExtraInformationModule, AccessControlModule, // Interfaces @@ -45,7 +48,8 @@ abstract contract CMTATBaseCommon is //////////////////////////////////////////////////////////////*/ function __CMTAT_commonModules_init_unchained(address admin, ICMTATConstructor.ERC20Attributes memory ERC20Attributes_, ICMTATConstructor.ExtraInformationAttributes memory ExtraInformationModuleAttributes_, ISnapshotEngine snapshotEngine_, - IERC1643 documentEngine_ ) internal virtual onlyInitializing { + IERC1643 documentEngine_, + IFixDescriptorEngine fixDescriptorEngine_ ) internal virtual onlyInitializing { // AccessControlModule_init_unchained is called firstly due to inheritance __AccessControlModule_init_unchained(admin); @@ -55,6 +59,7 @@ abstract contract CMTATBaseCommon is __ExtraInformationModule_init_unchained(ExtraInformationModuleAttributes_.tokenId, ExtraInformationModuleAttributes_.terms, ExtraInformationModuleAttributes_.information); __SnapshotEngineModule_init_unchained(snapshotEngine_); __DocumentEngineModule_init_unchained(documentEngine_); + __FixDescriptorEngineModule_init_unchained(fixDescriptorEngine_); } /*////////////////////////////////////////////////////////////// @@ -258,4 +263,10 @@ abstract contract CMTATBaseCommon is * - the caller must have the `SNAPSHOOTER_ROLE`. */ function _authorizeSnapshots() internal virtual override(SnapshotEngineModule) onlyRole(SNAPSHOOTER_ROLE){} + + /** + * @custom:access-control + * - the caller must have the `DESCRIPTOR_ENGINE_ROLE`. + */ + function _authorizeFixDescriptorEngine() internal virtual override(FixDescriptorEngineModule) onlyRole(DESCRIPTOR_ENGINE_ROLE){} } diff --git a/contracts/modules/1_CMTATBaseAllowlist.sol b/contracts/modules/1_CMTATBaseAllowlist.sol index bcb2962c..0333736d 100644 --- a/contracts/modules/1_CMTATBaseAllowlist.sol +++ b/contracts/modules/1_CMTATBaseAllowlist.sol @@ -23,6 +23,7 @@ import {ValidationModule, ValidationModuleCore} from "./wrapper/core/ValidationM /* ==== Interface and other library === */ import {ICMTATConstructor} from "../interfaces/technical/ICMTATConstructor.sol"; import {ISnapshotEngine} from "../interfaces/engine/ISnapshotEngine.sol"; +import {IFixDescriptorEngine} from "../interfaces/engine/IFixDescriptorEngine.sol"; import {Errors} from "../libraries/Errors.sol"; abstract contract CMTATBaseAllowlist is // OpenZeppelin @@ -46,20 +47,23 @@ abstract contract CMTATBaseAllowlist is * @param extraInformationAttributes_ tokenId, terms, information * @param snapshotEngine_ external contract * @param documentEngine_ external contract + * @param fixDescriptorEngine_ external contract */ function initialize( address admin, ICMTATConstructor.ERC20Attributes memory ERC20Attributes_, ICMTATConstructor.ExtraInformationAttributes memory extraInformationAttributes_, ISnapshotEngine snapshotEngine_, - IERC1643 documentEngine_ + IERC1643 documentEngine_, + IFixDescriptorEngine fixDescriptorEngine_ ) public virtual initializer { __CMTAT_init( admin, ERC20Attributes_, extraInformationAttributes_, snapshotEngine_, - documentEngine_ + documentEngine_, + fixDescriptorEngine_ ); } @@ -72,7 +76,8 @@ abstract contract CMTATBaseAllowlist is ICMTATConstructor.ERC20Attributes memory ERC20Attributes_, ICMTATConstructor.ExtraInformationAttributes memory extraInformationAttributes_, ISnapshotEngine snapshotEngine_, - IERC1643 documentEngine_ + IERC1643 documentEngine_, + IFixDescriptorEngine fixDescriptorEngine_ ) internal virtual onlyInitializing { /* OpenZeppelin library */ // OZ init_unchained functions are called firstly due to inheritance @@ -85,7 +90,7 @@ abstract contract CMTATBaseAllowlist is __CMTAT_openzeppelin_init_unchained(ERC20Attributes_); /* Wrapper modules */ - __CMTAT_modules_init_unchained(admin, ERC20Attributes_, extraInformationAttributes_, snapshotEngine_, documentEngine_ ); + __CMTAT_modules_init_unchained(admin, ERC20Attributes_, extraInformationAttributes_, snapshotEngine_, documentEngine_, fixDescriptorEngine_ ); } /* @@ -103,8 +108,9 @@ abstract contract CMTATBaseAllowlist is * @dev CMTAT wrapper modules */ function __CMTAT_modules_init_unchained(address admin, ICMTATConstructor.ERC20Attributes memory ERC20Attributes_, ICMTATConstructor.ExtraInformationAttributes memory ExtraInformationAttributes_, ISnapshotEngine snapshotEngine_, - IERC1643 documentEngine_ ) internal virtual onlyInitializing { - __CMTAT_commonModules_init_unchained(admin,ERC20Attributes_, ExtraInformationAttributes_, snapshotEngine_, documentEngine_); + IERC1643 documentEngine_, + IFixDescriptorEngine fixDescriptorEngine_ ) internal virtual onlyInitializing { + __CMTAT_commonModules_init_unchained(admin,ERC20Attributes_, ExtraInformationAttributes_, snapshotEngine_, documentEngine_, fixDescriptorEngine_); // option __Allowlist_init_unchained(); } diff --git a/contracts/modules/1_CMTATBaseRuleEngine.sol b/contracts/modules/1_CMTATBaseRuleEngine.sol index 20cf20e0..df1c24b5 100644 --- a/contracts/modules/1_CMTATBaseRuleEngine.sol +++ b/contracts/modules/1_CMTATBaseRuleEngine.sol @@ -112,7 +112,7 @@ abstract contract CMTATBaseRuleEngine is * @dev CMTAT wrapper modules */ function __CMTAT_modules_init_unchained(address admin, ICMTATConstructor.ERC20Attributes memory ERC20Attributes_, ICMTATConstructor.ExtraInformationAttributes memory extraInformationAttributes_, ICMTATConstructor.Engine memory engines_) internal virtual onlyInitializing { - __CMTAT_commonModules_init_unchained(admin,ERC20Attributes_, extraInformationAttributes_, engines_.snapshotEngine, engines_ .documentEngine); + __CMTAT_commonModules_init_unchained(admin,ERC20Attributes_, extraInformationAttributes_, engines_.snapshotEngine, engines_.documentEngine, engines_.fixDescriptorEngine); } /*////////////////////////////////////////////////////////////// diff --git a/test/common/FixDescriptorModule/FixDescriptorModuleCommon.js b/test/common/FixDescriptorModule/FixDescriptorModuleCommon.js new file mode 100644 index 00000000..2966b9c5 --- /dev/null +++ b/test/common/FixDescriptorModule/FixDescriptorModuleCommon.js @@ -0,0 +1,94 @@ +const { expect } = require('chai') +const { ZERO_ADDRESS } = require('../../utils') + +function FixDescriptorModuleCommon () { + context('FixDescriptor Module Test', function () { + beforeEach(async function () { + if (!this.definedAtDeployment) { + this.fixDescriptorEngineMock = await ethers.deployContract( + 'FixDescriptorEngineMock', + [ZERO_ADDRESS, this.admin] + ) + } + if ((await this.cmtat.fixDescriptorEngine()) === ZERO_ADDRESS) { + await this.cmtat + .connect(this.admin) + .setFixDescriptorEngine(this.fixDescriptorEngineMock.target) + } + }) + it('testCanReturnTheRightAddressIfSet', async function () { + if (this.definedAtDeployment) { + const fixDescriptorEngine = await this.cmtat.fixDescriptorEngine() + expect(this.fixDescriptorEngineMock.target).to.equal(fixDescriptorEngine) + } + }) + it('testCanGetFixDescriptor', async function () { + const descriptor = { + fixMajor: 4, + fixMinor: 4, + dictHash: ethers.keccak256(ethers.toUtf8Bytes('dictionary')), + fixRoot: ethers.keccak256(ethers.toUtf8Bytes('root')), + fixSBEPtr: ethers.ZeroAddress, + fixSBELen: 0, + schemaURI: 'ipfs://test' + } + + await this.fixDescriptorEngineMock.setFixDescriptor(descriptor) + + const result = await this.cmtat.getFixDescriptor() + expect(result.fixMajor).to.equal(descriptor.fixMajor) + expect(result.fixMinor).to.equal(descriptor.fixMinor) + expect(result.dictHash).to.equal(descriptor.dictHash) + expect(result.fixRoot).to.equal(descriptor.fixRoot) + expect(result.fixSBEPtr).to.equal(descriptor.fixSBEPtr) + expect(result.fixSBELen).to.equal(descriptor.fixSBELen) + expect(result.schemaURI).to.equal(descriptor.schemaURI) + }) + + it('testCanGetFixRoot', async function () { + const fixRoot = ethers.keccak256(ethers.toUtf8Bytes('test-root')) + const descriptor = { + fixMajor: 4, + fixMinor: 4, + dictHash: ethers.keccak256(ethers.toUtf8Bytes('dictionary')), + fixRoot: fixRoot, + fixSBEPtr: ethers.ZeroAddress, + fixSBELen: 0, + schemaURI: 'ipfs://test' + } + + await this.fixDescriptorEngineMock.setFixDescriptor(descriptor) + + const result = await this.cmtat.getFixRoot() + expect(result).to.equal(fixRoot) + }) + + it('testCanVerifyField', async function () { + const pathSBE = ethers.toUtf8Bytes('path') + const value = ethers.toUtf8Bytes('value') + const proof = [] + const directions = [] + + // Set verifyField to return true + await this.fixDescriptorEngineMock.setVerifyFieldResult(true) + const resultTrue = await this.cmtat.verifyField(pathSBE, value, proof, directions) + expect(resultTrue).to.equal(true) + + // Set verifyField to return false + await this.fixDescriptorEngineMock.setVerifyFieldResult(false) + const resultFalse = await this.cmtat.verifyField(pathSBE, value, proof, directions) + expect(resultFalse).to.equal(false) + }) + + it('testCanGetEmptyDescriptorIfNoEngine', async function () { + // Check that engine is ZERO_ADDRESS if not set + // Note: This test assumes the CMTAT contract was deployed without FixDescriptorEngineModule initialized + // In practice, if engine is not set, getFixDescriptor() calls will revert when engine is address(0) + const engine = await this.cmtat.fixDescriptorEngine() + // If engine is not set at deployment, it should be ZERO_ADDRESS + // The actual behavior depends on whether FixDescriptorEngineModule was initialized + expect(engine).to.be.a('string') + }) + }) +} +module.exports = FixDescriptorModuleCommon diff --git a/test/common/FixDescriptorModule/FixDescriptorModuleSetFixDescriptorEngineCommon.js b/test/common/FixDescriptorModule/FixDescriptorModuleSetFixDescriptorEngineCommon.js new file mode 100644 index 00000000..786e8e40 --- /dev/null +++ b/test/common/FixDescriptorModule/FixDescriptorModuleSetFixDescriptorEngineCommon.js @@ -0,0 +1,52 @@ +const { expect } = require('chai') +const { DESCRIPTOR_ENGINE_ROLE, ZERO_ADDRESS } = require('../../utils.js') + +function FixDescriptorModuleSetFixDescriptorEngineCommon () { + context('FixDescriptorEngineSetTest', function () { + it('testCanBeSetByAdmin', async function () { + this.fixDescriptorEngineMock = await ethers.deployContract( + 'FixDescriptorEngineMock', + [ZERO_ADDRESS, this.admin] + ) + // Act + this.logs = await this.cmtat + .connect(this.admin) + .setFixDescriptorEngine(this.fixDescriptorEngineMock.target) + // Assert + // emits a FixDescriptorEngineSet event + await expect(this.logs) + .to.emit(this.cmtat, 'FixDescriptorEngine') + .withArgs(this.fixDescriptorEngineMock.target) + }) + + it('testCannotBeSetByAdminWithTheSameValue', async function () { + const fixDescriptorEngineCurrent = await this.cmtat.fixDescriptorEngine() + // Act + await expect( + this.cmtat.connect(this.admin).setFixDescriptorEngine(fixDescriptorEngineCurrent) + ).to.be.revertedWithCustomError( + this.cmtat, + 'CMTAT_FixDescriptorModule_SameValue' + ) + }) + + it('testCannotBeSetByNonAdmin', async function () { + this.fixDescriptorEngineMock = await ethers.deployContract( + 'FixDescriptorEngineMock', + [ZERO_ADDRESS, this.admin] + ) + // Act + await expect( + this.cmtat + .connect(this.address1) + .setFixDescriptorEngine(this.fixDescriptorEngineMock.target) + ) + .to.be.revertedWithCustomError( + this.cmtat, + 'AccessControlUnauthorizedAccount' + ) + .withArgs(this.address1.address, DESCRIPTOR_ENGINE_ROLE) + }) + }) +} +module.exports = FixDescriptorModuleSetFixDescriptorEngineCommon diff --git a/test/deploymentUtils.js b/test/deploymentUtils.js index 381ca369..5cc8ce8c 100644 --- a/test/deploymentUtils.js +++ b/test/deploymentUtils.js @@ -94,6 +94,7 @@ async function deployCMTATAllowlistStandalone ( ['CMTA Token', 'CMTAT', DEPLOYMENT_DECIMAL], ['CMTAT_ISIN', TERMS, 'CMTAT_info'], ZERO_ADDRESS, + ZERO_ADDRESS, ZERO_ADDRESS ]) return cmtat @@ -176,6 +177,7 @@ async function deployCMTATAllowlistProxy (forwarder, admin, deployerAddress) { ['CMTA Token', 'CMTAT', DEPLOYMENT_DECIMAL], ['CMTAT_ISIN', TERMS, 'CMTAT_info'], ZERO_ADDRESS, + ZERO_ADDRESS, ZERO_ADDRESS ], { diff --git a/test/proxy/modules/FixDescriptorModule/FixDescriptorModule.test.js b/test/proxy/modules/FixDescriptorModule/FixDescriptorModule.test.js new file mode 100644 index 00000000..5e61df5f --- /dev/null +++ b/test/proxy/modules/FixDescriptorModule/FixDescriptorModule.test.js @@ -0,0 +1,19 @@ +const FixDescriptorModuleSetFixDescriptorEngineCommon = require('../../../common/FixDescriptorModule/FixDescriptorModuleSetFixDescriptorEngineCommon') +const FixDescriptorModuleCommon = require('../../../common/FixDescriptorModule/FixDescriptorModuleCommon') +const { + deployCMTATProxy, + fixture, + loadFixture +} = require('../../../deploymentUtils') +describe('Proxy - FixDescriptorModule', function () { + beforeEach(async function () { + Object.assign(this, await loadFixture(fixture)) + this.cmtat = await deployCMTATProxy( + this._.address, + this.admin.address, + this.deployerAddress.address + ) + }) + FixDescriptorModuleCommon() + FixDescriptorModuleSetFixDescriptorEngineCommon() +}) diff --git a/test/proxy/modules/FixDescriptorModule/FixDescriptorModuleConstructor.test.js b/test/proxy/modules/FixDescriptorModule/FixDescriptorModuleConstructor.test.js new file mode 100644 index 00000000..5bdaf293 --- /dev/null +++ b/test/proxy/modules/FixDescriptorModule/FixDescriptorModuleConstructor.test.js @@ -0,0 +1,34 @@ +const FixDescriptorModuleCommon = require('../../../common/FixDescriptorModule/FixDescriptorModuleCommon') +const { + deployCMTATProxyWithParameter, + fixture, + loadFixture, + TERMS, + DEPLOYMENT_DECIMAL +} = require('../../../deploymentUtils') + +const { ZERO_ADDRESS } = require('../../../utils') + +describe('Proxy - FixDescriptorModule - Constructor', function () { + beforeEach(async function () { + Object.assign(this, await loadFixture(fixture)) + this.fixDescriptorEngineMock = await ethers.deployContract( + 'FixDescriptorEngineMock', + [ZERO_ADDRESS, this.admin] + ) + this.definedAtDeployment = true + this.cmtat = await deployCMTATProxyWithParameter( + this.deployerAddress.address, + this._.address, + this.admin.address, + 'CMTA Token', + 'CMTAT', + DEPLOYMENT_DECIMAL, + 'CMTAT_ISIN', + TERMS, + 'CMTAT_info', + [ZERO_ADDRESS, ZERO_ADDRESS, ZERO_ADDRESS, this.fixDescriptorEngineMock.target] + ) + }) + FixDescriptorModuleCommon() +}) diff --git a/test/standard/modules/FixDescriptorModule/FixDescriptorModule.test.js b/test/standard/modules/FixDescriptorModule/FixDescriptorModule.test.js new file mode 100644 index 00000000..028d5062 --- /dev/null +++ b/test/standard/modules/FixDescriptorModule/FixDescriptorModule.test.js @@ -0,0 +1,19 @@ +const FixDescriptorModuleSetFixDescriptorEngineCommon = require('../../../common/FixDescriptorModule/FixDescriptorModuleSetFixDescriptorEngineCommon') +const FixDescriptorModuleCommon = require('../../../common/FixDescriptorModule/FixDescriptorModuleCommon') +const { + deployCMTATStandalone, + fixture, + loadFixture +} = require('../../../deploymentUtils') +describe('Standard - FixDescriptorModule', function () { + beforeEach(async function () { + Object.assign(this, await loadFixture(fixture)) + this.cmtat = await deployCMTATStandalone( + this._.address, + this.admin.address, + this.deployerAddress.address + ) + }) + FixDescriptorModuleCommon() + FixDescriptorModuleSetFixDescriptorEngineCommon() +}) diff --git a/test/standard/modules/FixDescriptorModule/FixDescriptorModuleConstructor.test.js b/test/standard/modules/FixDescriptorModule/FixDescriptorModuleConstructor.test.js new file mode 100644 index 00000000..cb803f03 --- /dev/null +++ b/test/standard/modules/FixDescriptorModule/FixDescriptorModuleConstructor.test.js @@ -0,0 +1,34 @@ +const FixDescriptorModuleCommon = require('../../../common/FixDescriptorModule/FixDescriptorModuleCommon') +const { + deployCMTATStandaloneWithParameter, + fixture, + loadFixture, + TERMS, + DEPLOYMENT_DECIMAL +} = require('../../../deploymentUtils') + +const { ZERO_ADDRESS } = require('../../../utils') + +describe('Standard - FixDescriptorModule - Constructor', function () { + beforeEach(async function () { + Object.assign(this, await loadFixture(fixture)) + this.fixDescriptorEngineMock = await ethers.deployContract( + 'FixDescriptorEngineMock', + [ZERO_ADDRESS, this.admin] + ) + this.definedAtDeployment = true + this.cmtat = await deployCMTATStandaloneWithParameter( + this.deployerAddress.address, + this._.address, + this.admin.address, + 'CMTA Token', + 'CMTAT', + DEPLOYMENT_DECIMAL, + 'CMTAT_ISIN', + TERMS, + 'CMTAT_info', + [ZERO_ADDRESS, ZERO_ADDRESS, ZERO_ADDRESS, this.fixDescriptorEngineMock.target] + ) + }) + FixDescriptorModuleCommon() +}) diff --git a/test/utils.js b/test/utils.js index 3f134759..9a3a9c2c 100644 --- a/test/utils.js +++ b/test/utils.js @@ -21,6 +21,8 @@ module.exports = { '0x0000000000000000000000000000000000000000000000000000000000000000', DOCUMENT_ROLE: '0xdd7c9aafbb91d54fb2041db1d5b172ea665309b32f5fffdbddf452802a1e3b20', + DESCRIPTOR_ENGINE_ROLE: + '0x685e41c7deaeb9fafe5d911d6517cee9d8a679233028ad5b0d8ff59d1c617d0c', // keccak256("DESCRIPTOR_ENGINE_ROLE"); CROSS_CHAIN_ROLE: '0x620d362b92b6ef580d4e86c5675d679fe08d31dff47b72f281959a4eecdd036a', PROXY_UPGRADE_ROLE: From ba04a4c7b98e2fbfd7e7861f258a3d135067612a Mon Sep 17 00:00:00 2001 From: Dhruv Gupta Date: Fri, 13 Feb 2026 15:45:30 +0530 Subject: [PATCH 3/7] Update library --- contracts/mocks/FixDescriptorEngineMock.sol | 2 +- contracts/mocks/library/fix/IFixDescriptor.sol | 6 ++---- .../FixDescriptorModule/FixDescriptorModuleCommon.js | 12 +++--------- 3 files changed, 6 insertions(+), 14 deletions(-) diff --git a/contracts/mocks/FixDescriptorEngineMock.sol b/contracts/mocks/FixDescriptorEngineMock.sol index b93ef2f3..c2fd9575 100644 --- a/contracts/mocks/FixDescriptorEngineMock.sol +++ b/contracts/mocks/FixDescriptorEngineMock.sol @@ -33,7 +33,7 @@ contract FixDescriptorEngineMock is IFixDescriptorEngine, IFixDescriptor { } else { emit FixDescriptorSet( descriptor.fixRoot, - descriptor.dictHash, + descriptor.schemaHash, descriptor.fixSBEPtr, descriptor.fixSBELen ); diff --git a/contracts/mocks/library/fix/IFixDescriptor.sol b/contracts/mocks/library/fix/IFixDescriptor.sol index cb0eb052..d527a7b0 100644 --- a/contracts/mocks/library/fix/IFixDescriptor.sol +++ b/contracts/mocks/library/fix/IFixDescriptor.sol @@ -10,9 +10,7 @@ pragma solidity ^0.8.20; interface IFixDescriptor { /// @notice FIX descriptor structure struct FixDescriptor { - uint16 fixMajor; // FIX version major (e.g., 4) - uint16 fixMinor; // FIX version minor (e.g., 4) - bytes32 dictHash; // FIX dictionary/Orchestra hash + bytes32 schemaHash; // FIX schema/dictionary hash bytes32 fixRoot; // Merkle root commitment address fixSBEPtr; // SSTORE2 data contract address uint32 fixSBELen; // SBE data length @@ -22,7 +20,7 @@ interface IFixDescriptor { /// @notice Emitted when descriptor is first set event FixDescriptorSet( bytes32 indexed fixRoot, - bytes32 indexed dictHash, + bytes32 indexed schemaHash, address fixSBEPtr, uint32 fixSBELen ); diff --git a/test/common/FixDescriptorModule/FixDescriptorModuleCommon.js b/test/common/FixDescriptorModule/FixDescriptorModuleCommon.js index 2966b9c5..2e40212b 100644 --- a/test/common/FixDescriptorModule/FixDescriptorModuleCommon.js +++ b/test/common/FixDescriptorModule/FixDescriptorModuleCommon.js @@ -24,9 +24,7 @@ function FixDescriptorModuleCommon () { }) it('testCanGetFixDescriptor', async function () { const descriptor = { - fixMajor: 4, - fixMinor: 4, - dictHash: ethers.keccak256(ethers.toUtf8Bytes('dictionary')), + schemaHash: ethers.keccak256(ethers.toUtf8Bytes('dictionary')), fixRoot: ethers.keccak256(ethers.toUtf8Bytes('root')), fixSBEPtr: ethers.ZeroAddress, fixSBELen: 0, @@ -36,9 +34,7 @@ function FixDescriptorModuleCommon () { await this.fixDescriptorEngineMock.setFixDescriptor(descriptor) const result = await this.cmtat.getFixDescriptor() - expect(result.fixMajor).to.equal(descriptor.fixMajor) - expect(result.fixMinor).to.equal(descriptor.fixMinor) - expect(result.dictHash).to.equal(descriptor.dictHash) + expect(result.schemaHash).to.equal(descriptor.schemaHash) expect(result.fixRoot).to.equal(descriptor.fixRoot) expect(result.fixSBEPtr).to.equal(descriptor.fixSBEPtr) expect(result.fixSBELen).to.equal(descriptor.fixSBELen) @@ -48,9 +44,7 @@ function FixDescriptorModuleCommon () { it('testCanGetFixRoot', async function () { const fixRoot = ethers.keccak256(ethers.toUtf8Bytes('test-root')) const descriptor = { - fixMajor: 4, - fixMinor: 4, - dictHash: ethers.keccak256(ethers.toUtf8Bytes('dictionary')), + schemaHash: ethers.keccak256(ethers.toUtf8Bytes('dictionary')), fixRoot: fixRoot, fixSBEPtr: ethers.ZeroAddress, fixSBELen: 0, From 31e41fa4eb328ffb9b6f4a11141a4722edc4c2ab Mon Sep 17 00:00:00 2001 From: Swapnil Raj Date: Mon, 23 Feb 2026 13:26:12 +0000 Subject: [PATCH 4/7] Update CMTAT engine tuples for FixDescriptor engine slot. Propagate the new four-field ICMTATConstructor.Engine shape across deployment helpers and proxy/deployment tests so all initialization paths pass the FixDescriptor engine position explicitly. Co-authored-by: Cursor --- test/deployment/deployment.test.js | 4 ++-- .../deploymentUpgradeableUUPSManual.test.js | 2 +- test/deploymentUtils.js | 18 +++++++++--------- test/proxy/general/Proxy.test.js | 2 +- test/proxy/general/UpgradeProxy.test.js | 2 +- test/proxy/general/UpgradeProxyUUPS.test.js | 2 +- test/proxy/modules/MetaTxModule.test.js | 2 +- 7 files changed, 16 insertions(+), 16 deletions(-) diff --git a/test/deployment/deployment.test.js b/test/deployment/deployment.test.js index 36d7d9ff..5d522696 100644 --- a/test/deployment/deployment.test.js +++ b/test/deployment/deployment.test.js @@ -32,7 +32,7 @@ describe('CMTAT - Deployment', function () { 'CMTAT_ISIN', TERMS, 'CMTAT_info', - [ZERO_ADDRESS, ZERO_ADDRESS, ZERO_ADDRESS] + [ZERO_ADDRESS, ZERO_ADDRESS, ZERO_ADDRESS, ZERO_ADDRESS] ) ).to.be.revertedWithCustomError( this.cmtatCustomError, @@ -52,7 +52,7 @@ describe('CMTAT - Deployment', function () { 'CMTAT_ISIN', TERMS, 'CMTAT_info', - [ZERO_ADDRESS, ZERO_ADDRESS, ZERO_ADDRESS] + [ZERO_ADDRESS, ZERO_ADDRESS, ZERO_ADDRESS, ZERO_ADDRESS] ) ).to.be.revertedWithCustomError( this.cmtatCustomError, diff --git a/test/deployment/deploymentUpgradeableUUPSManual.test.js b/test/deployment/deploymentUpgradeableUUPSManual.test.js index c471d490..0f53eafd 100644 --- a/test/deployment/deploymentUpgradeableUUPSManual.test.js +++ b/test/deployment/deploymentUpgradeableUUPSManual.test.js @@ -49,7 +49,7 @@ describe('CMTAT UUPS - Manual Deployment', function () { this.admin, ['CMTA Token', 'CMTAT', DEPLOYMENT_DECIMAL], ['CMTAT_ISIN', TERMS, 'CMTAT_info'], - [ZERO_ADDRESS, ZERO_ADDRESS, ZERO_ADDRESS] + [ZERO_ADDRESS, ZERO_ADDRESS, ZERO_ADDRESS, ZERO_ADDRESS] ) }) // Core diff --git a/test/deploymentUtils.js b/test/deploymentUtils.js index 5cc8ce8c..3f6df96f 100644 --- a/test/deploymentUtils.js +++ b/test/deploymentUtils.js @@ -38,7 +38,7 @@ async function deployCMTATStandalone (forwarder, admin, deployerAddress) { admin, ['CMTA Token', 'CMTAT', DEPLOYMENT_DECIMAL], ['CMTAT_ISIN', TERMS, 'CMTAT_info'], - [ZERO_ADDRESS, ZERO_ADDRESS, ZERO_ADDRESS] + [ZERO_ADDRESS, ZERO_ADDRESS, ZERO_ADDRESS, ZERO_ADDRESS] ]) return cmtat } @@ -49,7 +49,7 @@ async function deployCMTATERC7551Standalone (forwarder, admin, deployerAddress) admin, ['CMTA Token', 'CMTAT', DEPLOYMENT_DECIMAL], ['CMTAT_ISIN', TERMS, 'CMTAT_info'], - [ZERO_ADDRESS, ZERO_ADDRESS, ZERO_ADDRESS] + [ZERO_ADDRESS, ZERO_ADDRESS, ZERO_ADDRESS, ZERO_ADDRESS] ]) return cmtat } @@ -59,7 +59,7 @@ async function deployCMTATDebtStandalone (_, admin, deployerAddress) { admin, ['CMTA Token', 'CMTAT', DEPLOYMENT_DECIMAL], ['CMTAT_ISIN', TERMS, 'CMTAT_info'], - [ZERO_ADDRESS, ZERO_ADDRESS, ZERO_ADDRESS] + [ZERO_ADDRESS, ZERO_ADDRESS, ZERO_ADDRESS, ZERO_ADDRESS] ]) return cmtat } @@ -70,7 +70,7 @@ async function deployCMTATERC1363Standalone (forwarder, admin, deployerAddress) admin, ['CMTA Token', 'CMTAT', DEPLOYMENT_DECIMAL], ['CMTAT_ISIN', TERMS, 'CMTAT_info'], - [ZERO_ADDRESS, ZERO_ADDRESS, ZERO_ADDRESS] + [ZERO_ADDRESS, ZERO_ADDRESS, ZERO_ADDRESS, ZERO_ADDRESS] ]) return cmtat } @@ -153,7 +153,7 @@ async function deployCMTATERC1363Proxy (forwarder, admin, deployerAddress) { admin, ['CMTA Token', 'CMTAT', DEPLOYMENT_DECIMAL], ['CMTAT_ISIN', TERMS, 'CMTAT_info'], - [ZERO_ADDRESS, ZERO_ADDRESS, ZERO_ADDRESS] + [ZERO_ADDRESS, ZERO_ADDRESS, ZERO_ADDRESS, ZERO_ADDRESS] ], { initializer: 'initialize', @@ -219,7 +219,7 @@ async function deployCMTATERC7551Proxy (forwarder, admin, deployerAddress) { admin, ['CMTA Token', 'CMTAT', DEPLOYMENT_DECIMAL], ['CMTAT_ISIN', TERMS, 'CMTAT_info'], - [ZERO_ADDRESS, ZERO_ADDRESS, ZERO_ADDRESS] + [ZERO_ADDRESS, ZERO_ADDRESS, ZERO_ADDRESS, ZERO_ADDRESS] ], { initializer: 'initialize', @@ -242,7 +242,7 @@ async function deployCMTATProxy (forwarder, admin, deployerAddress) { admin, ['CMTA Token', 'CMTAT', DEPLOYMENT_DECIMAL], ['CMTAT_ISIN', TERMS, 'CMTAT_info'], - [ZERO_ADDRESS, ZERO_ADDRESS, ZERO_ADDRESS] + [ZERO_ADDRESS, ZERO_ADDRESS, ZERO_ADDRESS, ZERO_ADDRESS] ], { initializer: 'initialize', @@ -265,7 +265,7 @@ async function deployCMTATDebtProxy (_, admin, deployerAddress) { admin, ['CMTA Token', 'CMTAT', DEPLOYMENT_DECIMAL], ['CMTAT_ISIN', TERMS, 'CMTAT_info'], - [ZERO_ADDRESS, ZERO_ADDRESS, ZERO_ADDRESS] + [ZERO_ADDRESS, ZERO_ADDRESS, ZERO_ADDRESS, ZERO_ADDRESS] ], { initializer: 'initialize', @@ -288,7 +288,7 @@ async function deployCMTATUUPSProxy (_, admin, deployerAddress) { admin, ['CMTA Token', 'CMTAT', DEPLOYMENT_DECIMAL], ['CMTAT_ISIN', TERMS, 'CMTAT_info'], - [ZERO_ADDRESS, ZERO_ADDRESS, ZERO_ADDRESS] + [ZERO_ADDRESS, ZERO_ADDRESS, ZERO_ADDRESS, ZERO_ADDRESS] ], { initializer: 'initialize', diff --git a/test/proxy/general/Proxy.test.js b/test/proxy/general/Proxy.test.js index 84c0e8ba..0b3bd2cf 100644 --- a/test/proxy/general/Proxy.test.js +++ b/test/proxy/general/Proxy.test.js @@ -37,7 +37,7 @@ describe('Proxy - Security Test', function () { this.attacker, ['CMTA Token', 'CMTAT', DEPLOYMENT_DECIMAL], ['CMTAT_ISIN', TERMS, 'CMTAT_info'], - [ZERO_ADDRESS, ZERO_ADDRESS, ZERO_ADDRESS] + [ZERO_ADDRESS, ZERO_ADDRESS, ZERO_ADDRESS, ZERO_ADDRESS] ) ).to.be.revertedWithCustomError( this.implementationContract, diff --git a/test/proxy/general/UpgradeProxy.test.js b/test/proxy/general/UpgradeProxy.test.js index edcef61a..74f144f1 100644 --- a/test/proxy/general/UpgradeProxy.test.js +++ b/test/proxy/general/UpgradeProxy.test.js @@ -24,7 +24,7 @@ describe('UpgradeableCMTAT - Proxy', function () { this.admin.address, ['CMTA Token', 'CMTAT', DEPLOYMENT_DECIMAL], ['CMTAT_ISIN', TERMS, 'CMTAT_info'], - [ZERO_ADDRESS, ZERO_ADDRESS, ZERO_ADDRESS] + [ZERO_ADDRESS, ZERO_ADDRESS, ZERO_ADDRESS, ZERO_ADDRESS] ], { initializer: 'initialize', diff --git a/test/proxy/general/UpgradeProxyUUPS.test.js b/test/proxy/general/UpgradeProxyUUPS.test.js index e3976ff9..75b9427a 100644 --- a/test/proxy/general/UpgradeProxyUUPS.test.js +++ b/test/proxy/general/UpgradeProxyUUPS.test.js @@ -27,7 +27,7 @@ describe('CMTAT with UUPS Proxy', function () { this.admin.address, ['CMTA Token', 'CMTAT', DEPLOYMENT_DECIMAL], ['CMTAT_ISIN', TERMS, 'CMTAT_info'], - [ZERO_ADDRESS, ZERO_ADDRESS, ZERO_ADDRESS] + [ZERO_ADDRESS, ZERO_ADDRESS, ZERO_ADDRESS, ZERO_ADDRESS] ], { initializer: 'initialize', diff --git a/test/proxy/modules/MetaTxModule.test.js b/test/proxy/modules/MetaTxModule.test.js index cab1889a..447746aa 100644 --- a/test/proxy/modules/MetaTxModule.test.js +++ b/test/proxy/modules/MetaTxModule.test.js @@ -23,7 +23,7 @@ describe('Proxy - MetaTxModule', function () { 'CMTAT_ISIN', TERMS, 'CMTAT_info', - [ZERO_ADDRESS, ZERO_ADDRESS, ZERO_ADDRESS] + [ZERO_ADDRESS, ZERO_ADDRESS, ZERO_ADDRESS, ZERO_ADDRESS] ) }) From c3591b51193ce4474445a42cf6ce92bcef29dd05 Mon Sep 17 00:00:00 2001 From: Swapnil Raj Date: Mon, 23 Feb 2026 13:26:17 +0000 Subject: [PATCH 5/7] Enforce FixDescriptor engine/token binding at module setter. Expose token() on the engine interface and add a dedicated invalid-binding error, then guard setFixDescriptorEngine so only engines bound to the current CMTAT instance can be configured. Co-authored-by: Cursor --- contracts/interfaces/engine/IFixDescriptorEngine.sol | 6 +++++- .../interfaces/modules/IFixDescriptorEngineModule.sol | 4 ++++ .../wrapper/extensions/FixDescriptorEngineModule.sol | 7 +++++++ 3 files changed, 16 insertions(+), 1 deletion(-) diff --git a/contracts/interfaces/engine/IFixDescriptorEngine.sol b/contracts/interfaces/engine/IFixDescriptorEngine.sol index a082858c..90ac114f 100644 --- a/contracts/interfaces/engine/IFixDescriptorEngine.sol +++ b/contracts/interfaces/engine/IFixDescriptorEngine.sol @@ -6,5 +6,9 @@ pragma solidity ^0.8.20; * @dev minimum interface to define a FixDescriptorEngine */ interface IFixDescriptorEngine { - // nothing more + /** + * @notice Returns the address of the token this engine is bound to. + * @return token The associated token contract address. + */ + function token() external view returns (address token); } diff --git a/contracts/interfaces/modules/IFixDescriptorEngineModule.sol b/contracts/interfaces/modules/IFixDescriptorEngineModule.sol index a6b69ba9..bb8f016c 100644 --- a/contracts/interfaces/modules/IFixDescriptorEngineModule.sol +++ b/contracts/interfaces/modules/IFixDescriptorEngineModule.sol @@ -22,6 +22,10 @@ interface IFixDescriptorEngineModule { * @dev Reverts if the new FIX descriptor engine is the same as the current one. */ error CMTAT_FixDescriptorModule_SameValue(); + /** + * @dev Reverts if the provided engine is not bound to the current token. + */ + error CMTAT_FixDescriptorModule_InvalidTokenBinding(address expectedToken, address actualToken); /* ============ Functions ============ */ /** * @notice Sets the address of the FIX descriptor engine contract. diff --git a/contracts/modules/wrapper/extensions/FixDescriptorEngineModule.sol b/contracts/modules/wrapper/extensions/FixDescriptorEngineModule.sol index 5e339448..a9a29eaa 100644 --- a/contracts/modules/wrapper/extensions/FixDescriptorEngineModule.sol +++ b/contracts/modules/wrapper/extensions/FixDescriptorEngineModule.sol @@ -56,6 +56,13 @@ abstract contract FixDescriptorEngineModule is Initializable, IFixDescriptorEngi ) public virtual override(IFixDescriptorEngineModule) onlyDescriptorEngine { FixDescriptorEngineModuleStorage storage $ = _getFixDescriptorEngineModuleStorage(); require($._fixDescriptorEngine != fixDescriptorEngine_, CMTAT_FixDescriptorModule_SameValue()); + if (address(fixDescriptorEngine_) != address(0)) { + address engineToken = fixDescriptorEngine_.token(); + require( + engineToken == address(this), + CMTAT_FixDescriptorModule_InvalidTokenBinding(address(this), engineToken) + ); + } _setFixDescriptorEngine($, fixDescriptorEngine_); } From 31230c61755ebcb83a596aef3e89eaac76d9480a Mon Sep 17 00:00:00 2001 From: Swapnil Raj Date: Mon, 23 Feb 2026 13:26:21 +0000 Subject: [PATCH 6/7] Align FixDescriptor module tests with CMTAT module responsibilities. Remove token-level assertions for engine-only read APIs, bind mock engines to the active token in valid paths, add zero-address setter coverage, and assert wrong-token engine binding reverts. Co-authored-by: Cursor --- .../FixDescriptorModuleCommon.js | 66 ++----------------- ...iptorModuleSetFixDescriptorEngineCommon.js | 21 +++++- 2 files changed, 25 insertions(+), 62 deletions(-) diff --git a/test/common/FixDescriptorModule/FixDescriptorModuleCommon.js b/test/common/FixDescriptorModule/FixDescriptorModuleCommon.js index 2e40212b..67e8fc73 100644 --- a/test/common/FixDescriptorModule/FixDescriptorModuleCommon.js +++ b/test/common/FixDescriptorModule/FixDescriptorModuleCommon.js @@ -7,7 +7,7 @@ function FixDescriptorModuleCommon () { if (!this.definedAtDeployment) { this.fixDescriptorEngineMock = await ethers.deployContract( 'FixDescriptorEngineMock', - [ZERO_ADDRESS, this.admin] + [this.cmtat.target, this.admin] ) } if ((await this.cmtat.fixDescriptorEngine()) === ZERO_ADDRESS) { @@ -22,66 +22,12 @@ function FixDescriptorModuleCommon () { expect(this.fixDescriptorEngineMock.target).to.equal(fixDescriptorEngine) } }) - it('testCanGetFixDescriptor', async function () { - const descriptor = { - schemaHash: ethers.keccak256(ethers.toUtf8Bytes('dictionary')), - fixRoot: ethers.keccak256(ethers.toUtf8Bytes('root')), - fixSBEPtr: ethers.ZeroAddress, - fixSBELen: 0, - schemaURI: 'ipfs://test' - } - - await this.fixDescriptorEngineMock.setFixDescriptor(descriptor) - - const result = await this.cmtat.getFixDescriptor() - expect(result.schemaHash).to.equal(descriptor.schemaHash) - expect(result.fixRoot).to.equal(descriptor.fixRoot) - expect(result.fixSBEPtr).to.equal(descriptor.fixSBEPtr) - expect(result.fixSBELen).to.equal(descriptor.fixSBELen) - expect(result.schemaURI).to.equal(descriptor.schemaURI) - }) - - it('testCanGetFixRoot', async function () { - const fixRoot = ethers.keccak256(ethers.toUtf8Bytes('test-root')) - const descriptor = { - schemaHash: ethers.keccak256(ethers.toUtf8Bytes('dictionary')), - fixRoot: fixRoot, - fixSBEPtr: ethers.ZeroAddress, - fixSBELen: 0, - schemaURI: 'ipfs://test' - } - - await this.fixDescriptorEngineMock.setFixDescriptor(descriptor) - - const result = await this.cmtat.getFixRoot() - expect(result).to.equal(fixRoot) - }) - - it('testCanVerifyField', async function () { - const pathSBE = ethers.toUtf8Bytes('path') - const value = ethers.toUtf8Bytes('value') - const proof = [] - const directions = [] - - // Set verifyField to return true - await this.fixDescriptorEngineMock.setVerifyFieldResult(true) - const resultTrue = await this.cmtat.verifyField(pathSBE, value, proof, directions) - expect(resultTrue).to.equal(true) - - // Set verifyField to return false - await this.fixDescriptorEngineMock.setVerifyFieldResult(false) - const resultFalse = await this.cmtat.verifyField(pathSBE, value, proof, directions) - expect(resultFalse).to.equal(false) - }) + it('testCanSetZeroAddressEngine', async function () { + await this.cmtat + .connect(this.admin) + .setFixDescriptorEngine(ZERO_ADDRESS) - it('testCanGetEmptyDescriptorIfNoEngine', async function () { - // Check that engine is ZERO_ADDRESS if not set - // Note: This test assumes the CMTAT contract was deployed without FixDescriptorEngineModule initialized - // In practice, if engine is not set, getFixDescriptor() calls will revert when engine is address(0) - const engine = await this.cmtat.fixDescriptorEngine() - // If engine is not set at deployment, it should be ZERO_ADDRESS - // The actual behavior depends on whether FixDescriptorEngineModule was initialized - expect(engine).to.be.a('string') + expect(await this.cmtat.fixDescriptorEngine()).to.equal(ZERO_ADDRESS) }) }) } diff --git a/test/common/FixDescriptorModule/FixDescriptorModuleSetFixDescriptorEngineCommon.js b/test/common/FixDescriptorModule/FixDescriptorModuleSetFixDescriptorEngineCommon.js index 786e8e40..382e23b8 100644 --- a/test/common/FixDescriptorModule/FixDescriptorModuleSetFixDescriptorEngineCommon.js +++ b/test/common/FixDescriptorModule/FixDescriptorModuleSetFixDescriptorEngineCommon.js @@ -6,7 +6,7 @@ function FixDescriptorModuleSetFixDescriptorEngineCommon () { it('testCanBeSetByAdmin', async function () { this.fixDescriptorEngineMock = await ethers.deployContract( 'FixDescriptorEngineMock', - [ZERO_ADDRESS, this.admin] + [this.cmtat.target, this.admin] ) // Act this.logs = await this.cmtat @@ -33,7 +33,7 @@ function FixDescriptorModuleSetFixDescriptorEngineCommon () { it('testCannotBeSetByNonAdmin', async function () { this.fixDescriptorEngineMock = await ethers.deployContract( 'FixDescriptorEngineMock', - [ZERO_ADDRESS, this.admin] + [this.cmtat.target, this.admin] ) // Act await expect( @@ -47,6 +47,23 @@ function FixDescriptorModuleSetFixDescriptorEngineCommon () { ) .withArgs(this.address1.address, DESCRIPTOR_ENGINE_ROLE) }) + + it('testCannotSetEngineBoundToAnotherToken', async function () { + const otherToken = this.address2.address + this.fixDescriptorEngineMock = await ethers.deployContract( + 'FixDescriptorEngineMock', + [otherToken, this.admin] + ) + + await expect( + this.cmtat.connect(this.admin).setFixDescriptorEngine(this.fixDescriptorEngineMock.target) + ) + .to.be.revertedWithCustomError( + this.cmtat, + 'CMTAT_FixDescriptorModule_InvalidTokenBinding' + ) + .withArgs(this.cmtat.target, otherToken) + }) }) } module.exports = FixDescriptorModuleSetFixDescriptorEngineCommon From 543cf3db6b8e3a2a2b3c6043913dac4e65243fee Mon Sep 17 00:00:00 2001 From: Swapnil Raj Date: Mon, 23 Feb 2026 15:38:47 +0000 Subject: [PATCH 7/7] Align FixEngine module initialization with upstream style. Remove explicit zero-address snapshot/document/fix engine init calls from CMTATBaseAccessControl so initialization flow matches CMTA v3.2 patterns while preserving role-gated FixDescriptor engine wiring. Co-authored-by: Cursor --- contracts/modules/1_CMTATBaseAccessControl.sol | 5 ----- 1 file changed, 5 deletions(-) diff --git a/contracts/modules/1_CMTATBaseAccessControl.sol b/contracts/modules/1_CMTATBaseAccessControl.sol index 8f84254e..58664ec5 100644 --- a/contracts/modules/1_CMTATBaseAccessControl.sol +++ b/contracts/modules/1_CMTATBaseAccessControl.sol @@ -20,8 +20,6 @@ import {FixDescriptorEngineModule} from "./wrapper/extensions/FixDescriptorEngin import {ERC20BaseModule, ERC20Upgradeable} from "./wrapper/core/ERC20BaseModule.sol"; /* ==== Interface and other library === */ import {ICMTATConstructor} from "../interfaces/technical/ICMTATConstructor.sol"; -import {ISnapshotEngine} from "../interfaces/engine/ISnapshotEngine.sol"; -import {IFixDescriptorEngine} from "../interfaces/engine/IFixDescriptorEngine.sol"; import {CMTATBaseCommon} from "./0_CMTATBaseCommon.sol"; abstract contract CMTATBaseAccessControl is AccessControlModule, @@ -37,9 +35,6 @@ abstract contract CMTATBaseAccessControl is __ERC20BaseModule_init_unchained(ERC20Attributes_.decimalsIrrevocable, ERC20Attributes_.name, ERC20Attributes_.symbol); /* Extensions */ __ExtraInformationModule_init_unchained(ExtraInformationModuleAttributes_.tokenId, ExtraInformationModuleAttributes_.terms, ExtraInformationModuleAttributes_.information); - __SnapshotEngineModule_init_unchained(ISnapshotEngine(address(0))); - __DocumentEngineModule_init_unchained(IERC1643(address(0))); - __FixDescriptorEngineModule_init_unchained(IFixDescriptorEngine(address(0))); } /*//////////////////////////////////////////////////////////////