From c3f3497352cf7d8004a32dbc062cd1ccef20707e Mon Sep 17 00:00:00 2001 From: Geolffrey Mena Date: Sun, 6 Apr 2025 09:00:08 -0600 Subject: [PATCH 01/16] feat: hooks module --- contracts/assets/AssetOwnership.sol | 1 + contracts/core/interfaces/hooks/IHook.sol | 13 ++ .../core/interfaces/hooks/IHookRegistry.sol | 7 ++ .../hooks/IHookRegistryRegistrable.sol | 7 ++ .../hooks/IHookRegistryVerifiable.sol | 7 ++ contracts/core/primitives/Types.sol | 4 +- contracts/custody/CustodianFactory.sol | 4 +- contracts/custody/CustodianImpl.sol | 2 +- contracts/custody/CustodianReferendum.sol | 2 +- contracts/lifecycle/HookBase.sol | 2 + contracts/lifecycle/HookRegistry.sol | 118 ++++++++++++++++++ contracts/policies/PolicyAudit.sol | 8 +- contracts/policies/PolicyBase.sol | 1 - contracts/registries/HookRegistry.sol | 11 -- 14 files changed, 165 insertions(+), 22 deletions(-) create mode 100644 contracts/core/interfaces/hooks/IHook.sol create mode 100644 contracts/core/interfaces/hooks/IHookRegistry.sol create mode 100644 contracts/core/interfaces/hooks/IHookRegistryRegistrable.sol create mode 100644 contracts/core/interfaces/hooks/IHookRegistryVerifiable.sol create mode 100644 contracts/lifecycle/HookBase.sol create mode 100644 contracts/lifecycle/HookRegistry.sol delete mode 100644 contracts/registries/HookRegistry.sol diff --git a/contracts/assets/AssetOwnership.sol b/contracts/assets/AssetOwnership.sol index 4707296..a4fb5ae 100644 --- a/contracts/assets/AssetOwnership.sol +++ b/contracts/assets/AssetOwnership.sol @@ -112,6 +112,7 @@ contract AssetOwnership is } // TODO: build getURI => from custodian /erc721-metadata + // TODO: Update asset info control version restricted/approved by governance // TODO: Transfer Ownership Fee: Introducing a fee for transferring // ownership discourages frequent or unnecessary transfers, // adding an economic cost to any potential abuse of the system. Like bypassing content diff --git a/contracts/core/interfaces/hooks/IHook.sol b/contracts/core/interfaces/hooks/IHook.sol new file mode 100644 index 0000000..a56bb7e --- /dev/null +++ b/contracts/core/interfaces/hooks/IHook.sol @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: BUSL-1.1 +// NatSpec format convention - https://docs.soliditylang.org/en/v0.5.10/natspec-format.html +pragma solidity 0.8.26; + +interface IHook { + // check if the hook its associated with the target + // eg. SubscriptionPolicy its allowed to process this hook + function validate(address caller, address target) external; + /// @notice Called by the protocol to signal the use of the hook + /// @param caller The address invoking the hook (e.g., the user or distributor) + /// @param context Optional: bytes data for hook-specific usage + function execute(address caller, bytes calldata context) external; +} diff --git a/contracts/core/interfaces/hooks/IHookRegistry.sol b/contracts/core/interfaces/hooks/IHookRegistry.sol new file mode 100644 index 0000000..5a6e536 --- /dev/null +++ b/contracts/core/interfaces/hooks/IHookRegistry.sol @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: BUSL-1.1 +// NatSpec format convention - https://docs.soliditylang.org/en/v0.5.10/natspec-format.html +pragma solidity 0.8.26; + +interface IHookRegistry { + function lookup(bytes4 interfaceId) external returns (address); +} diff --git a/contracts/core/interfaces/hooks/IHookRegistryRegistrable.sol b/contracts/core/interfaces/hooks/IHookRegistryRegistrable.sol new file mode 100644 index 0000000..4ac17d3 --- /dev/null +++ b/contracts/core/interfaces/hooks/IHookRegistryRegistrable.sol @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: BUSL-1.1 +// NatSpec format convention - https://docs.soliditylang.org/en/v0.5.10/natspec-format.html +pragma solidity 0.8.26; + +interface IHookRegistryRegistrable { + function lookup(bytes4 interfaceId) external returns (address); +} diff --git a/contracts/core/interfaces/hooks/IHookRegistryVerifiable.sol b/contracts/core/interfaces/hooks/IHookRegistryVerifiable.sol new file mode 100644 index 0000000..8ee25e3 --- /dev/null +++ b/contracts/core/interfaces/hooks/IHookRegistryVerifiable.sol @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: BUSL-1.1 +// NatSpec format convention - https://docs.soliditylang.org/en/v0.5.10/natspec-format.html +pragma solidity 0.8.26; + +interface IHookRegistryVerifiable { + function lookup(bytes4 interfaceId) external returns (address); +} diff --git a/contracts/core/primitives/Types.sol b/contracts/core/primitives/Types.sol index 64a2962..7c0c4d2 100644 --- a/contracts/core/primitives/Types.sol +++ b/contracts/core/primitives/Types.sol @@ -56,9 +56,9 @@ library T { /// It includes fields for currency, amount, rate basis, calculation formula, and off-chain terms. struct Terms { uint256 amount; // The rate amount based on the rate basis, expressed in the smallest unit of the currency - address currency; // The currency in which the amount is denominated, e.g., MMC or USDC + address currency; // The currency in which the amount is denominated, e.g., MMC TimeFrame timeFrame; // The time frame for the amount, using a standardized enum (e.g., HOURLY, DAILY) - string uri; // URI pointing to off-chain terms for additional details or extended documentation + string uri; // URI pointing to off-chain terms for additional details or extended documentation // TODO we could extend the terms based on the real needs .. } diff --git a/contracts/custody/CustodianFactory.sol b/contracts/custody/CustodianFactory.sol index 1b1c79a..f6e30ee 100644 --- a/contracts/custody/CustodianFactory.sol +++ b/contracts/custody/CustodianFactory.sol @@ -34,6 +34,7 @@ contract CustodianFactory is UpgradeableBeacon, ICustodianFactory { /// @notice Event emitted when a new custodian is created. /// @param custodianAddress Address of the newly created custodian. /// @param endpoint Endpoint associated with the new custodian. + /// @param endpointHash Endpoint bytes32 hash associate with the new custodian. event CustodianCreated(address indexed custodianAddress, string indexed endpoint, bytes32 endpointHash); /// @notice Error to be thrown when attempting to register an already registered custodian. @@ -58,8 +59,7 @@ contract CustodianFactory is UpgradeableBeacon, ICustodianFactory { /// @param endpoint The endpoint associated with the new custodian. /// @return The address of the newly created custodian contract. function create(string calldata endpoint) external returns (address) { - // TODO additional validation needed to check endpoint schemes. eg: https, ip, etc - // TODO option two, penalize invalid endpoints, and revoked during referendum + // TODO penalize invalid endpoints, and revoked during referendum bytes32 endpointHash = _registerEndpoint(endpoint); address newContract = _deployCustodian(endpoint); _registerManager(newContract, msg.sender); diff --git a/contracts/custody/CustodianImpl.sol b/contracts/custody/CustodianImpl.sol index 1e4a550..0554d79 100644 --- a/contracts/custody/CustodianImpl.sol +++ b/contracts/custody/CustodianImpl.sol @@ -19,7 +19,7 @@ import { FinancialOps } from "@synaps3/core/libraries/FinancialOps.sol"; /// - Calls to this contract are made through a `BeaconProxy`, allowing upgrades at the beacon level. /// - This contract itself is NOT upgradeable directly; its updates are managed by the beacon. /// - It inherits from upgradeable contracts **ONLY** to maintain compatibility with their storage layout (ERC-7201). -/// - This approach ensures that future improvement to the implementation do not break the beacon's storage layout. +/// - This approach ensures that future improvement to the implementation do not break the beacon proxy's storage layout. contract CustodianImpl is Initializable, ERC165Upgradeable, OwnableUpgradeable, ICustodian { using FinancialOps for address; diff --git a/contracts/custody/CustodianReferendum.sol b/contracts/custody/CustodianReferendum.sol index acf1429..ae417c8 100644 --- a/contracts/custody/CustodianReferendum.sol +++ b/contracts/custody/CustodianReferendum.sol @@ -181,7 +181,7 @@ contract CustodianReferendum is (uint256 fees, T.Scheme scheme) = TOLLGATE.getFees(address(this), currency); if (scheme != T.Scheme.FLAT) revert InvalidFeeSchemeProvided("Expected a FLAT fee scheme."); - // TODO: additional check if exists in factory to validate emission + /// TODO penalize invalid endpoints, and revoked during referendum // eg: custodian.getCreator MUST be equal to msg.sender uint256 locked = LEDGER_VAULT.lock(msg.sender, fees, currency); // lock funds uint256 claimed = LEDGER_VAULT.claim(msg.sender, locked, currency); // claim the funds on behalf diff --git a/contracts/lifecycle/HookBase.sol b/contracts/lifecycle/HookBase.sol new file mode 100644 index 0000000..540e01e --- /dev/null +++ b/contracts/lifecycle/HookBase.sol @@ -0,0 +1,2 @@ +// se podria aprovechar la logica de los modifier para establecer control de ejecucion antes y despues +// crear un onCall modifier para agregar a los metodo y registrar en base a cada hook acciones especificas en el llamado \ No newline at end of file diff --git a/contracts/lifecycle/HookRegistry.sol b/contracts/lifecycle/HookRegistry.sol new file mode 100644 index 0000000..b4f004a --- /dev/null +++ b/contracts/lifecycle/HookRegistry.sol @@ -0,0 +1,118 @@ +// SPDX-License-Identifier: BUSL-1.1 +// NatSpec format convention - https://docs.soliditylang.org/en/v0.5.10/natspec-format.html +pragma solidity 0.8.26; + +import { ERC165Checker } from "@openzeppelin/contracts/utils/introspection/ERC165Checker.sol"; +import { Initializable } from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; +import { UUPSUpgradeable } from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; +import { AccessControlledUpgradeable } from "@synaps3/core/primitives/upgradeable/AccessControlledUpgradeable.sol"; +import { QuorumUpgradeable } from "@synaps3/core/primitives/upgradeable/QuorumUpgradeable.sol"; +import { IHook } from "@synaps3/core/interfaces/hooks/IHook.sol"; +import { IHookRegistry } from "@synaps3/core/interfaces/hooks/IHookRegistry.sol"; + +// Hook interfaces can define logic for: +// - Access control: e.g. only users holding certain tokens can access content. +// - Time-based rules: e.g. access available for a limited period. +// - Revenue sharing: e.g. split payments between creators or collaborators. +// This registry manages hooks based on their interface ID and ensures they implement IHook. + +contract HookRegistry is Initializable, UUPSUpgradeable, AccessControlledUpgradeable, QuorumUpgradeable, IHookRegistry { + using ERC165Checker for address; + + /// @dev The interface ID for IHook, used to verify that a hook contract implements the correct interface. + bytes4 private constant INTERFACE_HOOK = type(IHook).interfaceId; + + // Mapping from hook interface ID to hook contract address + mapping(bytes4 => address) private _hooks; + + /// @notice Event emitted when a hook is registered. + /// @param hook The address of the hook that has been registered. + /// @param interfaceId The interface ID that the hook implements. + /// @param submitter The address that submitted the hook. + event HookRegistered(address indexed hook, bytes4 interfaceId, address submitter); + + /// @notice Event emitted when a hook is approved. + /// @param hook The address of the hook that has been approved. + /// @param auditor The address of the auditor who approved the hook. + event HookApproved(address indexed hook, address auditor); + + /// @notice Event emitted when a hook is revoked. + /// @param hook The address of the hook that has been revoked. + /// @param auditor The address of the auditor who revoked the hook. + event HookRevoked(address indexed hook, address auditor); + + /// @dev Error thrown when the hook contract does not implement the IHook interface. + error InvalidHookContract(address); + + /// @dev Modifier to check that a hook contract implements the IHook interface. + /// @param hook The address of the hook contract. + /// Reverts if the hook does not implement the required interface. + modifier onlyValidHook(address hook) { + if (!hook.supportsInterface(INTERFACE_HOOK)) { + revert InvalidHookContract(hook); + } + _; + } + + /// @dev Constructor that disables initializers to prevent the implementation contract from being initialized. + /// @custom:oz-upgrades-unsafe-allow constructor + constructor() { + /// Prevent initialization of the logic contract + _disableInitializers(); + } + + /// @notice Initializes the contract with the necessary configurations. + /// This function is only called once upon deployment and sets up Quorum, UUPS, and Access control. + function initialize(address accessManager) public initializer { + __Quorum_init(); + __UUPSUpgradeable_init(); + __AccessControlled_init(accessManager); + } + + /// @notice Submit a hook for registration. + /// @param hook The address of the hook contract to register. + /// @param interfaceId The interface ID that this hook implements. + /// This allows different kinds of hook logic to be categorized and retrieved by interface ID. + function submit(address hook, bytes4 interfaceId) external onlyValidHook(hook) restricted { + _register(uint160(hook)); + _hooks[interfaceId] = hook; + emit HookRegistered(hook, interfaceId, msg.sender); + } + + /// @notice Approves a registered hook contract. + /// @param hook The address of the hook to be approved. + /// Emits a HookApproved event upon success. + function approve(address hook) external onlyValidHook(hook) restricted { + _approve(uint160(hook)); + emit HookApproved(hook, msg.sender); + } + + /// @notice Revokes a previously approved hook contract. + /// @param hook The address of the hook to revoke. + /// Emits a HookRevoked event upon success. + function reject(address hook) external onlyValidHook(hook) restricted { + _revoke(uint160(hook)); + emit HookRevoked(hook, msg.sender); + } + + /// @notice Returns the registered hook address for a given interface ID. + /// @param interfaceId The interface ID used to look up the registered hook. + /// @return The address of the hook associated with the given interface ID. + function lookup(bytes4 interfaceId) external returns (address) { + return _hooks[interfaceId]; + } + + /// @notice Checks if a hook associated with an interface ID is active (i.e., approved). + /// @param interfaceId The interface ID to check for an active hook. + /// @return True if the hook is registered and its status is active, false otherwise. + function isActive(bytes4 interfaceId) external view returns (bool) { + address hook = _hooks[interfaceId]; + bool audited = _status(uint160(hook)) == Status.Active; + return hook != address(0) && audited; + } + + /// @dev Authorizes the upgrade of the contract. + /// @notice Only the admin can authorize the upgrade. + /// @param newImplementation The address of the new implementation contract. + function _authorizeUpgrade(address newImplementation) internal override onlyAdmin {} +} diff --git a/contracts/policies/PolicyAudit.sol b/contracts/policies/PolicyAudit.sol index fe2f2c2..0a60f65 100644 --- a/contracts/policies/PolicyAudit.sol +++ b/contracts/policies/PolicyAudit.sol @@ -3,8 +3,8 @@ pragma solidity 0.8.26; import { ERC165Checker } from "@openzeppelin/contracts/utils/introspection/ERC165Checker.sol"; -import { UUPSUpgradeable } from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; import { Initializable } from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; +import { UUPSUpgradeable } from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; import { AccessControlledUpgradeable } from "@synaps3/core/primitives/upgradeable/AccessControlledUpgradeable.sol"; import { QuorumUpgradeable } from "@synaps3/core/primitives/upgradeable/QuorumUpgradeable.sol"; import { IPolicyAuditor } from "@synaps3/core/interfaces/policies/IPolicyAuditor.sol"; @@ -19,9 +19,6 @@ contract PolicyAudit is Initializable, UUPSUpgradeable, AccessControlledUpgradea /// @dev The interface ID for IPolicy, used to verify that a policy contract implements the correct interface. bytes4 private constant INTERFACE_POLICY = type(IPolicy).interfaceId; - /// @dev Error thrown when the policy contract does not implement the IPolicy interface. - error InvalidPolicyContract(address); - /// @notice Event emitted when a policy is submitted for audit. /// @param policy The address of the policy that has been submitted. /// @param submitter The address of the account that submitted the policy for audit. @@ -37,6 +34,9 @@ contract PolicyAudit is Initializable, UUPSUpgradeable, AccessControlledUpgradea /// @param auditor The address of the auditor that revoked the audit. event PolicyRevoked(address indexed policy, address auditor); + /// @dev Error thrown when the policy contract does not implement the IPolicy interface. + error InvalidPolicyContract(address); + /// @dev Modifier to check that a policy contract implements the IPolicy interface. /// @param policy The address of the license policy contract. /// Reverts if the policy does not implement the required interface. diff --git a/contracts/policies/PolicyBase.sol b/contracts/policies/PolicyBase.sol index 12b50a9..0171a03 100644 --- a/contracts/policies/PolicyBase.sol +++ b/contracts/policies/PolicyBase.sol @@ -166,7 +166,6 @@ abstract contract PolicyBase is ERC165, IPolicy { ) internal returns (uint256[] memory) { bytes memory payload = abi.encode(agreement); bytes memory data = abi.encode(holder, agreement.initiator, address(this), agreement.parties, payload); - // register policy metrics in the holder context to track analytics emit AgreementCommitted(holder, agreement.parties.length, agreement.total, agreement.fees); return ATTESTATION_PROVIDER.attest(agreement.parties, expireAt, data); } diff --git a/contracts/registries/HookRegistry.sol b/contracts/registries/HookRegistry.sol deleted file mode 100644 index bdaafe9..0000000 --- a/contracts/registries/HookRegistry.sol +++ /dev/null @@ -1,11 +0,0 @@ -// SPDX-License-Identifier: BUSL-1.1 -// NatSpec format convention - https://docs.soliditylang.org/en/v0.5.10/natspec-format.html -pragma solidity 0.8.26; - -// IAccessHook with preset rules of access for holders account or assets -// eg: holder can set if under some conditions the user can access -// - time locked free access => access to my content for the first 7 days if not already accessed -// - gated access conditions => hold X NFT and access my asset id 123 -// - locked access to N user - -// ISplitHook with earning splits From 118f9d94c148aca0a46cff18ae4846304639f7ae Mon Sep 17 00:00:00 2001 From: Geolffrey Mena Date: Mon, 7 Apr 2025 13:26:33 -0600 Subject: [PATCH 02/16] chore: temp disable sectest --- test/primitives/Quorum.t..sol | 132 ---------------------------------- 1 file changed, 132 deletions(-) delete mode 100644 test/primitives/Quorum.t..sol diff --git a/test/primitives/Quorum.t..sol b/test/primitives/Quorum.t..sol deleted file mode 100644 index be2447d..0000000 --- a/test/primitives/Quorum.t..sol +++ /dev/null @@ -1,132 +0,0 @@ -// SPDX-License-Identifier: BUSL-1.1 -pragma solidity 0.8.26; - -import "forge-std/Test.sol"; -import { QuorumUpgradeable } from "contracts/core/primitives/upgradeable/QuorumUpgradeable.sol"; - -contract QuorumWrapper is QuorumUpgradeable { - function status(uint256 entry) external view returns (uint256) { - return uint256(_status(entry)); - } - - function revoke(uint256 entry) external { - _revoke(entry); - } - - function blocked(uint256 entry) external { - _block(entry); - } - - function approve(uint256 entry) external { - _approve(entry); - } - - function quit(uint256 entry) external { - _quit(entry); - } - - function register(uint256 entry) external { - _register(entry); - } -} - -contract QuorumTest is Test { - QuorumWrapper quorum; - - function setUp() public { - quorum = new QuorumWrapper(); - } - - function test_DefaultStatus() public view { - assertEq(quorum.status(1234536789), 0); - } - - function test_RegisterStatusFlow() public { - uint256 entry = 1234567189; - // initial pending status - assertEq(quorum.status(entry), 0); - - // register status - quorum.register(entry); - assertEq(quorum.status(entry), 1); - } - - function test_ActiveStatusFlow() public { - uint256 entry = 1234526789; - // waiting status - quorum.register(entry); - // active status - quorum.approve(entry); - assertEq(quorum.status(entry), 2); - } - - function test_QuitStatusFlow() public { - uint256 entry = 1234256789; - // waiting status - quorum.register(entry); - // pending status - quorum.quit(entry); - assertEq(quorum.status(entry), 0); - } - - function test_BlockedStatusFlow() public { - uint256 entry = 123455589; - // waiting status - quorum.register(entry); - // blocked status happens before active - quorum.blocked(entry); - assertEq(quorum.status(entry), 3); - } - - function test_RevokeStatusFlow() public { - uint256 entry = 123455589; - // waiting status - quorum.register(entry); - // active status - quorum.approve(entry); - // revoked status happens after approved - quorum.revoke(entry); - assertEq(quorum.status(entry), 3); - } - - function test_Approve_RevertWhen_ApproveNotRegistered() public { - uint256 entry = 123456789; - // active status - vm.expectRevert(QuorumUpgradeable.NotWaitingApproval.selector); - quorum.approve(entry); - } - - function test_Register_RevertWhen_WaitingApproval() public { - uint256 entry = 123456789; - // active status - quorum.register(entry); - vm.expectRevert(QuorumUpgradeable.NotPendingApproval.selector); - quorum.register(entry); - } - - function test_Revoke_RevertWhen_BlockedNotActive() public { - uint256 entry = 12345677; - // waiting status - quorum.register(entry); - // blocked status - vm.expectRevert(QuorumUpgradeable.InvalidInactiveState.selector); - quorum.revoke(entry); - } - - function test_Quit_RevertWhen_QuitNotWaiting() public { - uint256 entry = 123456459; - // blocked status - vm.expectRevert(QuorumUpgradeable.NotWaitingApproval.selector); - quorum.quit(entry); - } - - function test_Quit_RevertWhen_Blocked() public { - uint256 entry = 123456789; - // waiting status - quorum.register(entry); - quorum.blocked(entry); - // active status - vm.expectRevert(QuorumUpgradeable.NotWaitingApproval.selector); - quorum.quit(entry); - } -} From c3507b1fff34fff6e28f5e9543a9890844583334 Mon Sep 17 00:00:00 2001 From: Geolffrey Mena Date: Mon, 7 Apr 2025 13:26:42 -0600 Subject: [PATCH 03/16] chore: temp disable sectest --- test/primitives/Quorum.t.sol | 132 +++++++++++++++++++++++++++++++++++ 1 file changed, 132 insertions(+) create mode 100644 test/primitives/Quorum.t.sol diff --git a/test/primitives/Quorum.t.sol b/test/primitives/Quorum.t.sol new file mode 100644 index 0000000..be2447d --- /dev/null +++ b/test/primitives/Quorum.t.sol @@ -0,0 +1,132 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.26; + +import "forge-std/Test.sol"; +import { QuorumUpgradeable } from "contracts/core/primitives/upgradeable/QuorumUpgradeable.sol"; + +contract QuorumWrapper is QuorumUpgradeable { + function status(uint256 entry) external view returns (uint256) { + return uint256(_status(entry)); + } + + function revoke(uint256 entry) external { + _revoke(entry); + } + + function blocked(uint256 entry) external { + _block(entry); + } + + function approve(uint256 entry) external { + _approve(entry); + } + + function quit(uint256 entry) external { + _quit(entry); + } + + function register(uint256 entry) external { + _register(entry); + } +} + +contract QuorumTest is Test { + QuorumWrapper quorum; + + function setUp() public { + quorum = new QuorumWrapper(); + } + + function test_DefaultStatus() public view { + assertEq(quorum.status(1234536789), 0); + } + + function test_RegisterStatusFlow() public { + uint256 entry = 1234567189; + // initial pending status + assertEq(quorum.status(entry), 0); + + // register status + quorum.register(entry); + assertEq(quorum.status(entry), 1); + } + + function test_ActiveStatusFlow() public { + uint256 entry = 1234526789; + // waiting status + quorum.register(entry); + // active status + quorum.approve(entry); + assertEq(quorum.status(entry), 2); + } + + function test_QuitStatusFlow() public { + uint256 entry = 1234256789; + // waiting status + quorum.register(entry); + // pending status + quorum.quit(entry); + assertEq(quorum.status(entry), 0); + } + + function test_BlockedStatusFlow() public { + uint256 entry = 123455589; + // waiting status + quorum.register(entry); + // blocked status happens before active + quorum.blocked(entry); + assertEq(quorum.status(entry), 3); + } + + function test_RevokeStatusFlow() public { + uint256 entry = 123455589; + // waiting status + quorum.register(entry); + // active status + quorum.approve(entry); + // revoked status happens after approved + quorum.revoke(entry); + assertEq(quorum.status(entry), 3); + } + + function test_Approve_RevertWhen_ApproveNotRegistered() public { + uint256 entry = 123456789; + // active status + vm.expectRevert(QuorumUpgradeable.NotWaitingApproval.selector); + quorum.approve(entry); + } + + function test_Register_RevertWhen_WaitingApproval() public { + uint256 entry = 123456789; + // active status + quorum.register(entry); + vm.expectRevert(QuorumUpgradeable.NotPendingApproval.selector); + quorum.register(entry); + } + + function test_Revoke_RevertWhen_BlockedNotActive() public { + uint256 entry = 12345677; + // waiting status + quorum.register(entry); + // blocked status + vm.expectRevert(QuorumUpgradeable.InvalidInactiveState.selector); + quorum.revoke(entry); + } + + function test_Quit_RevertWhen_QuitNotWaiting() public { + uint256 entry = 123456459; + // blocked status + vm.expectRevert(QuorumUpgradeable.NotWaitingApproval.selector); + quorum.quit(entry); + } + + function test_Quit_RevertWhen_Blocked() public { + uint256 entry = 123456789; + // waiting status + quorum.register(entry); + quorum.blocked(entry); + // active status + vm.expectRevert(QuorumUpgradeable.NotWaitingApproval.selector); + quorum.quit(entry); + } +} From fb376d7518be47ba2745992ffcbb54992e8a2538 Mon Sep 17 00:00:00 2001 From: Geolffrey Mena Date: Mon, 7 Apr 2025 20:03:17 -0600 Subject: [PATCH 04/16] refactor: added IQuorum as external representation of FSM generic entities --- contracts/assets/AssetReferendum.sol | 3 +- .../interfaces/assets/IAssetRegistrable.sol | 3 +- .../core/interfaces/base/IBalanceOperator.sol | 10 +- contracts/core/interfaces/base/IQuorum.sol | 16 ++++ .../interfaces/base/IQuorumInspectable.sol | 11 +++ .../interfaces/base/IQuorumRegistrable.sol | 22 +++++ .../core/interfaces/base/IQuorumRevokable.sol | 10 ++ .../custody/ICustodianInspectable.sol | 15 +++ .../custody/ICustodianReferendum.sol | 12 ++- .../custody/ICustodianRegistrable.sol | 12 +-- .../custody/ICustodianRevokable.sol | 10 ++ contracts/core/primitives/Types.sol | 12 ++- .../upgradeable/QuorumUpgradeable.sol | 33 +++---- contracts/custody/CustodianReferendum.sol | 6 +- contracts/lifecycle/HookRegistry.sol | 6 +- contracts/policies/PolicyAudit.sol | 3 +- test/primitives/Quorum.t.sol | 95 ++++++++++--------- test/syndication/DistributorReferendum.t.sol | 16 ++-- 18 files changed, 196 insertions(+), 99 deletions(-) create mode 100644 contracts/core/interfaces/base/IQuorum.sol create mode 100644 contracts/core/interfaces/base/IQuorumInspectable.sol create mode 100644 contracts/core/interfaces/base/IQuorumRegistrable.sol create mode 100644 contracts/core/interfaces/base/IQuorumRevokable.sol create mode 100644 contracts/core/interfaces/custody/ICustodianInspectable.sol create mode 100644 contracts/core/interfaces/custody/ICustodianRevokable.sol diff --git a/contracts/assets/AssetReferendum.sol b/contracts/assets/AssetReferendum.sol index e720cf8..30c1f38 100644 --- a/contracts/assets/AssetReferendum.sol +++ b/contracts/assets/AssetReferendum.sol @@ -11,6 +11,7 @@ import { EnumerableSet } from "@openzeppelin/contracts/utils/structs/EnumerableS import { AccessControlledUpgradeable } from "@synaps3/core/primitives/upgradeable/AccessControlledUpgradeable.sol"; import { QuorumUpgradeable } from "@synaps3/core/primitives/upgradeable/QuorumUpgradeable.sol"; import { IAssetReferendum } from "@synaps3/core/interfaces/assets/IAssetReferendum.sol"; +import { T } from "@synaps3/core/primitives/Types.sol"; import { C } from "@synaps3/core/primitives/Constants.sol"; /// @title AssetReferendum @@ -133,7 +134,7 @@ contract AssetReferendum is /// @notice Checks if the asset is active nor blocked. /// @param assetId The ID of the asset. function isActive(uint256 assetId) public view returns (bool) { - return _status(assetId) == Status.Active; + return _status(assetId) == T.Status.Active; } /// @notice Function that should revert when msg.sender is not authorized to upgrade the contract. diff --git a/contracts/core/interfaces/assets/IAssetRegistrable.sol b/contracts/core/interfaces/assets/IAssetRegistrable.sol index d5a2647..eac28cb 100644 --- a/contracts/core/interfaces/assets/IAssetRegistrable.sol +++ b/contracts/core/interfaces/assets/IAssetRegistrable.sol @@ -4,8 +4,7 @@ pragma solidity 0.8.26; /// @title IAssetRegistrable Interface /// @notice Defines the essential functions for managing asset registration and governance through a referendum process. -/// @dev Implement this interface in a referendum contract to allow -/// asset proposals, approvals, rejections, and revocations. +/// @dev This interface mirrors the FSM behavior from `IQuorum`, but scoped to asset governance. interface IAssetRegistrable { /// @notice Submits a new asset proposition for a referendum. /// @dev This function should allow entities to propose an asset for approval. diff --git a/contracts/core/interfaces/base/IBalanceOperator.sol b/contracts/core/interfaces/base/IBalanceOperator.sol index 315ff40..1ac66d5 100644 --- a/contracts/core/interfaces/base/IBalanceOperator.sol +++ b/contracts/core/interfaces/base/IBalanceOperator.sol @@ -2,14 +2,16 @@ // NatSpec format convention - https://docs.soliditylang.org/en/v0.5.10/natspec-format.html pragma solidity 0.8.26; -/// @title IBalanceOperator Interface -/// @notice Combines functionalities for verifying, depositing, withdrawing, and transferring balances. -/// @dev This interface aggregates multiple interfaces to standardize balance-related operations. import { IBalanceDepositor } from "@synaps3/core/interfaces/base/IBalanceDepositor.sol"; import { IBalanceWithdrawable } from "@synaps3/core/interfaces/base/IBalanceWithdrawable.sol"; import { IBalanceTransferable } from "@synaps3/core/interfaces/base/IBalanceTransferable.sol"; import { IBalanceVerifiable } from "@synaps3/core/interfaces/base/IBalanceVerifiable.sol"; +/// @title IBalanceOperator Interface +/// @notice Combines functionalities for verifying, depositing, withdrawing, and transferring balances. +/// @dev This interface aggregates multiple interfaces to standardize balance-related operations. /// @dev The `IBalanceOperator` interface extends multiple interfaces to provide a comprehensive suite of /// balance-related operations, including deposit, withdrawal, transfer, reserve, and balance verification. -interface IBalanceOperator is IBalanceDepositor, IBalanceWithdrawable, IBalanceTransferable, IBalanceVerifiable {} +interface IBalanceOperator is IBalanceDepositor, IBalanceWithdrawable, IBalanceTransferable, IBalanceVerifiable { + +} diff --git a/contracts/core/interfaces/base/IQuorum.sol b/contracts/core/interfaces/base/IQuorum.sol new file mode 100644 index 0000000..d263887 --- /dev/null +++ b/contracts/core/interfaces/base/IQuorum.sol @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.26; + +import { IQuorumRegistrable } from "@synaps3/core/interfaces/base/IQuorumRegistrable.sol"; +import { IQuorumRevokable } from "@synaps3/core/interfaces/base/IQuorumRevokable.sol"; +import { IQuorumInspectable } from "@synaps3/core/interfaces/base/IQuorumInspectable.sol"; + +/// @title IQuorum +/// @notice Aggregates the full lifecycle of an FSM-driven entity registration system. +/// @dev Combines registration, approval, rejection, revocation, and status inspection. +/// Intended for systems that use `QuorumUpgradeable` as FSM logic layer. +interface IQuorum is + IQuorumRegistrable, + IQuorumRevokable, + IQuorumInspectable +{} diff --git a/contracts/core/interfaces/base/IQuorumInspectable.sol b/contracts/core/interfaces/base/IQuorumInspectable.sol new file mode 100644 index 0000000..3fe295a --- /dev/null +++ b/contracts/core/interfaces/base/IQuorumInspectable.sol @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.26; + +import { T } from "@synaps3/core/primitives/Types.sol"; + +/// @title IQuorumInspectable +/// @notice Interface for querying the status of registered entities. +interface IQuorumInspectable { + /// @notice Returns the current FSM status of the entity. + function status(uint256 entry) external view returns (T.Status); +} diff --git a/contracts/core/interfaces/base/IQuorumRegistrable.sol b/contracts/core/interfaces/base/IQuorumRegistrable.sol new file mode 100644 index 0000000..ce126e1 --- /dev/null +++ b/contracts/core/interfaces/base/IQuorumRegistrable.sol @@ -0,0 +1,22 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.26; + +/// @title IQuorumRegistrable +/// @notice FSM interface for entities that go through registration and decision (approve/reject). +interface IQuorumRegistrable { + /// @notice Initiates the registration of an entity. + /// @param entry The generic ID (could be uint160(address) or asset ID). + function register(uint256 entry) external; + + /// @notice Approves an entity. + /// @param entry The ID of the entity. + function approve(uint256 entry) external; + + /// @notice Blocks or rejects an entity before approval. + /// @param entry The ID of the entity. + function reject(uint256 entry) external; + + /// @notice Internal function for an entity to resign. + /// @param entry The ID of the entity. + function quit(uint256 entry) external; +} diff --git a/contracts/core/interfaces/base/IQuorumRevokable.sol b/contracts/core/interfaces/base/IQuorumRevokable.sol new file mode 100644 index 0000000..d23b629 --- /dev/null +++ b/contracts/core/interfaces/base/IQuorumRevokable.sol @@ -0,0 +1,10 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.26; + +/// @title IQuorumRevokable +/// @notice FSM interface for revoking approved entities. +interface IQuorumRevokable { + /// @notice Revokes a previously approved entity. + /// @param entry The ID of the entity. + function revoke(uint256 entry) external; +} diff --git a/contracts/core/interfaces/custody/ICustodianInspectable.sol b/contracts/core/interfaces/custody/ICustodianInspectable.sol new file mode 100644 index 0000000..f63da5a --- /dev/null +++ b/contracts/core/interfaces/custody/ICustodianInspectable.sol @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.26; + +/// @title ICustodianInspectable +/// @dev Interface for retrieving custodian enrollment data. +interface ICustodianInspectable { + /// @notice Retrieves the enrollment deadline for a custodian. + /// @param custodian The address of the custodian. + /// @return The enrollment deadline timestamp. + function getEnrollmentDeadline(address custodian) external view returns (uint256); + + /// @notice Retrieves the total number of enrollments. + /// @return The number of enrollments. + function getEnrollmentCount() external view returns (uint256); +} diff --git a/contracts/core/interfaces/custody/ICustodianReferendum.sol b/contracts/core/interfaces/custody/ICustodianReferendum.sol index 8b071da..753e3a1 100644 --- a/contracts/core/interfaces/custody/ICustodianReferendum.sol +++ b/contracts/core/interfaces/custody/ICustodianReferendum.sol @@ -4,8 +4,18 @@ pragma solidity 0.8.26; import { ICustodianExpirable } from "@synaps3/core/interfaces/custody/ICustodianExpirable.sol"; import { ICustodianRegistrable } from "@synaps3/core/interfaces/custody/ICustodianRegistrable.sol"; +import { ICustodianInspectable } from "@synaps3/core/interfaces/custody/ICustodianInspectable.sol"; import { ICustodianVerifiable } from "@synaps3/core/interfaces/custody/ICustodianVerifiable.sol"; +import { ICustodianRevokable } from "@synaps3/core/interfaces/custody/ICustodianRevokable.sol"; /// @title ICustodianReferendum /// @notice Interface that defines the necessary operations for managing custodian registration. -interface ICustodianReferendum is ICustodianRegistrable, ICustodianVerifiable, ICustodianExpirable {} +interface ICustodianReferendum is + ICustodianRegistrable, + ICustodianVerifiable, + ICustodianExpirable, + ICustodianInspectable, + ICustodianRevokable +{ + +} diff --git a/contracts/core/interfaces/custody/ICustodianRegistrable.sol b/contracts/core/interfaces/custody/ICustodianRegistrable.sol index 9f3e6fb..e449416 100644 --- a/contracts/core/interfaces/custody/ICustodianRegistrable.sol +++ b/contracts/core/interfaces/custody/ICustodianRegistrable.sol @@ -4,6 +4,8 @@ pragma solidity 0.8.26; /// @title ICustodianRegistrable /// @dev Interface for managing custodians registration. +/// @dev This interface indirectly implements the FSM defined in `IQuorum` using `QuorumUpgradeable`. +/// Functions here are semantically equivalent to the FSM transitions: register → approve. interface ICustodianRegistrable { /// @notice Registers data with a given identifier. /// @param custodian The address of the custodian to register. @@ -14,14 +16,4 @@ interface ICustodianRegistrable { /// @param custodian The address of the custodian to approve. function approve(address custodian) external; - /// @notice Revokes the registration of an entity. - /// @param custodian The address of the custodian to revoke. - function revoke(address custodian) external; - - /// @notice Retrieves the enrollment deadline for a custodian. - /// @param custodian The address of the custodian. - function getEnrollmentDeadline(address custodian) external view returns (uint256); - - /// @notice Retrieves the total number of enrollments. - function getEnrollmentCount() external view returns (uint256); } diff --git a/contracts/core/interfaces/custody/ICustodianRevokable.sol b/contracts/core/interfaces/custody/ICustodianRevokable.sol new file mode 100644 index 0000000..1f139c2 --- /dev/null +++ b/contracts/core/interfaces/custody/ICustodianRevokable.sol @@ -0,0 +1,10 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.26; + +/// @title ICustodianRevokable +/// @dev Interface for revoking approved custodians. +interface ICustodianRevokable { + /// @notice Revokes the active status of a custodian. + /// @param custodian The address of the custodian to revoke. + function revoke(address custodian) external; +} diff --git a/contracts/core/primitives/Types.sol b/contracts/core/primitives/Types.sol index 7c0c4d2..01113a1 100644 --- a/contracts/core/primitives/Types.sol +++ b/contracts/core/primitives/Types.sol @@ -6,6 +6,14 @@ pragma solidity 0.8.26; /// @notice This library provides common type definitions for use in other contracts. /// @dev This library defines types and structures that can be imported and used in other contracts. library T { + /// @notice Enum to represent the status of an entity. + enum Status { + Pending, // 0: The entity is default pending approval + Waiting, // 1: The entity is waiting for approval + Active, // 2: The entity is active + Blocked // 3: The entity is blocked + } + /// @title Scheme /// @notice Enum representing different fee calculation schemes in the protocol. /// Each scheme determines how fees are computed based on the operation's context. @@ -56,9 +64,9 @@ library T { /// It includes fields for currency, amount, rate basis, calculation formula, and off-chain terms. struct Terms { uint256 amount; // The rate amount based on the rate basis, expressed in the smallest unit of the currency - address currency; // The currency in which the amount is denominated, e.g., MMC + address currency; // The currency in which the amount is denominated, e.g., MMC TimeFrame timeFrame; // The time frame for the amount, using a standardized enum (e.g., HOURLY, DAILY) - string uri; // URI pointing to off-chain terms for additional details or extended documentation + string uri; // URI pointing to off-chain terms for additional details or extended documentation // TODO we could extend the terms based on the real needs .. } diff --git a/contracts/core/primitives/upgradeable/QuorumUpgradeable.sol b/contracts/core/primitives/upgradeable/QuorumUpgradeable.sol index dfac81c..101951a 100644 --- a/contracts/core/primitives/upgradeable/QuorumUpgradeable.sol +++ b/contracts/core/primitives/upgradeable/QuorumUpgradeable.sol @@ -3,6 +3,7 @@ pragma solidity 0.8.26; import { Initializable } from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; +import { T } from "@synaps3/core/primitives/Types.sol"; /** * @title QuorumUpgradeable @@ -24,17 +25,9 @@ import { Initializable } from "@openzeppelin/contracts-upgradeable/proxy/utils/I * (3: Blocked) */ abstract contract QuorumUpgradeable is Initializable { - /// @notice Enum to represent the status of an entity. - enum Status { - Pending, // 0: The entity is default pending approval - Waiting, // 1: The entity is waiting for approval - Active, // 2: The entity is active - Blocked // 3: The entity is blocked - } - /// @custom:storage-location erc7201:quorumupgradeable struct RegistryStorage { - mapping(uint256 => Status) _state; // Mapping to store the status of entities + mapping(uint256 => T.Status) _state; // Mapping to store the status of entities } /// @dev Storage slot for LedgerStorage, calculated using a unique namespace to avoid conflicts. @@ -61,7 +54,7 @@ abstract contract QuorumUpgradeable is Initializable { /// @notice Internal function to get the status of an entity. /// @param entry The ID of the entity. - function _status(uint256 entry) internal view returns (Status) { + function _status(uint256 entry) internal view returns (T.Status) { RegistryStorage storage $ = _getRegistryStorage(); return $._state[entry]; } @@ -71,8 +64,8 @@ abstract contract QuorumUpgradeable is Initializable { /// @param entry The ID of the entity. function _revoke(uint256 entry) internal { RegistryStorage storage $ = _getRegistryStorage(); - if (_status(entry) != Status.Active) revert InvalidInactiveState(); - $._state[entry] = Status.Blocked; + if (_status(entry) != T.Status.Active) revert InvalidInactiveState(); + $._state[entry] = T.Status.Blocked; } /// @notice Internal function to block an entity before approval. @@ -80,32 +73,32 @@ abstract contract QuorumUpgradeable is Initializable { /// @param entry The ID of the entity. function _block(uint256 entry) internal { RegistryStorage storage $ = _getRegistryStorage(); - if (_status(entry) != Status.Waiting) revert NotWaitingApproval(); - $._state[entry] = Status.Blocked; + if (_status(entry) != T.Status.Waiting) revert NotWaitingApproval(); + $._state[entry] = T.Status.Blocked; } /// @notice Internal function to approve an entity's access. /// @param entry The ID of the entity. function _approve(uint256 entry) internal { RegistryStorage storage $ = _getRegistryStorage(); - if (_status(entry) != Status.Waiting) revert NotWaitingApproval(); - $._state[entry] = Status.Active; + if (_status(entry) != T.Status.Waiting) revert NotWaitingApproval(); + $._state[entry] = T.Status.Active; } /// @notice Internal function for an entity to resign. /// @param entry The ID of the entity. function _quit(uint256 entry) internal { RegistryStorage storage $ = _getRegistryStorage(); - if (_status(entry) != Status.Waiting) revert NotWaitingApproval(); - $._state[entry] = Status.Pending; + if (_status(entry) != T.Status.Waiting) revert NotWaitingApproval(); + $._state[entry] = T.Status.Pending; } /// @notice Internal function to start an entity's registration. /// @param entry The ID of the entity. function _register(uint256 entry) internal { RegistryStorage storage $ = _getRegistryStorage(); - if (_status(entry) != Status.Pending) revert NotPendingApproval(); - $._state[entry] = Status.Waiting; + if (_status(entry) != T.Status.Pending) revert NotPendingApproval(); + $._state[entry] = T.Status.Waiting; } /// @notice Internal function to get the registry storage. diff --git a/contracts/custody/CustodianReferendum.sol b/contracts/custody/CustodianReferendum.sol index ae417c8..eb8c520 100644 --- a/contracts/custody/CustodianReferendum.sol +++ b/contracts/custody/CustodianReferendum.sol @@ -145,21 +145,21 @@ contract CustodianReferendum is // This mechanism helps to verify the availability of the custodian, // forcing recurrent registrations and ensuring ongoing participation. bool notExpiredDeadline = _enrollmentDeadline[custodian] > block.timestamp; - return _status(uint160(custodian)) == Status.Active && notExpiredDeadline; + return _status(uint160(custodian)) == T.Status.Active && notExpiredDeadline; } /// @notice Checks if the entity is waiting. /// @dev This function verifies the waiting status of the custodian. /// @param custodian The custodian's address to check. function isWaiting(address custodian) external view onlyValidCustodian(custodian) returns (bool) { - return _status(uint160(custodian)) == Status.Waiting; + return _status(uint160(custodian)) == T.Status.Waiting; } /// @notice Checks if the entity is blocked. /// @dev This function verifies the blocked status of the custodian. /// @param custodian The custodian's address to check. function isBlocked(address custodian) external view onlyValidCustodian(custodian) returns (bool) { - return _status(uint160(custodian)) == Status.Blocked; + return _status(uint160(custodian)) == T.Status.Blocked; } /// @notice Registers a custodian by sending a payment to the contract. diff --git a/contracts/lifecycle/HookRegistry.sol b/contracts/lifecycle/HookRegistry.sol index b4f004a..471b4ab 100644 --- a/contracts/lifecycle/HookRegistry.sol +++ b/contracts/lifecycle/HookRegistry.sol @@ -7,8 +7,10 @@ import { Initializable } from "@openzeppelin/contracts-upgradeable/proxy/utils/I import { UUPSUpgradeable } from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; import { AccessControlledUpgradeable } from "@synaps3/core/primitives/upgradeable/AccessControlledUpgradeable.sol"; import { QuorumUpgradeable } from "@synaps3/core/primitives/upgradeable/QuorumUpgradeable.sol"; -import { IHook } from "@synaps3/core/interfaces/hooks/IHook.sol"; import { IHookRegistry } from "@synaps3/core/interfaces/hooks/IHookRegistry.sol"; +import { IHook } from "@synaps3/core/interfaces/hooks/IHook.sol"; +import { T } from "@synaps3/core/primitives/Types.sol"; + // Hook interfaces can define logic for: // - Access control: e.g. only users holding certain tokens can access content. @@ -107,7 +109,7 @@ contract HookRegistry is Initializable, UUPSUpgradeable, AccessControlledUpgrade /// @return True if the hook is registered and its status is active, false otherwise. function isActive(bytes4 interfaceId) external view returns (bool) { address hook = _hooks[interfaceId]; - bool audited = _status(uint160(hook)) == Status.Active; + bool audited = _status(uint160(hook)) == T.Status.Active; return hook != address(0) && audited; } diff --git a/contracts/policies/PolicyAudit.sol b/contracts/policies/PolicyAudit.sol index 0a60f65..5ace9b8 100644 --- a/contracts/policies/PolicyAudit.sol +++ b/contracts/policies/PolicyAudit.sol @@ -9,6 +9,7 @@ import { AccessControlledUpgradeable } from "@synaps3/core/primitives/upgradeabl import { QuorumUpgradeable } from "@synaps3/core/primitives/upgradeable/QuorumUpgradeable.sol"; import { IPolicyAuditor } from "@synaps3/core/interfaces/policies/IPolicyAuditor.sol"; import { IPolicy } from "@synaps3/core/interfaces/policies/IPolicy.sol"; +import { T } from "@synaps3/core/primitives/Types.sol"; /// @title PolicyAudit /// @notice This contract audits content policies and ensures that only authorized entities can approve or revoke. @@ -90,7 +91,7 @@ contract PolicyAudit is Initializable, UUPSUpgradeable, AccessControlledUpgradea /// @notice Checks if a specific policy contract has been audited. /// @param policy The address of the policy contract to verify. function isAudited(address policy) external view returns (bool) { - return _status(uint160(policy)) == Status.Active; + return _status(uint160(policy)) == T.Status.Active; } /// @dev Authorizes the upgrade of the contract. diff --git a/test/primitives/Quorum.t.sol b/test/primitives/Quorum.t.sol index be2447d..44a8ab3 100644 --- a/test/primitives/Quorum.t.sol +++ b/test/primitives/Quorum.t.sol @@ -3,20 +3,20 @@ pragma solidity 0.8.26; import "forge-std/Test.sol"; import { QuorumUpgradeable } from "contracts/core/primitives/upgradeable/QuorumUpgradeable.sol"; +import { IQuorumInspectable } from "contracts/core/interfaces/base/IQuorumInspectable.sol"; +import { IQuorumRegistrable } from "contracts/core/interfaces/base/IQuorumRegistrable.sol"; +import { IQuorumRevokable } from "contracts/core/interfaces/base/IQuorumRevokable.sol"; +import { T } from "@synaps3/core/primitives/Types.sol"; contract QuorumWrapper is QuorumUpgradeable { - function status(uint256 entry) external view returns (uint256) { - return uint256(_status(entry)); + function status(uint256 entry) external view returns (uint8) { + return uint8(_status(entry)); } function revoke(uint256 entry) external { _revoke(entry); } - function blocked(uint256 entry) external { - _block(entry); - } - function approve(uint256 entry) external { _approve(entry); } @@ -28,105 +28,108 @@ contract QuorumWrapper is QuorumUpgradeable { function register(uint256 entry) external { _register(entry); } + + function reject(uint256 entry) external { + _block(entry); + } } contract QuorumTest is Test { - QuorumWrapper quorum; + address quorum; function setUp() public { - quorum = new QuorumWrapper(); + quorum = address(new QuorumWrapper()); } function test_DefaultStatus() public view { - assertEq(quorum.status(1234536789), 0); + T.Status status = IQuorumInspectable(quorum).status(1234536789); + assertTrue(status == T.Status.Pending); } function test_RegisterStatusFlow() public { uint256 entry = 1234567189; // initial pending status - assertEq(quorum.status(entry), 0); + T.Status prevStatus = IQuorumInspectable(quorum).status(entry); + assertTrue(prevStatus == T.Status.Pending); // register status - quorum.register(entry); - assertEq(quorum.status(entry), 1); + IQuorumRegistrable(quorum).register(entry); + T.Status newStatus = IQuorumInspectable(quorum).status(entry); + assertTrue(newStatus == T.Status.Waiting); } function test_ActiveStatusFlow() public { uint256 entry = 1234526789; - // waiting status - quorum.register(entry); - // active status - quorum.approve(entry); - assertEq(quorum.status(entry), 2); + // waiting status -> active status + IQuorumRegistrable(quorum).register(entry); + IQuorumRegistrable(quorum).approve(entry); + T.Status newStatus = IQuorumInspectable(quorum).status(entry); + assertTrue(newStatus == T.Status.Active); } function test_QuitStatusFlow() public { uint256 entry = 1234256789; - // waiting status - quorum.register(entry); - // pending status - quorum.quit(entry); - assertEq(quorum.status(entry), 0); + // waiting status -> pending status + IQuorumRegistrable(quorum).register(entry); + IQuorumRegistrable(quorum).quit(entry); + T.Status newStatus = IQuorumInspectable(quorum).status(entry); + assertTrue(newStatus == T.Status.Pending); } function test_BlockedStatusFlow() public { uint256 entry = 123455589; - // waiting status - quorum.register(entry); + // waiting status -> blocked + IQuorumRegistrable(quorum).register(entry); // blocked status happens before active - quorum.blocked(entry); - assertEq(quorum.status(entry), 3); + IQuorumRegistrable(quorum).reject(entry); + T.Status newStatus = IQuorumInspectable(quorum).status(entry); + assertTrue(newStatus == T.Status.Blocked); } function test_RevokeStatusFlow() public { uint256 entry = 123455589; - // waiting status - quorum.register(entry); - // active status - quorum.approve(entry); + // waiting status -> active -> blocked + IQuorumRegistrable(quorum).register(entry); + IQuorumRegistrable(quorum).approve(entry); // revoked status happens after approved - quorum.revoke(entry); - assertEq(quorum.status(entry), 3); + IQuorumRevokable(quorum).revoke(entry); + T.Status newStatus = IQuorumInspectable(quorum).status(entry); + assertTrue(newStatus == T.Status.Blocked); } function test_Approve_RevertWhen_ApproveNotRegistered() public { uint256 entry = 123456789; - // active status vm.expectRevert(QuorumUpgradeable.NotWaitingApproval.selector); - quorum.approve(entry); + IQuorumRegistrable(quorum).approve(entry); } function test_Register_RevertWhen_WaitingApproval() public { uint256 entry = 123456789; - // active status - quorum.register(entry); + IQuorumRegistrable(quorum).register(entry); vm.expectRevert(QuorumUpgradeable.NotPendingApproval.selector); - quorum.register(entry); + IQuorumRegistrable(quorum).register(entry); } function test_Revoke_RevertWhen_BlockedNotActive() public { uint256 entry = 12345677; - // waiting status - quorum.register(entry); - // blocked status + IQuorumRegistrable(quorum).register(entry); vm.expectRevert(QuorumUpgradeable.InvalidInactiveState.selector); - quorum.revoke(entry); + IQuorumRevokable(quorum).revoke(entry); } function test_Quit_RevertWhen_QuitNotWaiting() public { uint256 entry = 123456459; // blocked status vm.expectRevert(QuorumUpgradeable.NotWaitingApproval.selector); - quorum.quit(entry); + IQuorumRegistrable(quorum).quit(entry); } function test_Quit_RevertWhen_Blocked() public { uint256 entry = 123456789; // waiting status - quorum.register(entry); - quorum.blocked(entry); - // active status + IQuorumRegistrable(quorum).register(entry); + IQuorumRegistrable(quorum).reject(entry); vm.expectRevert(QuorumUpgradeable.NotWaitingApproval.selector); - quorum.quit(entry); + IQuorumRegistrable(quorum).quit(entry); } } diff --git a/test/syndication/DistributorReferendum.t.sol b/test/syndication/DistributorReferendum.t.sol index 4bf0d44..21b0105 100644 --- a/test/syndication/DistributorReferendum.t.sol +++ b/test/syndication/DistributorReferendum.t.sol @@ -9,6 +9,8 @@ import { ILedgerVault } from "contracts/core/interfaces/financial/ILedgerVault.s import { ICustodianVerifiable } from "contracts/core/interfaces/custody/ICustodianVerifiable.sol"; import { ICustodianExpirable } from "contracts/core/interfaces/custody/ICustodianExpirable.sol"; import { ICustodianRegistrable } from "contracts/core/interfaces/custody/ICustodianRegistrable.sol"; +import { ICustodianInspectable} from "contracts/core/interfaces/custody/ICustodianInspectable.sol"; +import { ICustodianRevokable } from "contracts/core/interfaces/custody/ICustodianRevokable.sol"; import { ICustodianFactory } from "contracts/core/interfaces/custody/ICustodianFactory.sol"; import { BaseTest } from "test/BaseTest.t.sol"; @@ -100,7 +102,7 @@ contract CustodianReferendumTest is BaseTest { } function test_Register_SetValidEnrollmentTime() public { - ICustodianRegistrable registrable = ICustodianRegistrable(referendum); + ICustodianInspectable inspectable = ICustodianInspectable(referendum); ICustodianExpirable expirable = ICustodianExpirable(referendum); _setFeesAsGovernor(1 * 1e18); @@ -111,7 +113,7 @@ contract CustodianReferendumTest is BaseTest { // register the custodian expecting the right enrollment time.. _registerCustodianWithApproval(custodian, 1 * 1e18); uint256 expected = currentTime + expectedExpiration; - uint256 got = registrable.getEnrollmentDeadline(custodian); + uint256 got = inspectable.getEnrollmentDeadline(custodian); assertEq(got, expected); } @@ -154,7 +156,7 @@ contract CustodianReferendumTest is BaseTest { _registerAndApproveCustodian(custodian3); // still governor prank // valid approvals, increments the total of enrollments - assertEq(ICustodianRegistrable(referendum).getEnrollmentCount(), 3); + assertEq(ICustodianInspectable(referendum).getEnrollmentCount(), 3); } function test_Revoke_RevokedEventEmitted() public { @@ -164,22 +166,22 @@ contract CustodianReferendumTest is BaseTest { // after register a custodian a Registered event is expected vm.expectEmit(true, false, false, true, address(referendum)); emit CustodianReferendum.Revoked(custodian); - ICustodianRegistrable(referendum).revoke(custodian); + ICustodianRevokable(referendum).revoke(custodian); } function test_Revoke_DecrementEnrollmentCount() public { _registerAndApproveCustodian(custodian); // still governor prank // valid approvals, increments the total of enrollments vm.prank(governor); - ICustodianRegistrable(referendum).revoke(custodian); - assertEq(ICustodianRegistrable(referendum).getEnrollmentCount(), 0); + ICustodianRevokable(referendum).revoke(custodian); + assertEq(ICustodianInspectable(referendum).getEnrollmentCount(), 0); } function test_Revoke_SetBlockedState() public { _registerAndApproveCustodian(custodian); // still governor prank // custodian get revoked by governance.. vm.prank(governor); - ICustodianRegistrable(referendum).revoke(custodian); + ICustodianRevokable(referendum).revoke(custodian); assertTrue(ICustodianVerifiable(referendum).isBlocked(custodian)); } From fa460e8ccce24346fd9a30e14d31a03b8a0fc7f2 Mon Sep 17 00:00:00 2001 From: Geolffrey Mena Date: Sat, 26 Apr 2025 12:13:53 -0600 Subject: [PATCH 05/16] refactor: simplified logic for active policies --- .../core/interfaces/policies/IPolicy.sol | 2 +- contracts/custody/CustodianFactory.sol | 4 --- contracts/custody/CustodianReferendum.sol | 2 -- contracts/financial/AgreementManager.sol | 2 +- contracts/policies/PolicyBase.sol | 30 ------------------- contracts/rights/RightsAssetCustodian.sol | 3 +- contracts/rights/RightsPolicyManager.sol | 2 +- packages/protocol/package.json | 2 +- packages/types/package.json | 2 +- test/libraries/FinancialOps.t.sol | 1 + 10 files changed, 8 insertions(+), 42 deletions(-) diff --git a/contracts/core/interfaces/policies/IPolicy.sol b/contracts/core/interfaces/policies/IPolicy.sol index 58061fd..5afe759 100644 --- a/contracts/core/interfaces/policies/IPolicy.sol +++ b/contracts/core/interfaces/policies/IPolicy.sol @@ -24,7 +24,7 @@ interface IPolicy { /// @notice Executes the agreement between the asset holder and the account based on the policy's rules. /// @dev Rights Policies Manager contract should be the only one allowed to call this method. - /// @param holder The rights holder whose authorization is required for accessing the asset. + /// @param holder The rights holder whose authorization is required for accessing the assets. /// @param agreement An object containing the terms agreed upon between the asset holder and the user. function enforce(address holder, T.Agreement calldata agreement) external returns (uint256[] memory); diff --git a/contracts/custody/CustodianFactory.sol b/contracts/custody/CustodianFactory.sol index f6e30ee..d7710ec 100644 --- a/contracts/custody/CustodianFactory.sol +++ b/contracts/custody/CustodianFactory.sol @@ -51,15 +51,11 @@ contract CustodianFactory is UpgradeableBeacon, ICustodianFactory { return _manager[custodian]; } - // TODO: check domain existence eg. existingDomain('an.com') - // TODO: avoid domains spam/pollution/faking - /// @notice Function to create a new custodian contract. /// @dev Ensures that the same endpoint is not registered twice. /// @param endpoint The endpoint associated with the new custodian. /// @return The address of the newly created custodian contract. function create(string calldata endpoint) external returns (address) { - // TODO penalize invalid endpoints, and revoked during referendum bytes32 endpointHash = _registerEndpoint(endpoint); address newContract = _deployCustodian(endpoint); _registerManager(newContract, msg.sender); diff --git a/contracts/custody/CustodianReferendum.sol b/contracts/custody/CustodianReferendum.sol index eb8c520..00e9a75 100644 --- a/contracts/custody/CustodianReferendum.sol +++ b/contracts/custody/CustodianReferendum.sol @@ -180,9 +180,7 @@ contract CustodianReferendum is // !IMPORTANT If tollgate does not support the currency, will revert.. (uint256 fees, T.Scheme scheme) = TOLLGATE.getFees(address(this), currency); if (scheme != T.Scheme.FLAT) revert InvalidFeeSchemeProvided("Expected a FLAT fee scheme."); - /// TODO penalize invalid endpoints, and revoked during referendum - // eg: custodian.getCreator MUST be equal to msg.sender uint256 locked = LEDGER_VAULT.lock(msg.sender, fees, currency); // lock funds uint256 claimed = LEDGER_VAULT.claim(msg.sender, locked, currency); // claim the funds on behalf uint256 confirmed = LEDGER_VAULT.withdraw(address(this), claimed, currency); // collect funds diff --git a/contracts/financial/AgreementManager.sol b/contracts/financial/AgreementManager.sol index bd7f373..e02892c 100644 --- a/contracts/financial/AgreementManager.sol +++ b/contracts/financial/AgreementManager.sol @@ -14,7 +14,7 @@ import { FinancialOps } from "@synaps3/core/libraries/FinancialOps.sol"; import { FeesOps } from "@synaps3/core/libraries/FeesOps.sol"; import { T } from "@synaps3/core/primitives/Types.sol"; -// TODO Trustless escrow system - modular escrow framework +// TODO Doc: Trustless escrow system - modular escrow framework - escrow mechanism (agreement settlement) /// @title AgreementManager /// @notice Manages the lifecycle (trustless escrow system) of agreements, including creation and retrieval. diff --git a/contracts/policies/PolicyBase.sol b/contracts/policies/PolicyBase.sol index 0171a03..37a90e3 100644 --- a/contracts/policies/PolicyBase.sol +++ b/contracts/policies/PolicyBase.sol @@ -25,8 +25,6 @@ abstract contract PolicyBase is ERC165, IPolicy { /// @custom:oz-upgrades-unsafe-allow state-variable-immutable IAssetOwnership public immutable ASSET_OWNERSHIP; - /// @dev Policy state - bool private _active; /// @dev Registry to store the relation between (context & account) key => attestation mapping(bytes32 => uint256) private _attestations; /// @dev Reserved storage slots to avoid conflicts with child storage if upgradeability is required @@ -86,29 +84,6 @@ abstract contract PolicyBase is ERC165, IPolicy { _; } - /// @notice Marks the contract as initialized and allows further execution. - /// @dev This modifier sets the `initialized` state to `true` when invoked. - /// Use this in functions that require a one-time setup phase. - /// Once executed, the contract is considered initialized. - /// @custom:modifiers setup - modifier activate() { - _active = false; - _; - _active = true; - } - - /// @notice Ensures that the contract has been properly initialized before execution. - /// @dev This modifier checks if the `initialized` flag is set to `true`. - /// If the contract is not initialized, it reverts with an `InvalidPolicyInitialization` error. - /// Use this to restrict access to functions that depend on the contract's initial setup. - /// @custom:modifiers withValidSetup - modifier active() { - if (!_active) { - revert InvalidPolicyInitialization(); - } - _; - } - constructor( address rightsPolicyManager, address rightsAuthorizer, @@ -121,11 +96,6 @@ abstract contract PolicyBase is ERC165, IPolicy { ASSET_OWNERSHIP = IAssetOwnership(assetOwnership); } - /// @notice Checks if the policy has been initialized. - function isActive() external view returns (bool) { - return _active; - } - /// @notice Retrieves the address of the attestation provider. /// @return The address of the provider associated with the policy. function getAttestationProvider() external view returns (address) { diff --git a/contracts/rights/RightsAssetCustodian.sol b/contracts/rights/RightsAssetCustodian.sol index 53fba9f..1832951 100644 --- a/contracts/rights/RightsAssetCustodian.sol +++ b/contracts/rights/RightsAssetCustodian.sol @@ -202,8 +202,9 @@ contract RightsAssetCustodian is Initializable, UUPSUpgradeable, AccessControlle // Example: First node = weight 3 * 10,000 / total weight (s) // This ensures nodes with higher weights (closer to the start) have a greater // probability of being selected. - acc += ((n - i) * C.BPS_MAX) / s; // TODO add "demand" as variable to calc weight address candidate = _custodiansByHolder[holder].at(i); + // uint256 demand = _holdersUnderCustodian[candidate]; + acc += ((n - i) * C.BPS_MAX) / s; // TODO add "demand" as variable to calc weight if (acc >= random && _isValidActiveCustodian(candidate)) { chosen = candidate; } diff --git a/contracts/rights/RightsPolicyManager.sol b/contracts/rights/RightsPolicyManager.sol index 7f37cca..0c9a4c2 100644 --- a/contracts/rights/RightsPolicyManager.sol +++ b/contracts/rights/RightsPolicyManager.sol @@ -89,7 +89,7 @@ contract RightsPolicyManager is Initializable, UUPSUpgradeable, AccessControlled /// @notice Finalizes the agreement by registering the agreed-upon policy, effectively closing the agreement. /// @dev This function verifies the policy's authorization, executes the agreement and registers the policy. /// @param proof The unique identifier of the agreement to be enforced. - /// @param holder The rights holder whose authorization is required for accessing the asset. + /// @param holder The rights holder whose authorization is required for accessing the assets. /// @param policy The address of the policy contract managing the agreement. function registerPolicy( uint256 proof, diff --git a/packages/protocol/package.json b/packages/protocol/package.json index 244d87a..8cd6eb4 100644 --- a/packages/protocol/package.json +++ b/packages/protocol/package.json @@ -1,6 +1,6 @@ { "name": "@synaps3/protocol", - "version": "1.10.10", + "version": "1.10.13", "description": "Core contracts for the Synapse Protocol", "homepage": "https://github.com/Synaps3Protocol/protocol-core-v1#readme", "license": "BUSL-1.1", diff --git a/packages/types/package.json b/packages/types/package.json index d1b2714..61cf82e 100644 --- a/packages/types/package.json +++ b/packages/types/package.json @@ -1,6 +1,6 @@ { "name": "@synaps3/types", - "version": "1.10.10", + "version": "1.10.11", "description": "Essential interfaces and types for Synapse Protocol.", "homepage": "https://github.com/Synaps3Protocol/protocol-core-v1#readme", "license": "BUSL-1.1", diff --git a/test/libraries/FinancialOps.t.sol b/test/libraries/FinancialOps.t.sol index e69de29..49c96a9 100644 --- a/test/libraries/FinancialOps.t.sol +++ b/test/libraries/FinancialOps.t.sol @@ -0,0 +1 @@ +// TODO complete here \ No newline at end of file From f7d9b4f38c85ad19252f634927d4b71328a2e63b Mon Sep 17 00:00:00 2001 From: Geolffrey Mena Date: Sun, 4 May 2025 17:30:03 -0600 Subject: [PATCH 06/16] refactor: trustless escrow as the central module for custody registration fees --- .env.vault | 16 +-- Makefile | 2 +- contracts/access/AccessManager.sol | 32 +++++- contracts/assets/AssetReferendum.sol | 5 - .../custody/ICustodianRegistrable.sol | 4 +- .../economics/IFeeSchemeValidator.sol | 15 +++ contracts/core/primitives/Constants.sol | 1 + contracts/custody/CustodianFactory.sol | 2 + contracts/custody/CustodianReferendum.sol | 55 +++++---- contracts/economics/Tollgate.sol | 20 +++- contracts/financial/AgreementManager.sol | 2 +- contracts/financial/AgreementSettler.sol | 17 ++- contracts/rights/RightsAssetCustodian.sol | 1 + lib/forge-std | 2 +- lib/solady | 2 +- .../03_Deploy_Economics_Token.s.sol | 2 +- ...0_Deploy_Custody_CustodianReferendum.s.sol | 5 +- .../01_Orchestrate_ProtocolHydration.s.sol | 3 + ...Orchestrate_ProtocolCustodianNetwork.s.sol | 25 +++- .../permissions/Permissions_HookRegistry.sol | 11 ++ ..._Upgrade_Custody_CustodianReferendum.s.sol | 5 +- test/BaseTest.t.sol | 107 ++++++++++++------ test/assets/AssetReferendum.t.sol | 48 ++++---- test/assets/AssetSafe.t.sol | 27 ++--- .../CustodianImpl.t.sol} | 10 +- .../CustodianReferendum.t.sol} | 103 ++++++++++------- test/economics/Tollgate.t.sol | 6 +- test/primitives/BalanceOperator.t.sol | 3 +- 28 files changed, 333 insertions(+), 198 deletions(-) create mode 100644 contracts/core/interfaces/economics/IFeeSchemeValidator.sol create mode 100644 script/permissions/Permissions_HookRegistry.sol rename test/{syndication/DistributorImpl.t.sol => custody/CustodianImpl.t.sol} (94%) rename test/{syndication/DistributorReferendum.t.sol => custody/CustodianReferendum.t.sol} (66%) diff --git a/.env.vault b/.env.vault index a8e5b01..0cf26de 100644 --- a/.env.vault +++ b/.env.vault @@ -4,20 +4,20 @@ #/--------------------------------------------------/ # ci -DOTENV_VAULT_CI="Z2Ld9+OvCJ+nkp5oMlaRLKw7Q3ZNyvrltK3hkUYmr8OrclEQWLpMLKkTiZQRfUiNUvCsUlL6EVq7bUvZv8NUpCWHLHr2bMdlS21LBEyokXsh8iuPg67T7SW90S0exB2He/qJGU8wlaMQhdKvgenkHpkDemwu/X8P7/Ot817jtI9gB11swJ994GFtG2Kxws9+N3ZQ0+BD5nWCGSBcsxamRT+QKqnI/SwZYfrs2iJTvgWi54vcHtr5w20tsyV4KDt5dd5jeAAgbf71YQPWO7/85dvyfPrUUo24OmAi7kGlEIKRNH40qS5ctUc9HqHFqpPYCOOgU4cHZTs3G4Eu1syY2+GNDhd7RD8JJejvzDwzwvNWtf9Xyyq/RaZ7IcBp0t51UQNUreH2BZMzoNw3NKjYkF+6ftHU7roYtFCxVyumZ7AnkJKVDLgIs7DBXF4e7u/eZ3KHXRzdDB0NDDudsyQdQ7WKL0XYLSPQuDjeOW3BBd238rI7W54fUkwYQ+IeZvavrw2ed/hRKlr4QV9j0KnyXKde2YxONRO0TtA7vB5nlvqBVx9JiBTc27XpyNlpCz4M0Xm1KS6dyJLolW2YSbS3T+Ix4Z5gRw08oMKRxFbbm2tM3YVUfr9onBEK0C/TqA9cj3VcepFtkjhaUPE6NYYTbBnwW3izqax24Np/KEZUnJrJv4LV3OXMh3d+NKIpO9xTbTFBKOthvukp4qAaWYdOVmXl1S7Gz6ogx8PG4GeQ8MNI2xrMHfFQyI/tXauq57aYS3U4KMySNhHYEUeiu6wb2K50lVO29zGpFaxRc0qlBB0fuHEt14WwpGFP84tT+vpHitpYWAo07K4Bilv+5dLDNQwx8e4JZc7Ynj2UKmQGuUiJJY1RWh2cJZJ3Rqn+z5VqxUvhHi4QQ38hXms+Jmk7RjaGkQAIMlsiP/8C/Z9GVLyu9I1rmsl9HeEIsd19u48HPsgqbuKH1BfxHX49dPfR3tupy7lg0bFBrKwVYNuHW45N+Tp1PLbNcQRj0UNuT7vHF4sApHk+tc3X8wuD6FWsGddL99sYVgfCGVHQzz6+ibMQhUzMCHvSBTZF6JDF0mxjJCpoVEDid7RPAZWoDX+p2QFAuWVIezaKVfn5X1WF2BYJvZUUolIKwsYKPT4W/B6iyxiQF20HhgXh0eVS1whdyNnabuOEexWNzbRhaH5sahP1G3CxztoX1sVapveKDVBCTm+DMtDSOMyVtJCmIofrwXBNZ1xHNEZkrjXvq6JOsKcpYE2rXJINu2j2HXDRwjpS15U67u9hU7bhHdRg42mpV4++ynsgz0Df4zGCPtLgb8IG54t16LHnDcROkPArDFMTCLUT1bswzjCvWglCcS+J17A28Drdehgn9krVktXwrxcaTMKIauLT4gCHHuR4mXaR/A2876gr3eTEZ6v5JETjvWQInO+eZBd8s2dehPjR6fcK9PB4OJ1zxZ9fiC63EYYM+TiOLPKGitu1Ql03f6lFPzqyaNJkqh5U1AUUhyX4//bMQ8vdxKD4NSCq3eAqXR0bAwyk/g86wWZfHKCT1E5BPotc+hdRes1HFD95L7t8dHsrb7dD79Rx867hjpegNOdyqx6xhBSRSLtYHNZIipRs5YOpboj/NKkQk7rdNFqB2ArjvAFYywVt9tse6qM3tqQEfZc875BysWXmc1IDiC64MirG8T1MTRku+MmSpiTNL6IC52+3jHFHbB3bOEErPCoRZjTOFp3zY/8BEUgJlUXSKlQjjnvr0+Z+ReyUQb39qXR9oPtckWLqpcTp0hX8DK5ghqwR4tgOsItMj6NW8FABuE9/8sbzhZYwJrUd4DnoJVdufqE4gczGoC/KtqwC4AtcdV3UsF0=" -DOTENV_VAULT_CI_VERSION=7 +DOTENV_VAULT_CI="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" +DOTENV_VAULT_CI_VERSION=8 # staging -DOTENV_VAULT_STAGING="Mnk49jK9XdRqHLaxkuAI9+04IdKjfVayfCaLnM0eHH4qtX7wCKjSQW8pt/d1fMQKYjJqCXPcg8MDhSOwAMtS4aJkSJNNlJSf+GRbPXio2BtFnP6G7IatBX3pAS0pkmHfvu/SFdUH3abulCSjQcrKz0fLwG6nuNlOknB/amtgAuDaPIPYcy0wZ0egQ0DUL2BchKPUmgB2GS0NPQ0tt6RqXXf2zrtz6ipOuqAJDRT1I8JEAoYACQHD6/Wa6YSPMaB8RerMNpd4q1Xu7Ock2Ddr5wI9DlfxWQ+cD+tUR7Lrq3XX+AvIjXgrdsz+kR7wpLlkVrKTxcMBObsBZNGPWRHc7YqAHGDYMRAdT04awZS3ka6tn5nZgAKnHU/Z6jRjdJsmdSKu2y3gNw+CVBzKnkuP/aZ+y7kK0kP2isrhj6P9hkYCej0weQYIy9X2vYATZ4quweOpHrXESnakqItfk0ZpXpofjtlpBngXC1/LJDlaEiV/tkc+gjkrfL7aQpaKtfksABSB7Je1AmpePmNBotFp3oLLoQFeYs8toApz3roXdkcmRo9MaOQ4vzDHxYWRNJ6Brjz5IohdcR1U1AbuQ4EDTCeVenMx3Fa6ThG+1ZDjZS/E+1BPuC2Yd2SrFRju57Yjq3P/nvTles31pT+ba5s5vVHaYKcri+GhMLQyAl+6PPll5bv4MLPwQChUNUg6HFvo+LCeEYkaA5l5OwL3Xeg7R+ytxQG52rpG50mCqzHS8SNrHf5SDkk9RMW2/Vys4TzfVDwhum+GTFBGT5Eri+nLyrwlRKLflIRPopBuG9L0EejmZF1BT/gnVw/r67KVEHvPCCUazTHid4xv7Q0POuQFTiAv734sFvrJozwqM+49SO6ohSp39NODyZX0Fc2PsKt92Pl9n902gMD5aiZr24MpBfJDF28zmxFl/ym+ejbmTMuQotBg9oR6ShvAqP9RYEufbVTHL0yKRYwpc+EPPeswVmJhYXlTNO3qdN8BFyPpM2bm1znJVQvJDvfxqxZhCarvER7lvA4RwbHkajsp3ahi7H1Ffvg4FC+DmqyY44Hto+Zp0yyNEw8hGJeHJuQdWOdUatQq37bCOfMqM+zrzdbAqJcliY2tjaigtaBKC7nWL0hHymeovfEcH6TopeDK58m+oIVw/d5n7su90H6hvebwP/gOg5EYlRGL+n7TrwadLm0j3KllPP+qhX1XzQBZ8BnIVy0uYcObT5s4wA4b4KNsY55ZbfAlviff1pTB+r71TEbhYZ1lHEKWQLBAQHxqCyuFYD+N0kjkc2HXAplCcDFlD2VI6L9Ehm9dp+oT8REpZKD6HLcKli2xjtlLr1lcDCgKAwm3pqv9Jmw2SMxBG4Wkola3vZrdCuQeCrgMW7ZjVVTUWT8Ghl7G80aDl6h0/YHWx+NvIlrBTgaWpr6bqt+40G+iOyG6Qa1xdBw+0AGqfY2RxCueQztQwnO0clpg02/PQyR9RAX5L5g9igGuXCD8u152yZZIhiHw5PVzso1A5UtEWk9JIpb1EH02RUde8DEFBAzpaDpcBy1sriwSiofxgQJQ2BAk/QdHpvJidsU3MU9j2nEUsRUNFXi55utwyiEVCf07D4J1WFBHPij1ipphwVUex9e96zyh/yEYQ+u67b1cWg1smAxQvYdkqSht09MXfprq2tRpH6oMNDdvIKcvBI7NgJdrbP+HWSlpnc4D/umg+UF13n4+OztcNdGI4KlIQZUgRpZAwYnAng0QtzbiCVJWHvwq5Bv9pOOPkaoKAJ8LAwvjlOP1OEWO9sP3h374aYgRTik0s+VHjGtkhcjLKTkHYSVLOmxvOdq98Fi137wvIrHpXMamMae/Ah6TYDoEt5+UZGM=" -DOTENV_VAULT_STAGING_VERSION=7 +DOTENV_VAULT_STAGING="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" +DOTENV_VAULT_STAGING_VERSION=8 # production -DOTENV_VAULT_PRODUCTION="sHaPcAFF6mSG6f9iz6N2syc6aAjUCEIqPf47b044vYwqcPR7AxBA2I57tQdQjZg7vI9JXsN406WdX+qqXbidlhvkqrp5ulsTv4P9bD220Gdmz71D3sllGNUrUyGbsV6AOGa4ZAt1ZFmmt+5j4g4+BF0FQbmVFpz3BKfiiuQDNhAtUlhDzVZYg5yvqw/BaOAESLueIMrPKixHK+uIv6KxZgdPc8tUNgGfaC5X6lUjkX/l62O5yyecN2Icco7rW+KNtKeW0bKLODw/Rcob4zJcDq25+fBPPrUtslKI9oULdFsbGEYV5idieMey8uv8zP3D1AViuauT7GpqOCPPaelEcs3pxSIMt0qbgFCKzTwjKG+cZNiX7uyuWVVoCRQtVpqERuGUYg4Z56Ypm1daZcMo2ybQaHh9INKtNDuv5lRBDXkMLCBgPkJJMlNaSwmYwhI6qfcQdsIlvbAUrcVT5AwvKmxj5QyejTj1Yv4el8XNRIUlpcyGB91GQwMbL9EBQSj7FjVmHVOoEtcjGeMwrayjhxXItzRYVJdjGQWtDcZMO0vw7WeOyAEXAVk5J9Nft7EqTlVRp2d1YN9FAlWG23uIN3STkbKmwK8J+QvsVa0STTFKKaIUSfvjXZ7ZphQ6euWgBkn0FI44DtcLEH6qyxFisGLcLghnHiQ4nM+AUHC0OUv6X5QWe04zyKqPMpkg0Ft7x1oHORF+05BBWwWXzJEKqBeg6TwQa5Uvhz9hxMj2fT2kl1WJ0dSn2O/vykO2H2wl0qyAlatLkCViv9D4fGKpsxcT28jo+3hveAaezGMrsKXYNxOmlGma79NuY98gdjfXd7ckuRkdAdFAQIvDSBsxbIdIKvQWa7FBniRZlU2k+N4xFJCeicRYOw7fSgBU8g4VbqCACd1cy6IhnPlPoH6XXsmy7yr1ITjj3Ey/p7e56hJgnzFGWYBFsJzUV7ADI0p9ChA2GrTW1cwaXCTew2Ri8tcgf0BFkS1592AKOL55Xr/jeZ/zIYcytYXIXU/YnNJh8RfslUEzZW9cx4bDKIYcwetBEfw0/aqIOVCEEaZtNScKkSJyUIkR06Z0L4L7VC3IBhZRGs0WFaPXy9X83P0Ht1Xi6Br2A0fTLjuzv9iAlqfKrhj8V/Zfs8nTfcCX8cXKShWw+cM678VZmTAQ+hFgZGE7dbqtol8N4flMAZhFZOlOfCLvsxYs5P7tgY1THkXYpbme68kPxNz04O3q2GpapJFka4zVF58KAglbNc6Joi2HG/lA5Fvywh3f0HlfdoYw0FFkUVE1I8cWVGQYMu5zSyFbwaSeKdgM8tzntOrYySrbdQ8NCOuYWweJrFGlmr1ZAG88fE8a/G6qjVILws+JpvoFVvoS/HLn2Gi/YVQ8/UGLPI2PbAD6yaGUW0q77kGjHXoliWLZ65a+SlCyyAm3cIT2MNHtznspJvzqFjKlP/GRkQfG9voWzx7kDiZ98p54DDV42G1nn0u+lxHHe42mrIkhAO8bANy57HqAp+UXxgOwqeDNCiLbvN69DFJcYwH0aBjcjB018Jm2Jk/H881ttj21i23VcHN86BCBkhpV8snp9SJgeGXQBfk0OB2QGir1vISY8iaHXXumQGKmZhzhjoxA1cV2KnFv6YWNqXOUMFliAZrjaV0Wa8fGEgIxi6JRnEOxeUcpM/qpgNZtTRz1j073g5AlZxjBuQNym9raXUGgwpB+iG7rgwarSnwGmvZ1Ows4UB6LdQmi0ejIzKFfqPbHxCY21Scx2lCDOX5ceVhIAAj/l/zi1Yvsb8FcvNxWoayO96zUUEcT9m+WGXbuBltHagwtCVmi/ajaVOGPJfFDmnNOwBfYybZxfqTY5wCmk2Ac1X0=" -DOTENV_VAULT_PRODUCTION_VERSION=7 +DOTENV_VAULT_PRODUCTION="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" +DOTENV_VAULT_PRODUCTION_VERSION=8 # development -DOTENV_VAULT_DEVELOPMENT="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" -DOTENV_VAULT_DEVELOPMENT_VERSION=8 +DOTENV_VAULT_DEVELOPMENT="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" +DOTENV_VAULT_DEVELOPMENT_VERSION=9 #/----------------settings/metadata-----------------/ DOTENV_VAULT="vlt_c13402acfe780a1795cccf0f6ba62b7a1680026d5c398b258cb9787607bf9c21" diff --git a/Makefile b/Makefile index bc605df..bd03934 100644 --- a/Makefile +++ b/Makefile @@ -54,7 +54,7 @@ force-compile: .PHONY: test ## run tests test: - @CI=true && forge test --show-progress --gas-report -vvv --fail-fast + @export CI=true && forge test --show-progress --gas-report -vvvv --fail-fast .PHONY: coverage ## run tests coverage report coverage: diff --git a/contracts/access/AccessManager.sol b/contracts/access/AccessManager.sol index 86ddd5b..784502b 100644 --- a/contracts/access/AccessManager.sol +++ b/contracts/access/AccessManager.sol @@ -31,9 +31,39 @@ contract AccessManager is Initializable, UUPSUpgradeable, AccessManagerUpgradeab // return (true, getRoleAdmin(roleId), 0); // => (true, 0, 0) // } + // Strategic roles for governance classification within the protocol: + // + // Community Governance Role: + // - GOV_ROLE: Represents decentralized community governance. Decisions are made through collective voting mechanisms (e.g., token-weighted, quadratic). + // + // Group/Sub-DAO Based Roles: + // - ADMIN_ROLE: Managed by a smart account or sub-DAO. Handles protocol upgrades, pause mechanisms, and operational role assignments. + // - MOD_ROLE: Managed by a smart account or sub-DAO. Approves policy submissions and moderates hook operations. + // - REF_ROLE: Managed by a smart account or sub-DAO. Participates in governance referenda for content curation and distributor selection. + // + // Individual/Contract Based Roles: + // - OPS_ROLE: Internal operational role assigned to protocol-trusted contracts for direct module interactions. No human involvement. + // - VER_ROLE: Individual role assigned to trusted creators, enabling content uploads without conventional verification. + + /* + GOV_ROLE (Community Governance) + │ + ├── ADMIN_ROLE (Smart Account / Sub-DAO) + │ │ + │ ├── MOD_ROLE (Smart Account / Sub-DAO) + │ │ + │ └── OPS_ROLE (Internal Contract Role) + │ + ├── REF_ROLE (Smart Account / Sub-DAO) + │ + ├── VER_ROLE (Individual Trusted Creator) + */ + + _setRoleAdmin(C.VER_ROLE, C.GOV_ROLE); + _setRoleAdmin(C.REF_ROLE, C.GOV_ROLE); _setRoleAdmin(C.MOD_ROLE, C.ADMIN_ROLE); _setRoleAdmin(C.OPS_ROLE, C.ADMIN_ROLE); - _setRoleAdmin(C.VER_ROLE, C.GOV_ROLE); + } // TODO pause protocol based on permission and roles diff --git a/contracts/assets/AssetReferendum.sol b/contracts/assets/AssetReferendum.sol index 30c1f38..627b62a 100644 --- a/contracts/assets/AssetReferendum.sol +++ b/contracts/assets/AssetReferendum.sol @@ -3,8 +3,6 @@ pragma solidity 0.8.26; import { UUPSUpgradeable } from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; -import { EIP712Upgradeable } from "@openzeppelin/contracts-upgradeable/utils/cryptography/EIP712Upgradeable.sol"; -import { NoncesUpgradeable } from "@openzeppelin/contracts-upgradeable/utils/NoncesUpgradeable.sol"; import { Initializable } from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; import { EnumerableSet } from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; @@ -21,8 +19,6 @@ contract AssetReferendum is Initializable, UUPSUpgradeable, AccessControlledUpgradeable, - NoncesUpgradeable, - EIP712Upgradeable, QuorumUpgradeable, IAssetReferendum { @@ -67,7 +63,6 @@ contract AssetReferendum is function initialize(address accessManager) public initializer { __Quorum_init(); __UUPSUpgradeable_init(); - __EIP712_init("Referendum", "1"); __AccessControlled_init(accessManager); } diff --git a/contracts/core/interfaces/custody/ICustodianRegistrable.sol b/contracts/core/interfaces/custody/ICustodianRegistrable.sol index e449416..07ad11b 100644 --- a/contracts/core/interfaces/custody/ICustodianRegistrable.sol +++ b/contracts/core/interfaces/custody/ICustodianRegistrable.sol @@ -8,9 +8,9 @@ pragma solidity 0.8.26; /// Functions here are semantically equivalent to the FSM transitions: register → approve. interface ICustodianRegistrable { /// @notice Registers data with a given identifier. - /// @param custodian The address of the custodian to register. + /// @param proof The unique identifier of the agreement to be enforced. /// @param currency The currency used to pay enrollment. - function register(address custodian, address currency) external; + function register(uint256 proof, address currency) external; /// @notice Approves the data associated with the given identifier. /// @param custodian The address of the custodian to approve. diff --git a/contracts/core/interfaces/economics/IFeeSchemeValidator.sol b/contracts/core/interfaces/economics/IFeeSchemeValidator.sol new file mode 100644 index 0000000..5f6f38b --- /dev/null +++ b/contracts/core/interfaces/economics/IFeeSchemeValidator.sol @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: BUSL-1.1 +// NatSpec format convention - https://docs.soliditylang.org/en/v0.5.10/natspec-format.html +pragma solidity 0.8.26; + +import { T } from "@synaps3/core/primitives/Types.sol"; + +/// @title IFeeSchemeValidator +/// @notice Interface for authorizing which fee schemes are accepted in a protocol context. +/// @dev This contract represents a context-bound authorizer, so no target is passed. +interface IFeeSchemeValidator { + /// @notice Returns true if the fee scheme is supported by this contract. + /// @param scheme The scheme to check. + /// @return True if supported, false if explicitly not supported. + function isFeeSchemeSupported(T.Scheme scheme) external view returns (bool); +} diff --git a/contracts/core/primitives/Constants.sol b/contracts/core/primitives/Constants.sol index a164c63..5294ebe 100644 --- a/contracts/core/primitives/Constants.sol +++ b/contracts/core/primitives/Constants.sol @@ -16,6 +16,7 @@ library C { uint64 internal constant MOD_ROLE = 2; // moderator role uint64 internal constant VER_ROLE = 3; // account verified role uint64 internal constant OPS_ROLE = 4; // operations roles + uint64 internal constant REF_ROLE = 5; // referendum roles bytes32 internal constant REFERENDUM_SUBMIT_TYPEHASH = keccak256("Submission(uint256 assetId, address initiator, uint256 nonce)"); diff --git a/contracts/custody/CustodianFactory.sol b/contracts/custody/CustodianFactory.sol index d7710ec..8cd724a 100644 --- a/contracts/custody/CustodianFactory.sol +++ b/contracts/custody/CustodianFactory.sol @@ -51,6 +51,8 @@ contract CustodianFactory is UpgradeableBeacon, ICustodianFactory { return _manager[custodian]; } + // TODO: isRegistered(endpoint) + /// @notice Function to create a new custodian contract. /// @dev Ensures that the same endpoint is not registered twice. /// @param endpoint The endpoint associated with the new custodian. diff --git a/contracts/custody/CustodianReferendum.sol b/contracts/custody/CustodianReferendum.sol index 00e9a75..7938f63 100644 --- a/contracts/custody/CustodianReferendum.sol +++ b/contracts/custody/CustodianReferendum.sol @@ -6,14 +6,12 @@ import { ERC165Checker } from "@openzeppelin/contracts/utils/introspection/ERC16 import { UUPSUpgradeable } from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; import { Initializable } from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; // solhint-disable-next-line max-line-length -import { ReentrancyGuardTransientUpgradeable } from "@openzeppelin/contracts-upgradeable/utils/ReentrancyGuardTransientUpgradeable.sol"; import { AccessControlledUpgradeable } from "@synaps3/core/primitives/upgradeable/AccessControlledUpgradeable.sol"; -import { FeesCollectorUpgradeable } from "@synaps3/core/primitives/upgradeable/FeesCollectorUpgradeable.sol"; import { QuorumUpgradeable } from "@synaps3/core/primitives/upgradeable/QuorumUpgradeable.sol"; import { ITollgate } from "@synaps3/core/interfaces/economics/ITollgate.sol"; -import { ITreasury } from "@synaps3/core/interfaces/economics/ITreasury.sol"; -import { ILedgerVault } from "@synaps3/core/interfaces/financial/ILedgerVault.sol"; import { ICustodian } from "@synaps3/core/interfaces/custody/ICustodian.sol"; +import { IAgreementSettler } from "@synaps3/core/interfaces/financial/IAgreementSettler.sol"; +import { IFeeSchemeValidator } from "@synaps3/core/interfaces/economics/IFeeSchemeValidator.sol"; import { ICustodianReferendum } from "@synaps3/core/interfaces/custody/ICustodianReferendum.sol"; import { FinancialOps } from "@synaps3/core/libraries/FinancialOps.sol"; import { T } from "@synaps3/core/primitives/Types.sol"; @@ -28,9 +26,8 @@ contract CustodianReferendum is UUPSUpgradeable, QuorumUpgradeable, AccessControlledUpgradeable, - ReentrancyGuardTransientUpgradeable, - FeesCollectorUpgradeable, - ICustodianReferendum + ICustodianReferendum, + IFeeSchemeValidator { using FinancialOps for address; using ERC165Checker for address; @@ -38,14 +35,11 @@ contract CustodianReferendum is /// @dev Stores the interface ID for ICustodian, ensuring compatibility verification. bytes4 private constant INTERFACE_ID_CUSTODIAN = type(ICustodian).interfaceId; - ///Our immutables behave as constants after deployment - //slither-disable-start naming-convention + ///Our immutables behave as constants after deployment //slither-disable-start naming-convention /// @custom:oz-upgrades-unsafe-allow state-variable-immutable ITollgate public immutable TOLLGATE; /// @custom:oz-upgrades-unsafe-allow state-variable-immutable - ITreasury public immutable TREASURY; - /// @custom:oz-upgrades-unsafe-allow state-variable-immutable - ILedgerVault public immutable LEDGER_VAULT; + IAgreementSettler public immutable AGREEMENT_SETTLER; //slither-disable-end naming-convention /// @dev Defines the expiration period for enrollment, determining how long a custodian remains active. @@ -76,9 +70,9 @@ contract CustodianReferendum is /// @param invalid The address of the custodian contract that is invalid error InvalidCustodianContract(address invalid); - /// @notice Error thrown when an invalid fee scheme is provided for a referendum operation. - /// @param message A descriptive message explaining the reason for the invalid fee scheme. - error InvalidFeeSchemeProvided(string message); + /// @notice Error thrown when an invalid param is provided for a referendum registration. + /// @param message A descriptive message explaining the reason for the invalid. + error InvalidRegisterParams(string message); /// @notice Modifier to ensure that the given custodian contract supports the ICustodian interface. /// @param custodian The custodian contract address. @@ -90,26 +84,31 @@ contract CustodianReferendum is } /// @custom:oz-upgrades-unsafe-allow constructor - constructor(address treasury, address tollgate, address ledgerVault) { + constructor(address tollgate, address agreementSettler) { /// https://forum.openzeppelin.com/t/what-does-disableinitializers-function-mean/28730/5 /// https://forum.openzeppelin.com/t/uupsupgradeable-vulnerability-post-mortem/15680 _disableInitializers(); - TREASURY = ITreasury(treasury); + AGREEMENT_SETTLER = IAgreementSettler(agreementSettler); TOLLGATE = ITollgate(tollgate); - LEDGER_VAULT = ILedgerVault(ledgerVault); } /// @notice Initializes the proxy state. function initialize(address accessManager) public initializer { __Quorum_init(); __UUPSUpgradeable_init(); - __ReentrancyGuardTransient_init(); __AccessControlled_init(accessManager); - __FeesCollector_init(address(TREASURY)); // 6 months initially.. _expirationPeriod = 180 days; } + /// @notice Checks if the given fee scheme is supported in this context. + /// @param scheme The fee scheme to validate. + /// @return True if the scheme is supported. + function isFeeSchemeSupported(T.Scheme scheme) external pure returns (bool) { + // support only FLAT scheme + return scheme == T.Scheme.FLAT; + } + /// @notice Retrieves the current expiration period for enrollments or registrations. function getExpirationPeriod() external view returns (uint256) { return _expirationPeriod; @@ -163,9 +162,10 @@ contract CustodianReferendum is } /// @notice Registers a custodian by sending a payment to the contract. + /// @param proof The unique identifier of the agreement to be enforced. /// @param custodian The address of the custodian to register. - /// @param currency The currency used to pay enrollment. - function register(address custodian, address currency) external onlyValidCustodian(custodian) { + function register(uint256 proof, address custodian) external onlyValidCustodian(custodian) { + /// TODO penalize invalid endpoints, and revoked during referendum // !IMPORTANT: // Fees act as a mechanism to prevent abuse or spam by users // when submitting custodians for approval. This discourages users from @@ -178,18 +178,15 @@ contract CustodianReferendum is // The collected fees are used to support the protocol's operations, aligning // individual actions with the broader sustainability of the network. // !IMPORTANT If tollgate does not support the currency, will revert.. - (uint256 fees, T.Scheme scheme) = TOLLGATE.getFees(address(this), currency); - if (scheme != T.Scheme.FLAT) revert InvalidFeeSchemeProvided("Expected a FLAT fee scheme."); - /// TODO penalize invalid endpoints, and revoked during referendum - uint256 locked = LEDGER_VAULT.lock(msg.sender, fees, currency); // lock funds - uint256 claimed = LEDGER_VAULT.claim(msg.sender, locked, currency); // claim the funds on behalf - uint256 confirmed = LEDGER_VAULT.withdraw(address(this), claimed, currency); // collect funds + T.Agreement memory agreement = AGREEMENT_SETTLER.settleAgreement(proof, msg.sender); + if (custodian != agreement.parties[0]) revert InvalidRegisterParams("Custodian is not part of the agreement."); + // register custodian as pending approval _register(uint160(custodian)); // set the custodian active enrollment period.. // after this time the custodian is considered inactive and cannot collect his profits... _enrollmentDeadline[custodian] = block.timestamp + _expirationPeriod; - emit Registered(custodian, confirmed); + emit Registered(custodian, agreement.fees); } /// @notice Approves a custodian's registration. diff --git a/contracts/economics/Tollgate.sol b/contracts/economics/Tollgate.sol index bdd3ef1..1c602a9 100644 --- a/contracts/economics/Tollgate.sol +++ b/contracts/economics/Tollgate.sol @@ -6,6 +6,7 @@ import { Initializable } from "@openzeppelin/contracts-upgradeable/proxy/utils/I import { EnumerableSet } from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; import { UUPSUpgradeable } from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; import { AccessControlledUpgradeable } from "@synaps3/core/primitives/upgradeable/AccessControlledUpgradeable.sol"; +import { IFeeSchemeValidator } from "@synaps3/core/interfaces/economics/IFeeSchemeValidator.sol"; import { ITollgate } from "@synaps3/core/interfaces/economics/ITollgate.sol"; import { T } from "@synaps3/core/primitives/Types.sol"; @@ -61,6 +62,23 @@ contract Tollgate is Initializable, UUPSUpgradeable, AccessControlledUpgradeable _; } + /// @notice Allows execution if the scheme is accepted or if support cannot be determined. + /// @dev This modifier checks whether the `target` contract explicitly supports the scheme via `isSupported`. + /// If `isSupported` exists and returns `false`, the call is reverted with InvalidTargetContext. + /// If the call to `isSupported` fails (e.g., target does not implement the function or reverts), execution is allowed by default. + /// This enables compatibility with contracts that do not implement scheme validation. + /// @param scheme The scheme to validate. + /// @param target The address of the contract expected to support the scheme. + modifier onlyIfFeeSchemeSupported(T.Scheme scheme, address target) { + try IFeeSchemeValidator(target).isFeeSchemeSupported(scheme) returns (bool ok) { + if (!ok) revert InvalidTargetContext(target); + _; + } catch { + // by default all schemes are allowed + _; + } + } + /// @custom:oz-upgrades-unsafe-allow constructor constructor() { _disableInitializers(); @@ -114,7 +132,7 @@ contract Tollgate is Initializable, UUPSUpgradeable, AccessControlledUpgradeable address target, uint256 fee, address currency - ) external restricted onlyValidFeeRepresentation(scheme, fee) { + ) external onlyIfFeeSchemeSupported(scheme, target) onlyValidFeeRepresentation(scheme, fee) restricted { // Compute a unique composed key based on the target, currency, and scheme. // The composed key uniquely identifies a deterministic combination of these parameters // in a flat storage mapping. This avoids nested mappings, improving gas efficiency diff --git a/contracts/financial/AgreementManager.sol b/contracts/financial/AgreementManager.sol index e02892c..3a5ca2b 100644 --- a/contracts/financial/AgreementManager.sol +++ b/contracts/financial/AgreementManager.sol @@ -132,7 +132,7 @@ contract AgreementManager is Initializable, UUPSUpgradeable, AccessControlledUpg revert NoPartiesInAgreement(); } - // TODO Even if we are covered by gas fees, a good way to avoid abuse is penalize parties after N length + // TODO Even if we are covered by gas fees, during executing a good way to avoid abuse is penalize parties after N length // eg. The max parties allowed is 5, any extra parties are charged with a extra * fee // IMPORTANT: diff --git a/contracts/financial/AgreementSettler.sol b/contracts/financial/AgreementSettler.sol index 5c09a91..1caef1c 100644 --- a/contracts/financial/AgreementSettler.sol +++ b/contracts/financial/AgreementSettler.sol @@ -7,7 +7,6 @@ import { Initializable } from "@openzeppelin/contracts-upgradeable/proxy/utils/I import { UUPSUpgradeable } from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; import { AccessControlledUpgradeable } from "@synaps3/core/primitives/upgradeable/AccessControlledUpgradeable.sol"; // solhint-disable-next-line max-line-length -import { ReentrancyGuardTransientUpgradeable } from "@openzeppelin/contracts-upgradeable/utils/ReentrancyGuardTransientUpgradeable.sol"; import { FeesCollectorUpgradeable } from "@synaps3/core/primitives/upgradeable/FeesCollectorUpgradeable.sol"; import { ILedgerVault } from "@synaps3/core/interfaces/financial/ILedgerVault.sol"; @@ -27,7 +26,6 @@ contract AgreementSettler is Initializable, UUPSUpgradeable, AccessControlledUpgradeable, - ReentrancyGuardTransientUpgradeable, FeesCollectorUpgradeable, IAgreementSettler { @@ -101,7 +99,6 @@ contract AgreementSettler is __UUPSUpgradeable_init(); __AccessControlled_init(accessManager); __FeesCollector_init(address(TREASURY)); - __ReentrancyGuardTransient_init(); } // TODO add hook management for royalties and earning splits @@ -118,7 +115,7 @@ contract AgreementSettler is /// @notice Allows the initiator to quit the agreement and receive the committed funds. /// @param proof The unique identifier of the agreement. - function quitAgreement(uint256 proof) external onlyValidAgreement(proof) nonReentrant returns (T.Agreement memory) { + function quitAgreement(uint256 proof) external onlyValidAgreement(proof) returns (T.Agreement memory) { T.Agreement memory agreement = AGREEMENT_MANAGER.getAgreement(proof); if (agreement.initiator != msg.sender) revert UnauthorizedInitiator(); @@ -142,10 +139,11 @@ contract AgreementSettler is _setProofAsSettled(proof); // slither-disable-start unused-return - // part of the agreement locked amount is released to the account + LEDGER_VAULT.claim(initiator, fees, currency); - LEDGER_VAULT.release(initiator, available, currency); LEDGER_VAULT.withdraw(address(this), fees, currency); + // part of the agreement locked amount is released to the account + if (available > 0) LEDGER_VAULT.release(initiator, available, currency); // slither-disable-end unused-return emit AgreementCancelled(initiator, proof, fees); return agreement; @@ -167,18 +165,17 @@ contract AgreementSettler is uint256 available = total - fees; // holder earnings address initiator = agreement.initiator; address currency = agreement.currency; + // TODO: Implement a time window to enforce the validity period for agreement settlement. // Once the window expires, the agreement should be marked as invalid or revert, // then quit is only way to close the agreement. _setProofAsSettled(proof); - // slither-disable-start unused-return // move the funds to settler and transfer the available to counterparty LEDGER_VAULT.claim(initiator, total, currency); - LEDGER_VAULT.transfer(counterparty, available, currency); LEDGER_VAULT.withdraw(address(this), fees, currency); - // slither-disable-end unused-return - + // could exists cases where available become zero when fees are flat + if (available > 0) LEDGER_VAULT.transfer(counterparty, available, currency); emit AgreementSettled(msg.sender, counterparty, proof, fees); return agreement; } diff --git a/contracts/rights/RightsAssetCustodian.sol b/contracts/rights/RightsAssetCustodian.sol index 1832951..8ac6c37 100644 --- a/contracts/rights/RightsAssetCustodian.sol +++ b/contracts/rights/RightsAssetCustodian.sol @@ -117,6 +117,7 @@ contract RightsAssetCustodian is Initializable, UUPSUpgradeable, AccessControlle // TODO using the maxAvailable we could limit the number of balanced custodians eg: 5 // to allow add more redundancy like "backup" but under max control to handle balanced // window=[max=[0...5]...10]... later [max=[0...6]...10] <- expanded max to 6 + // TODO grant custody fee to avoid manipulation of the network or abuse -> the more custodian = more fees bool addedCustodian = _custodiansByHolder[msg.sender].add(custodian); if (!addedCustodian) revert GrantCustodyFailed(custodian, msg.sender); _incrementCustody(custodian); // +1 under custody its analog to "demand" diff --git a/lib/forge-std b/lib/forge-std index 3353993..77041d2 160000 --- a/lib/forge-std +++ b/lib/forge-std @@ -1 +1 @@ -Subproject commit 3353993420c6e488a2914ce02d88174e80ad80f8 +Subproject commit 77041d2ce690e692d6e03cc812b57d1ddaa4d505 diff --git a/lib/solady b/lib/solady index dcdfab8..bf973bd 160000 --- a/lib/solady +++ b/lib/solady @@ -1 +1 @@ -Subproject commit dcdfab80f4e6cb9ac35c91610b2a2ec42689ec79 +Subproject commit bf973bd715908728ea81e49100629f7f26ecf566 diff --git a/script/deployment/03_Deploy_Economics_Token.s.sol b/script/deployment/03_Deploy_Economics_Token.s.sol index ee7ee07..90e20d4 100644 --- a/script/deployment/03_Deploy_Economics_Token.s.sol +++ b/script/deployment/03_Deploy_Economics_Token.s.sol @@ -9,7 +9,7 @@ contract DeployToken is DeployBase { uint256 privateKey = getAdminPK(); address publicKey = vm.addr(privateKey); - vm.startBroadcast(getAdminPK()); + vm.startBroadcast(privateKey); bytes memory creationCode = type(MMC).creationCode; // add constructor args as 1,000,000,000 MMC initial supply bytes memory initCode = abi.encodePacked(creationCode, abi.encode(publicKey, 1_000_000_000)); diff --git a/script/deployment/10_Deploy_Custody_CustodianReferendum.s.sol b/script/deployment/10_Deploy_Custody_CustodianReferendum.s.sol index 64306ec..758d4a8 100644 --- a/script/deployment/10_Deploy_Custody_CustodianReferendum.s.sol +++ b/script/deployment/10_Deploy_Custody_CustodianReferendum.s.sol @@ -8,11 +8,10 @@ import { C } from "contracts/core/primitives/Constants.sol"; contract DeployCustodianReferendum is DeployBase { function run() external returns (address) { vm.startBroadcast(getAdminPK()); - address treasury = computeCreate3Address("SALT_TREASURY"); address tollgate = computeCreate3Address("SALT_TOLLGATE"); - address vault = computeCreate3Address("SALT_LEDGER_VAULT"); + address agreementSettler = computeCreate3Address("SALT_AGREEMENT_SETTLER"); address accessManager = computeCreate3Address("SALT_ACCESS_MANAGER"); - address impl = address(new CustodianReferendum(treasury, tollgate, vault)); + address impl = address(new CustodianReferendum(tollgate, agreementSettler)); bytes memory init = abi.encodeCall(CustodianReferendum.initialize, (accessManager)); address referendum = deployUUPS(impl, init, "SALT_CUSTODIAN_REFERENDUM"); vm.stopBroadcast(); diff --git a/script/orchestration/01_Orchestrate_ProtocolHydration.s.sol b/script/orchestration/01_Orchestrate_ProtocolHydration.s.sol index 33475d2..06e9861 100644 --- a/script/orchestration/01_Orchestrate_ProtocolHydration.s.sol +++ b/script/orchestration/01_Orchestrate_ProtocolHydration.s.sol @@ -13,6 +13,7 @@ import { getGovPermissions as CustodianReferendumGovPermissions } from "script/p import { getGovPermissions as AssetReferendumGovPermissions } from "script/permissions/Permissions_AssetReferendum.sol"; import { getModPermissions as PolicyAuditorModPermissions } from "script/permissions/Permissions_PolicyAuditor.sol"; import { getOpsPermissions as LedgerVaultOpsPermissions } from "script/permissions/Permissions_LedgerVault.sol"; +import { getModPermissions as HooksModPermissions } from "script/permissions/Permissions_HookRegistry.sol"; contract OrchestrateProtocolHydration is Script { function run() external { @@ -51,8 +52,10 @@ contract OrchestrateProtocolHydration is Script { // assign moderation permissions authority.grantRole(C.MOD_ROLE, adminAddress, 0); + bytes4[] memory hookModAllowed = HooksModPermissions(); bytes4[] memory auditorAllowed = PolicyAuditorModPermissions(); authority.setTargetFunctionRole(auditorAddress, auditorAllowed, C.MOD_ROLE); + // authority.setTargetFunctionRole(auditorAddress, hookModAllowed, C.MOD_ROLE); // assign operations permissions authority.grantRole(C.OPS_ROLE, agreementManager, 0); diff --git a/script/orchestration/02_Orchestrate_ProtocolCustodianNetwork.s.sol b/script/orchestration/02_Orchestrate_ProtocolCustodianNetwork.s.sol index c011b32..edeed37 100644 --- a/script/orchestration/02_Orchestrate_ProtocolCustodianNetwork.s.sol +++ b/script/orchestration/02_Orchestrate_ProtocolCustodianNetwork.s.sol @@ -7,15 +7,17 @@ import { ILedgerVault } from "contracts/core/interfaces/financial/ILedgerVault.s import { ICustodian } from "contracts/core/interfaces/custody/ICustodian.sol"; import { ICustodianFactory } from "contracts/core/interfaces/custody/ICustodianFactory.sol"; import { ICustodianReferendum } from "contracts/core/interfaces/custody/ICustodianReferendum.sol"; +import { IAgreementManager } from "contracts/core/interfaces/financial/IAgreementManager.sol"; contract OrchestrateProtocolCustodianNetwork is DeployBase { function run() external { uint256 admin = getAdminPK(); address mmc = vm.envAddress("MMC"); - uint256 synFees = vm.envUint("CUSTODY_FEES"); // 100 MMC flat fee + uint256 fees = vm.envUint("CUSTODY_FEES"); // 100 MMC flat fee address vault = computeCreate3Address("SALT_LEDGER_VAULT"); address custodianFactory = vm.envAddress("CUSTODIAN_FACTORY"); address custodianReferendum = vm.envAddress("CUSTODIAN_REFERENDUM"); + address agreementManager = vm.envAddress("AGREEMENT_MANAGER"); vm.startBroadcast(admin); // approve initial custodian @@ -29,10 +31,23 @@ contract OrchestrateProtocolCustodianNetwork is DeployBase { require(got == expected); // deposit funds to register custodian - IERC20(mmc).approve(vault, synFees); - ILedgerVault(vault).deposit(vm.addr(admin), synFees, mmc); - - referendum.register(address(custodian), mmc); + IERC20(mmc).approve(vault, fees); + ILedgerVault(vault).deposit(vm.addr(admin), fees, mmc); + ILedgerVault(vault).approve(address(referendum), fees, mmc); + + address custody = address(custodian); + address[] memory parties = new address[](1); + parties[0] = custody; + + uint256 proof = IAgreementManager(agreementManager).createAgreement( + fees, + mmc, + address(referendum), + parties, + "" + ); + + referendum.register(proof, address(custodian)); referendum.approve(address(custodian)); vm.stopBroadcast(); diff --git a/script/permissions/Permissions_HookRegistry.sol b/script/permissions/Permissions_HookRegistry.sol new file mode 100644 index 0000000..f9eb6d8 --- /dev/null +++ b/script/permissions/Permissions_HookRegistry.sol @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.26; +import { HookRegistry} from "contracts/lifecycle/HookRegistry.sol"; + +function getModPermissions() pure returns (bytes4[] memory) { + bytes4[] memory auditorAllowed = new bytes4[](2); + auditorAllowed[0] = HookRegistry.submit.selector; + auditorAllowed[1] = HookRegistry.approve.selector; + auditorAllowed[2] = HookRegistry.reject.selector; + return auditorAllowed; +} diff --git a/script/upgrades/10_Upgrade_Custody_CustodianReferendum.s.sol b/script/upgrades/10_Upgrade_Custody_CustodianReferendum.s.sol index 80b7c7e..354154a 100644 --- a/script/upgrades/10_Upgrade_Custody_CustodianReferendum.s.sol +++ b/script/upgrades/10_Upgrade_Custody_CustodianReferendum.s.sol @@ -8,10 +8,9 @@ import { C } from "contracts/core/primitives/Constants.sol"; contract UpgradeCustodianReferendum is UpgradeBase { function run() external returns (address) { vm.startBroadcast(getAdminPK()); - address treasuryAddress = vm.envAddress("TREASURY"); + address agreementSettler = vm.envAddress("AGREEMENT_SETTLER"); address tollgateAddress = vm.envAddress("TOLLGATE"); - address ledgerVault = vm.envAddress("LEDGER_VAULT"); - address impl = address(new CustodianReferendum(treasuryAddress, tollgateAddress, ledgerVault)); + address impl = address(new CustodianReferendum(tollgateAddress, agreementSettler)); address referendumProxy = vm.envAddress("CUSTODIAN_REFERENDUM"); // address accessManager = vm.envAddress("ACCESS_MANAGER"); //!IMPORTANT: This is not a safe upgrade, take any caution or 2-check needed before run this method diff --git a/test/BaseTest.t.sol b/test/BaseTest.t.sol index 2f42e03..c7a7afc 100644 --- a/test/BaseTest.t.sol +++ b/test/BaseTest.t.sol @@ -7,13 +7,15 @@ import { DeployCreate3Factory } from "script/deployment/01_Deploy_Base_Create3.s import { DeployAccessManager } from "script/deployment/02_Deploy_Access_AccessManager.s.sol"; import { DeployTollgate } from "script/deployment/04_Deploy_Economics_Tollgate.s.sol"; import { DeployToken } from "script/deployment/03_Deploy_Economics_Token.s.sol"; -import { DeployLedgerVault } from "script/deployment/06_Deploy_Financial_LedgerVault.s.sol"; import { DeployTreasury } from "script/deployment/05_Deploy_Economics_Treasury.s.sol"; +import { DeployLedgerVault } from "script/deployment/06_Deploy_Financial_LedgerVault.s.sol"; import { DeployAssetReferendum } from "script/deployment/11_Deploy_Assets_AssetReferendum.s.sol"; import { DeployAssetSafe } from "script/deployment/13_Deploy_Assets_AssetSafe.s.sol"; import { DeployAssetOwnership } from "script/deployment/12_Deploy_Assets_AssetOwnership.s.sol"; import { DeployCustodianFactory } from "script/deployment/09_Deploy_Custody_CustodianFactory.s.sol"; import { DeployCustodianReferendum } from "script/deployment/10_Deploy_Custody_CustodianReferendum.s.sol"; +import { DeployAgreementManager } from "script/deployment/07_Deploy_Financial_AgreementManager.s.sol"; +import { DeployAgreementSettler } from "script/deployment/08_Deploy_Financial_AgreementSettler.s.sol"; import { getGovPermissions as TollgateGovPermissions } from "script/permissions/Permissions_Tollgate.sol"; import { getGovPermissions as TreasuryGovPermissions } from "script/permissions/Permissions_Treasury.sol"; @@ -26,21 +28,39 @@ import { C } from "contracts/core/primitives/Constants.sol"; import { console } from "forge-std/console.sol"; +// dependencies are declared explicitly in each deployment even if nested or repeated to ensure deployment clarity. +// singleton pattern is used underneath to avoid multiple deployments and enforce global consistency. +// each call ensures the dependency is initialized only once and shared across consumers. abstract contract BaseTest is Test { address admin; address user; address governor; address accessManager; + + address agreementManager; + address agreementSettler; + + address assetSafe; + address assetReferendum; + address assetOwnership; + + address custodianReferendum; + address custodianFactory; + + address tollgate; + address treasury; address ledger; + address token; modifier initialize() { // setup the admin to operate in tests.. user = vm.addr(2); governor = vm.addr(1); admin = vm.addr(vm.envUint("PRIVATE_KEY")); + deployCreate3Factory(); deployAccessManager(); - deployLedgerVault(); + _; } @@ -51,14 +71,22 @@ abstract contract BaseTest is Test { address factory = create3.run(); // we need access to create3 factory globally string memory factoryAddress = Strings.toHexString(factory); + console.logString(factoryAddress); vm.setEnv("CREATE3_FACTORY", factoryAddress); } + // 03_DeployToken + function deployToken() public { + // set default admin as deployer.. + DeployToken mmcDeployer = new DeployToken(); + token = token == address(0) ? mmcDeployer.run() : token; + } + // 01_DeployAccessManager function deployAccessManager() public { // set default admin as deployer.. DeployAccessManager accessManagerDeployer = new DeployAccessManager(); - accessManager = accessManagerDeployer.run(); + accessManager = accessManager == address(0) ? accessManagerDeployer.run() : accessManager; vm.prank(admin); // add to governor the gov role @@ -67,14 +95,15 @@ abstract contract BaseTest is Test { } // 02_DeployTollgate - function deployTollgate() public returns (address) { + function deployTollgate() public { // set default admin as deployer.. + deployToken(); + DeployTollgate tollgateDeployer = new DeployTollgate(); bytes4[] memory tollgateAllowed = TollgateGovPermissions(); - address tollgate = tollgateDeployer.run(); + tollgate = tollgate == address(0) ? tollgateDeployer.run() : tollgate; // add permission to gov role to set fees _setGovPermissions(tollgate, tollgateAllowed); - return tollgate; } // 02_DeployTollgate @@ -82,7 +111,7 @@ abstract contract BaseTest is Test { // set default admin as deployer.. DeployLedgerVault ledgerDeployer = new DeployLedgerVault(); bytes4[] memory ledgerAllowed = LedgerVaultOpsPermissions(); - ledger = ledgerDeployer.run(); + ledger = ledger == address(0) ? ledgerDeployer.run() : ledger; vm.prank(admin); // op role needed to call functions in ledger contract @@ -91,68 +120,76 @@ abstract contract BaseTest is Test { return ledger; } - // 03_DeployToken - function deployToken() public returns (address) { - // set default admin as deployer.. - DeployToken mmcDeployer = new DeployToken(); - address token = mmcDeployer.run(); - return token; - } - // 04_DeployTreasury - function deployTreasury() public returns (address) { + function deployTreasury() public { // set default admin as deployer.. DeployTreasury treasuryDeployer = new DeployTreasury(); bytes4[] memory treasuryAllowed = TreasuryGovPermissions(); - address treasury = treasuryDeployer.run(); + treasury = treasury == address(0) ? treasuryDeployer.run() : treasury; _setGovPermissions(treasury, treasuryAllowed); - return treasury; + } + + function deployAgreementManager() public { + deployTollgate(); + deployLedgerVault(); + + DeployAgreementManager agreementManagerDeployer = new DeployAgreementManager(); + agreementManager = agreementManager == address(0) ? agreementManagerDeployer.run() : agreementManager; + } + + function deployAgreementSettler() public { + deployTreasury(); + deployLedgerVault(); + deployAgreementManager(); + + DeployAgreementSettler agreementSettlerDeployer = new DeployAgreementSettler(); + agreementSettler = agreementSettler == address(0) ? agreementSettlerDeployer.run() : agreementSettler; } // 05_DeployAssetReferendum - function deployAssetReferendum() public returns (address) { + function deployAssetReferendum() public { // set default admin as deployer.. DeployAssetReferendum assetReferendumDeployer = new DeployAssetReferendum(); bytes4[] memory referendumAllowed = AssetReferendumGovPermissions(); - address assetReferendum = assetReferendumDeployer.run(); + assetReferendum = assetReferendum == address(0) ? assetReferendumDeployer.run() : assetReferendum; _setGovPermissions(assetReferendum, referendumAllowed); - return assetReferendum; } - function deployAssetOwnership() public returns (address) { + function deployAssetOwnership() public { + deployAssetReferendum(); // set default admin as deployer.. DeployAssetOwnership assetOwnershipDeployer = new DeployAssetOwnership(); - address assetReferendum = assetOwnershipDeployer.run(); - return assetReferendum; + assetOwnership = assetOwnership == address(0) ? assetOwnershipDeployer.run() : assetOwnership; } - function deployAssetSafe() public returns (address) { - // set default admin as deployer.. + function deployAssetSafe() public { + deployAssetOwnership(); + DeployAssetSafe assetVaultDeployer = new DeployAssetSafe(); bytes4[] memory referendumAllowed = AssetReferendumGovPermissions(); - address assetReferendum = assetVaultDeployer.run(); - _setGovPermissions(assetReferendum, referendumAllowed); - return assetReferendum; + assetSafe = assetSafe == address(0) ? assetVaultDeployer.run() : assetSafe; + _setGovPermissions(assetSafe, referendumAllowed); } - // 08_DeployCustodian - function deployCustodianFactory() public returns (address) { + function deployCustodianFactory() public { DeployCustodianFactory distDeployer = new DeployCustodianFactory(); - return distDeployer.run(); + custodianFactory = custodianFactory == address(0) ? distDeployer.run() : custodianFactory; } // 09_DeployCustodianReferendum - function deployCustodianReferendum() public returns (address) { + function deployCustodianReferendum() public { + deployTollgate(); + deployAgreementSettler(); + // set default admin as deployer.. DeployCustodianReferendum distReferendumDeployer = new DeployCustodianReferendum(); bytes4[] memory custodianReferendumAllowed = CustodianReferendumGovPermissions(); - address custodianReferendum = distReferendumDeployer.run(); + custodianReferendum = custodianReferendum == address(0) ? distReferendumDeployer.run() : custodianReferendum; // OP role granted to custodian referendum to operate on ledger _assignOpRole(custodianReferendum); // GOV permission set to custodian referendum functions _setGovPermissions(custodianReferendum, custodianReferendumAllowed); - return custodianReferendum; } function _setGovPermissions(address target, bytes4[] memory allowed) public { diff --git a/test/assets/AssetReferendum.t.sol b/test/assets/AssetReferendum.t.sol index 94ba831..bd42994 100644 --- a/test/assets/AssetReferendum.t.sol +++ b/test/assets/AssetReferendum.t.sol @@ -12,25 +12,23 @@ import { T } from "contracts/core/primitives/Types.sol"; import { C } from "contracts/core/primitives/Constants.sol"; contract AssetReferendumTest is BaseTest { - address referendum; - function setUp() public initialize { // setup the access manager to use during tests.. - referendum = deployAssetReferendum(); + deployAssetReferendum(); } function test_Submit_SubmittedEventEmitted() public { vm.warp(1641070800); vm.prank(user); - vm.expectEmit(true, false, false, true, address(referendum)); + vm.expectEmit(true, false, false, true, address(assetReferendum)); emit AssetReferendum.Submitted(user, 1); - IAssetRegistrable(referendum).submit(1); + IAssetRegistrable(assetReferendum).submit(1); } function test_Submit_SubmittedValidStates() public { _submitContentAsUser(1); - assertFalse(IAssetVerifiable(referendum).isActive(1)); - assertFalse(IAssetVerifiable(referendum).isApproved(user, 1)); + assertFalse(IAssetVerifiable(assetReferendum).isActive(1)); + assertFalse(IAssetVerifiable(assetReferendum).isApproved(user, 1)); } function test_Approve_ApprovedEventEmitted() public { @@ -38,9 +36,9 @@ contract AssetReferendumTest is BaseTest { _submitContentAsUser(assetId); vm.warp(1641070805); vm.startPrank(governor); // approve by governance.. - vm.expectEmit(false, false, false, true, address(referendum)); + vm.expectEmit(false, false, false, true, address(assetReferendum)); emit AssetReferendum.Approved(assetId); - IAssetRegistrable(referendum).approve(assetId); + IAssetRegistrable(assetReferendum).approve(assetId); vm.stopPrank(); } @@ -49,9 +47,9 @@ contract AssetReferendumTest is BaseTest { _submitContentAsUser(assetId); vm.prank(governor); // approve by governance.. - IAssetRegistrable(referendum).approve(assetId); - assertTrue(IAssetVerifiable(referendum).isActive(assetId)); - assertTrue(IAssetVerifiable(referendum).isApproved(user, assetId)); + IAssetRegistrable(assetReferendum).approve(assetId); + assertTrue(IAssetVerifiable(assetReferendum).isActive(assetId)); + assertTrue(IAssetVerifiable(assetReferendum).isApproved(user, assetId)); } function test_Reject_RejectedEventEmitted() public { @@ -59,9 +57,9 @@ contract AssetReferendumTest is BaseTest { _submitContentAsUser(assetId); vm.warp(1641070805); vm.prank(governor); // approve by governance.. - vm.expectEmit(false, false, false, true, address(referendum)); + vm.expectEmit(false, false, false, true, address(assetReferendum)); emit AssetReferendum.Rejected(assetId); - IAssetRegistrable(referendum).reject(assetId); + IAssetRegistrable(assetReferendum).reject(assetId); } function test_Reject_RejectedValidStates() public { @@ -69,9 +67,9 @@ contract AssetReferendumTest is BaseTest { _submitContentAsUser(assetId); vm.prank(governor); // approve by governance.. - IAssetRegistrable(referendum).reject(assetId); - assertFalse(IAssetVerifiable(referendum).isActive(assetId)); - assertFalse(IAssetVerifiable(referendum).isApproved(user, assetId)); + IAssetRegistrable(assetReferendum).reject(assetId); + assertFalse(IAssetVerifiable(assetReferendum).isActive(assetId)); + assertFalse(IAssetVerifiable(assetReferendum).isApproved(user, assetId)); } function test_Revoked_RevokedEventEmitted() public { @@ -81,11 +79,11 @@ contract AssetReferendumTest is BaseTest { vm.startPrank(governor); // approve by governance.. // first an approval should ve done // then a revoke should ve done - IAssetRegistrable(referendum).approve(assetId); + IAssetRegistrable(assetReferendum).approve(assetId); - vm.expectEmit(false, false, false, true, address(referendum)); + vm.expectEmit(false, false, false, true, address(assetReferendum)); emit AssetReferendum.Revoked(assetId); - IAssetRegistrable(referendum).revoke(assetId); + IAssetRegistrable(assetReferendum).revoke(assetId); vm.stopPrank(); // reject by governance.. } @@ -93,21 +91,21 @@ contract AssetReferendumTest is BaseTest { uint256 assetId = 1; _submitAndApproveContent(assetId); vm.prank(governor); // approve by governance.. - IAssetRegistrable(referendum).revoke(assetId); - assertFalse(IAssetVerifiable(referendum).isActive(assetId)); - assertFalse(IAssetVerifiable(referendum).isApproved(user, assetId)); + IAssetRegistrable(assetReferendum).revoke(assetId); + assertFalse(IAssetVerifiable(assetReferendum).isActive(assetId)); + assertFalse(IAssetVerifiable(assetReferendum).isApproved(user, assetId)); } function _submitAndApproveContent(uint256 assetId) internal { _submitContentAsUser(assetId); vm.warp(1641070805); vm.startPrank(governor); // approve by governance.. - IAssetRegistrable(referendum).approve(assetId); + IAssetRegistrable(assetReferendum).approve(assetId); vm.stopPrank(); // approve by governance.. } function _submitContentAsUser(uint256 assetId) internal { vm.prank(user); // the default user submitting content.. - IAssetRegistrable(referendum).submit(assetId); + IAssetRegistrable(assetReferendum).submit(assetId); } } diff --git a/test/assets/AssetSafe.t.sol b/test/assets/AssetSafe.t.sol index 35434ee..1fc9051 100644 --- a/test/assets/AssetSafe.t.sol +++ b/test/assets/AssetSafe.t.sol @@ -14,15 +14,12 @@ import { T } from "contracts/core/primitives/Types.sol"; import { C } from "contracts/core/primitives/Constants.sol"; contract AssetSafeTest is BaseTest { - address safe; - address ownership; - address referendum; + function setUp() public initialize { // setup the access manager to use during tests.. - safe = deployAssetSafe(); - ownership = deployAssetOwnership(); - referendum = deployAssetReferendum(); + deployAssetSafe(); + } function test_SetContent_ValidOwner() public { @@ -31,7 +28,7 @@ contract AssetSafeTest is BaseTest { _registerAndApproveAsset(user, assetId); vm.prank(user); - IAssetSafe assetSafe = IAssetSafe(safe); + IAssetSafe assetSafe = IAssetSafe(assetSafe); assetSafe.setContent(assetId, T.Cipher.LIT, ""); assertEq(assetSafe.getContent(assetId, T.Cipher.LIT), ""); } @@ -43,7 +40,7 @@ contract AssetSafeTest is BaseTest { vm.prank(user); vm.expectRevert(abi.encodeWithSignature("InvalidAssetRightsHolder()")); - IAssetSafe assetSafe = IAssetSafe(safe); + IAssetSafe assetSafe = IAssetSafe(assetSafe); assetSafe.setContent(assetId, T.Cipher.LIT, ""); } @@ -52,9 +49,9 @@ contract AssetSafeTest is BaseTest { _registerAndApproveAsset(user, assetId); vm.prank(user); - vm.expectEmit(true, true, false, true, address(safe)); + vm.expectEmit(true, true, false, true, address(assetSafe)); emit AssetSafe.ContentStored(assetId, user, T.Cipher.LIT); - IAssetSafe assetSafe = IAssetSafe(safe); + IAssetSafe assetSafe = IAssetSafe(assetSafe); assetSafe.setContent(assetId, T.Cipher.LIT, ""); } @@ -64,7 +61,7 @@ contract AssetSafeTest is BaseTest { _registerAndApproveAsset(user, assetId); vm.prank(user); - IAssetSafe assetSafe = IAssetSafe(safe); + IAssetSafe assetSafe = IAssetSafe(assetSafe); assetSafe.setContent(assetId, T.Cipher.LIT, ""); T.Cipher safeType = assetSafe.getType(assetId); @@ -88,7 +85,7 @@ contract AssetSafeTest is BaseTest { bytes memory data = abi.encode(b64); vm.prank(user); - IAssetSafe assetSafe = IAssetSafe(safe); + IAssetSafe assetSafe = IAssetSafe(assetSafe); assetSafe.setContent(assetId, T.Cipher.LIT, data); vm.prank(admin); @@ -99,11 +96,11 @@ contract AssetSafeTest is BaseTest { function _registerAndApproveAsset(address to, uint256 assetId) private { vm.prank(to); - IAssetRegistrable(referendum).submit(assetId); + IAssetRegistrable(assetReferendum).submit(assetId); vm.prank(governor); - IAssetRegistrable(referendum).approve(assetId); + IAssetRegistrable(assetReferendum).approve(assetId); vm.prank(to); - IAssetOwnership(ownership).register(to, assetId); + IAssetOwnership(assetOwnership).register(to, assetId); } } diff --git a/test/syndication/DistributorImpl.t.sol b/test/custody/CustodianImpl.t.sol similarity index 94% rename from test/syndication/DistributorImpl.t.sol rename to test/custody/CustodianImpl.t.sol index 0022e5e..e41060e 100644 --- a/test/syndication/DistributorImpl.t.sol +++ b/test/custody/CustodianImpl.t.sol @@ -11,18 +11,16 @@ import { ICustodianFactory } from "contracts/core/interfaces/custody/ICustodianF import { BaseTest } from "test/BaseTest.t.sol"; contract CustodianImplTest is BaseTest { - address token; - address distFactory; function setUp() public initialize { - token = deployToken(); - distFactory = deployCustodianFactory(); + deployToken(); + deployCustodianFactory(); } function deployCustodian(string memory endpoint) public returns(address) { vm.prank(admin); - ICustodianFactory custodianFactory = ICustodianFactory(distFactory); - return custodianFactory.create(endpoint); + ICustodianFactory factory = ICustodianFactory(custodianFactory); + return factory.create(endpoint); } function test_Create_ValidCustodian() public { diff --git a/test/syndication/DistributorReferendum.t.sol b/test/custody/CustodianReferendum.t.sol similarity index 66% rename from test/syndication/DistributorReferendum.t.sol rename to test/custody/CustodianReferendum.t.sol index 21b0105..9d31450 100644 --- a/test/syndication/DistributorReferendum.t.sol +++ b/test/custody/CustodianReferendum.t.sol @@ -6,10 +6,11 @@ import { IERC20 } from "@openzeppelin/contracts/interfaces/IERC20.sol"; import { ITreasury } from "contracts/core/interfaces/economics/ITreasury.sol"; import { ITollgate } from "contracts/core/interfaces/economics/ITollgate.sol"; import { ILedgerVault } from "contracts/core/interfaces/financial/ILedgerVault.sol"; +import { IAgreementManager } from "contracts/core/interfaces/financial/IAgreementManager.sol"; import { ICustodianVerifiable } from "contracts/core/interfaces/custody/ICustodianVerifiable.sol"; import { ICustodianExpirable } from "contracts/core/interfaces/custody/ICustodianExpirable.sol"; import { ICustodianRegistrable } from "contracts/core/interfaces/custody/ICustodianRegistrable.sol"; -import { ICustodianInspectable} from "contracts/core/interfaces/custody/ICustodianInspectable.sol"; +import { ICustodianInspectable } from "contracts/core/interfaces/custody/ICustodianInspectable.sol"; import { ICustodianRevokable } from "contracts/core/interfaces/custody/ICustodianRevokable.sol"; import { ICustodianFactory } from "contracts/core/interfaces/custody/ICustodianFactory.sol"; @@ -19,22 +20,16 @@ import { T } from "contracts/core/primitives/Types.sol"; contract CustodianReferendumTest is BaseTest { address custodian; - address distFactory; - address referendum; - address tollgate; - address token; function setUp() public initialize { - token = deployToken(); - tollgate = deployTollgate(); - referendum = deployCustodianReferendum(); - distFactory = deployCustodianFactory(); + deployCustodianReferendum(); + deployCustodianFactory(); custodian = deployCustodian("contentrider.com"); } function deployCustodian(string memory endpoint) public returns (address) { vm.prank(admin); - ICustodianFactory custodianFactory = ICustodianFactory(distFactory); + ICustodianFactory custodianFactory = ICustodianFactory(custodianFactory); return custodianFactory.create(endpoint); } @@ -43,28 +38,28 @@ contract CustodianReferendumTest is BaseTest { function test_Init_ExpirationPeriod() public view { // test initialized treasury address uint256 expected = 180 days; - uint256 period = ICustodianExpirable(referendum).getExpirationPeriod(); + uint256 period = ICustodianExpirable(custodianReferendum).getExpirationPeriod(); assertEq(period, expected); } function test_SetExpirationPeriod_ValidExpiration() public { uint256 expireIn = 3600; // seconds vm.prank(governor); - ICustodianExpirable(referendum).setExpirationPeriod(expireIn); - assertEq(ICustodianExpirable(referendum).getExpirationPeriod(), expireIn); + ICustodianExpirable(custodianReferendum).setExpirationPeriod(expireIn); + assertEq(ICustodianExpirable(custodianReferendum).getExpirationPeriod(), expireIn); } function test_SetExpirationPeriod_EmitPeriodSet() public { uint256 expireIn = 3600; // seconds vm.prank(governor); - vm.expectEmit(true, false, false, true, address(referendum)); + vm.expectEmit(true, false, false, true, address(custodianReferendum)); emit CustodianReferendum.PeriodSet(expireIn); - ICustodianExpirable(referendum).setExpirationPeriod(expireIn); + ICustodianExpirable(custodianReferendum).setExpirationPeriod(expireIn); } function test_SetExpirationPeriod_RevertWhen_Unauthorized() public { vm.expectRevert(); - ICustodianExpirable(referendum).setExpirationPeriod(10); + ICustodianExpirable(custodianReferendum).setExpirationPeriod(10); } function test_Register_RegisteredEventEmitted() public { @@ -76,10 +71,22 @@ contract CustodianReferendumTest is BaseTest { // approve fees payment: admin default account IERC20(token).approve(ledger, expectedFees); ILedgerVault(ledger).deposit(admin, expectedFees, token); + ILedgerVault(ledger).approve(address(agreementManager), expectedFees, token); - vm.expectEmit(true, false, false, true, address(referendum)); + address[] memory parties = new address[](1); + parties[0] = custodian; + + uint256 proof = IAgreementManager(agreementManager).createAgreement( + expectedFees, + token, + address(custodianReferendum), + parties, + "" + ); + + vm.expectEmit(true, false, false, true, address(custodianReferendum)); emit CustodianReferendum.Registered(custodian, expectedFees); - ICustodianRegistrable(referendum).register(custodian, token); + ICustodianRegistrable(custodianReferendum).register(proof, custodian); vm.stopPrank(); } @@ -90,20 +97,28 @@ contract CustodianReferendumTest is BaseTest { // 2-deploy and register contract _registerCustodianWithApproval(custodian, expectedFees); // zero after disburse all the balance - assertEq(IERC20(token).balanceOf(referendum), expectedFees); + assertEq(IERC20(token).balanceOf(custodianReferendum), expectedFees); + } + + function test_Register_RevertIf_InvalidAgreement() public { + uint256 expectedFees = 100 * 1e18; // 100 MMC + _setFeesAsGovernor(expectedFees); + // expected revert if not valid allowance + vm.expectRevert(abi.encodeWithSignature("NoFundsToLock()")); + ICustodianRegistrable(custodianReferendum).register(0, custodian); } - function test_Register_RevertIf_InvalidAllowance() public { + function test_Register_RevertIf_InvalidFlatFee() public { uint256 expectedFees = 100 * 1e18; // 100 MMC _setFeesAsGovernor(expectedFees); // expected revert if not valid allowance vm.expectRevert(abi.encodeWithSignature("NoFundsToLock()")); - ICustodianRegistrable(referendum).register(custodian, token); + ICustodianRegistrable(custodianReferendum).register(0, custodian); } function test_Register_SetValidEnrollmentTime() public { - ICustodianInspectable inspectable = ICustodianInspectable(referendum); - ICustodianExpirable expirable = ICustodianExpirable(referendum); + ICustodianInspectable inspectable = ICustodianInspectable(custodianReferendum); + ICustodianExpirable expirable = ICustodianExpirable(custodianReferendum); _setFeesAsGovernor(1 * 1e18); uint256 expectedExpiration = expirable.getExpirationPeriod(); @@ -121,13 +136,13 @@ contract CustodianReferendumTest is BaseTest { _setFeesAsGovernor(1 * 1e18); // register the custodian expecting the right status. _registerCustodianWithApproval(custodian, 1 * 1e18); - assertTrue(ICustodianVerifiable(referendum).isWaiting(custodian)); + assertTrue(ICustodianVerifiable(custodianReferendum).isWaiting(custodian)); } function test_Register_RevertIf_InvalidCustodian() public { // register the custodian expecting the right status. vm.expectRevert(abi.encodeWithSignature("InvalidCustodianContract(address)", address(0))); - ICustodianRegistrable(referendum).register(address(0), token); + ICustodianRegistrable(custodianReferendum).register(0, address(0)); } function test_Approve_ApprovedEventEmitted() public { @@ -137,14 +152,14 @@ contract CustodianReferendumTest is BaseTest { vm.prank(governor); // as governor. vm.warp(1641070802); // after register a custodian a Registered event is expected - vm.expectEmit(true, false, false, true, address(referendum)); + vm.expectEmit(true, false, false, true, address(custodianReferendum)); emit CustodianReferendum.Approved(custodian); - ICustodianRegistrable(referendum).approve(custodian); + ICustodianRegistrable(custodianReferendum).approve(custodian); } function test_Approve_SetActiveState() public { _registerAndApproveCustodian(custodian); - assertTrue(ICustodianVerifiable(referendum).isActive(custodian)); + assertTrue(ICustodianVerifiable(custodianReferendum).isActive(custodian)); } function test_Approve_IncrementEnrollmentCount() public { @@ -156,7 +171,7 @@ contract CustodianReferendumTest is BaseTest { _registerAndApproveCustodian(custodian3); // still governor prank // valid approvals, increments the total of enrollments - assertEq(ICustodianInspectable(referendum).getEnrollmentCount(), 3); + assertEq(ICustodianInspectable(custodianReferendum).getEnrollmentCount(), 3); } function test_Revoke_RevokedEventEmitted() public { @@ -164,30 +179,30 @@ contract CustodianReferendumTest is BaseTest { vm.prank(governor); vm.warp(1641070801); // after register a custodian a Registered event is expected - vm.expectEmit(true, false, false, true, address(referendum)); + vm.expectEmit(true, false, false, true, address(custodianReferendum)); emit CustodianReferendum.Revoked(custodian); - ICustodianRevokable(referendum).revoke(custodian); + ICustodianRevokable(custodianReferendum).revoke(custodian); } function test_Revoke_DecrementEnrollmentCount() public { _registerAndApproveCustodian(custodian); // still governor prank // valid approvals, increments the total of enrollments vm.prank(governor); - ICustodianRevokable(referendum).revoke(custodian); - assertEq(ICustodianInspectable(referendum).getEnrollmentCount(), 0); + ICustodianRevokable(custodianReferendum).revoke(custodian); + assertEq(ICustodianInspectable(custodianReferendum).getEnrollmentCount(), 0); } function test_Revoke_SetBlockedState() public { _registerAndApproveCustodian(custodian); // still governor prank // custodian get revoked by governance.. vm.prank(governor); - ICustodianRevokable(referendum).revoke(custodian); - assertTrue(ICustodianVerifiable(referendum).isBlocked(custodian)); + ICustodianRevokable(custodianReferendum).revoke(custodian); + assertTrue(ICustodianVerifiable(custodianReferendum).isBlocked(custodian)); } function _setFeesAsGovernor(uint256 fees) internal { vm.startPrank(governor); - ITollgate(tollgate).setFees(T.Scheme.FLAT, referendum, fees, token); + ITollgate(tollgate).setFees(T.Scheme.FLAT, custodianReferendum, fees, token); vm.stopPrank(); } @@ -198,8 +213,20 @@ contract CustodianReferendumTest is BaseTest { // approve approval to ledger to deposit funds IERC20(token).approve(ledger, approval); ILedgerVault(ledger).deposit(admin, approval, token); + ILedgerVault(ledger).approve(address(agreementManager), approval, token); + + address[] memory parties = new address[](1); + parties[0] = d9r; + + uint256 proof = IAgreementManager(agreementManager).createAgreement( + approval, + token, + address(custodianReferendum), + parties, + "" + ); // operate over msg.sender ledger registered funds - ICustodianRegistrable(referendum).register(d9r, token); + ICustodianRegistrable(custodianReferendum).register(proof, d9r); vm.stopPrank(); } @@ -216,6 +243,6 @@ contract CustodianReferendumTest is BaseTest { _registerCustodianWithApproval(d9r, 1 * 1e18); vm.prank(governor); // as governor. // distribuitor approved only by governor.. - ICustodianRegistrable(referendum).approve(d9r); + ICustodianRegistrable(custodianReferendum).approve(d9r); } } diff --git a/test/economics/Tollgate.t.sol b/test/economics/Tollgate.t.sol index 1ae960d..c9ec732 100644 --- a/test/economics/Tollgate.t.sol +++ b/test/economics/Tollgate.t.sol @@ -7,13 +7,9 @@ import { ITollgate } from "contracts/core/interfaces/economics/ITollgate.sol"; import { T } from "contracts/core/primitives/Types.sol"; contract TollgateTest is BaseTest { - address tollgate; - address token; function setUp() public initialize { - // setup the access manager to use during tests.. - token = deployToken(); - tollgate = deployTollgate(); + deployTollgate(); } function test_SetFees_ValidFlatFees() public { diff --git a/test/primitives/BalanceOperator.t.sol b/test/primitives/BalanceOperator.t.sol index bee7075..57db0d0 100644 --- a/test/primitives/BalanceOperator.t.sol +++ b/test/primitives/BalanceOperator.t.sol @@ -15,11 +15,10 @@ contract BalanceOperatorWrapper is BalanceOperatorUpgradeable {} contract BalanceOperatorTest is BaseTest { address op; - address token; function setUp() public initialize { + deployToken(); op = address(new BalanceOperatorWrapper()); - token = deployToken(); } function test_Deposit_ValidDeposit() public { From 4a26ba2f3f252b463bb37ae2debc25b21021298b Mon Sep 17 00:00:00 2001 From: Geolffrey Mena Date: Sun, 4 May 2025 17:30:33 -0600 Subject: [PATCH 07/16] refactor: trustless escrow as the central module for custody registration fees --- contracts/economics/Tollgate.sol | 24 ++++++----- ...0_Deploy_Custody_CustodianReferendum.s.sol | 3 +- .../01_Orchestrate_ProtocolHydration.s.sol | 1 - ..._Upgrade_Custody_CustodianReferendum.s.sol | 3 +- test/BaseTest.t.sol | 7 ++-- test/custody/CustodianReferendum.t.sol | 32 ++++++--------- test/economics/Tollgate.t.sol | 41 +++++++++++++++---- 7 files changed, 65 insertions(+), 46 deletions(-) diff --git a/contracts/economics/Tollgate.sol b/contracts/economics/Tollgate.sol index 1c602a9..c831b72 100644 --- a/contracts/economics/Tollgate.sol +++ b/contracts/economics/Tollgate.sol @@ -63,20 +63,24 @@ contract Tollgate is Initializable, UUPSUpgradeable, AccessControlledUpgradeable } /// @notice Allows execution if the scheme is accepted or if support cannot be determined. - /// @dev This modifier checks whether the `target` contract explicitly supports the scheme via `isSupported`. - /// If `isSupported` exists and returns `false`, the call is reverted with InvalidTargetContext. - /// If the call to `isSupported` fails (e.g., target does not implement the function or reverts), execution is allowed by default. + /// @dev This modifier checks whether the `target` contract explicitly supports the scheme via `isFeeSchemeSupported`. + /// If `isFeeSchemeSupported` exists and returns `false`, the call is reverted with InvalidTargetContext. + /// If the call to `v` fails (e.g., target does not implement the function or reverts), execution is allowed by default. /// This enables compatibility with contracts that do not implement scheme validation. /// @param scheme The scheme to validate. /// @param target The address of the contract expected to support the scheme. - modifier onlyIfFeeSchemeSupported(T.Scheme scheme, address target) { - try IFeeSchemeValidator(target).isFeeSchemeSupported(scheme) returns (bool ok) { + modifier onlySupportedScheme(T.Scheme scheme, address target) { + bytes memory callData = abi.encodeCall(IFeeSchemeValidator.isFeeSchemeSupported, (scheme)); + (bool success, bytes memory result) = target.call(callData); + // if the call was successful, the target implements the method and returned a result. + // decode the result and validate the scheme acceptance. + if (success) { + bool ok = abi.decode(result, (bool)); if (!ok) revert InvalidTargetContext(target); - _; - } catch { - // by default all schemes are allowed - _; } + + // if call fails → by default all schemes are allowed + _; } /// @custom:oz-upgrades-unsafe-allow constructor @@ -132,7 +136,7 @@ contract Tollgate is Initializable, UUPSUpgradeable, AccessControlledUpgradeable address target, uint256 fee, address currency - ) external onlyIfFeeSchemeSupported(scheme, target) onlyValidFeeRepresentation(scheme, fee) restricted { + ) external onlySupportedScheme(scheme, target) onlyValidFeeRepresentation(scheme, fee) restricted { // Compute a unique composed key based on the target, currency, and scheme. // The composed key uniquely identifies a deterministic combination of these parameters // in a flat storage mapping. This avoids nested mappings, improving gas efficiency diff --git a/script/deployment/10_Deploy_Custody_CustodianReferendum.s.sol b/script/deployment/10_Deploy_Custody_CustodianReferendum.s.sol index 758d4a8..894aa7b 100644 --- a/script/deployment/10_Deploy_Custody_CustodianReferendum.s.sol +++ b/script/deployment/10_Deploy_Custody_CustodianReferendum.s.sol @@ -8,10 +8,9 @@ import { C } from "contracts/core/primitives/Constants.sol"; contract DeployCustodianReferendum is DeployBase { function run() external returns (address) { vm.startBroadcast(getAdminPK()); - address tollgate = computeCreate3Address("SALT_TOLLGATE"); address agreementSettler = computeCreate3Address("SALT_AGREEMENT_SETTLER"); address accessManager = computeCreate3Address("SALT_ACCESS_MANAGER"); - address impl = address(new CustodianReferendum(tollgate, agreementSettler)); + address impl = address(new CustodianReferendum(agreementSettler)); bytes memory init = abi.encodeCall(CustodianReferendum.initialize, (accessManager)); address referendum = deployUUPS(impl, init, "SALT_CUSTODIAN_REFERENDUM"); vm.stopBroadcast(); diff --git a/script/orchestration/01_Orchestrate_ProtocolHydration.s.sol b/script/orchestration/01_Orchestrate_ProtocolHydration.s.sol index 06e9861..80560c0 100644 --- a/script/orchestration/01_Orchestrate_ProtocolHydration.s.sol +++ b/script/orchestration/01_Orchestrate_ProtocolHydration.s.sol @@ -60,7 +60,6 @@ contract OrchestrateProtocolHydration is Script { // assign operations permissions authority.grantRole(C.OPS_ROLE, agreementManager, 0); authority.grantRole(C.OPS_ROLE, agreementSettler, 0); - authority.grantRole(C.OPS_ROLE, custodianReferendum, 0); bytes4[] memory vaultAllowed = LedgerVaultOpsPermissions(); authority.setTargetFunctionRole(ledgerVault, vaultAllowed, C.OPS_ROLE); diff --git a/script/upgrades/10_Upgrade_Custody_CustodianReferendum.s.sol b/script/upgrades/10_Upgrade_Custody_CustodianReferendum.s.sol index 354154a..a751562 100644 --- a/script/upgrades/10_Upgrade_Custody_CustodianReferendum.s.sol +++ b/script/upgrades/10_Upgrade_Custody_CustodianReferendum.s.sol @@ -9,8 +9,7 @@ contract UpgradeCustodianReferendum is UpgradeBase { function run() external returns (address) { vm.startBroadcast(getAdminPK()); address agreementSettler = vm.envAddress("AGREEMENT_SETTLER"); - address tollgateAddress = vm.envAddress("TOLLGATE"); - address impl = address(new CustodianReferendum(tollgateAddress, agreementSettler)); + address impl = address(new CustodianReferendum(agreementSettler)); address referendumProxy = vm.envAddress("CUSTODIAN_REFERENDUM"); // address accessManager = vm.envAddress("ACCESS_MANAGER"); //!IMPORTANT: This is not a safe upgrade, take any caution or 2-check needed before run this method diff --git a/test/BaseTest.t.sol b/test/BaseTest.t.sol index c7a7afc..806df9d 100644 --- a/test/BaseTest.t.sol +++ b/test/BaseTest.t.sol @@ -135,6 +135,8 @@ abstract contract BaseTest is Test { DeployAgreementManager agreementManagerDeployer = new DeployAgreementManager(); agreementManager = agreementManager == address(0) ? agreementManagerDeployer.run() : agreementManager; + // OP role granted to custodian referendum to operate on ledger + _assignOpRole(agreementManager); } function deployAgreementSettler() public { @@ -144,6 +146,8 @@ abstract contract BaseTest is Test { DeployAgreementSettler agreementSettlerDeployer = new DeployAgreementSettler(); agreementSettler = agreementSettler == address(0) ? agreementSettlerDeployer.run() : agreementSettler; + // OP role granted to custodian referendum to operate on ledger + _assignOpRole(agreementSettler); } // 05_DeployAssetReferendum @@ -179,15 +183,12 @@ abstract contract BaseTest is Test { // 09_DeployCustodianReferendum function deployCustodianReferendum() public { - deployTollgate(); deployAgreementSettler(); // set default admin as deployer.. DeployCustodianReferendum distReferendumDeployer = new DeployCustodianReferendum(); bytes4[] memory custodianReferendumAllowed = CustodianReferendumGovPermissions(); custodianReferendum = custodianReferendum == address(0) ? distReferendumDeployer.run() : custodianReferendum; - // OP role granted to custodian referendum to operate on ledger - _assignOpRole(custodianReferendum); // GOV permission set to custodian referendum functions _setGovPermissions(custodianReferendum, custodianReferendumAllowed); } diff --git a/test/custody/CustodianReferendum.t.sol b/test/custody/CustodianReferendum.t.sol index 9d31450..708cf84 100644 --- a/test/custody/CustodianReferendum.t.sol +++ b/test/custody/CustodianReferendum.t.sol @@ -19,12 +19,10 @@ import { CustodianReferendum } from "contracts/custody/CustodianReferendum.sol"; import { T } from "contracts/core/primitives/Types.sol"; contract CustodianReferendumTest is BaseTest { - address custodian; function setUp() public initialize { deployCustodianReferendum(); deployCustodianFactory(); - custodian = deployCustodian("contentrider.com"); } function deployCustodian(string memory endpoint) public returns (address) { @@ -64,6 +62,7 @@ contract CustodianReferendumTest is BaseTest { function test_Register_RegisteredEventEmitted() public { uint256 expectedFees = 100 * 1e18; + address custodian = deployCustodian("contentrider.com"); _setFeesAsGovernor(expectedFees); // free enrollment: test purpose // after register a custodian a Registered event is expected vm.warp(1641070803); @@ -90,33 +89,20 @@ contract CustodianReferendumTest is BaseTest { vm.stopPrank(); } - function test_Register_ValidFees() public { - uint256 expectedFees = 100 * 1e18; // 100 MMC - // 1-set enrollment fees. - _setFeesAsGovernor(expectedFees); - // 2-deploy and register contract - _registerCustodianWithApproval(custodian, expectedFees); - // zero after disburse all the balance - assertEq(IERC20(token).balanceOf(custodianReferendum), expectedFees); - } function test_Register_RevertIf_InvalidAgreement() public { uint256 expectedFees = 100 * 1e18; // 100 MMC + address custodian = deployCustodian("contentrider.com"); _setFeesAsGovernor(expectedFees); // expected revert if not valid allowance - vm.expectRevert(abi.encodeWithSignature("NoFundsToLock()")); + vm.prank(user); + vm.expectRevert(abi.encodeWithSignature("UnauthorizedCustodianManager(address)", user)); ICustodianRegistrable(custodianReferendum).register(0, custodian); } - function test_Register_RevertIf_InvalidFlatFee() public { - uint256 expectedFees = 100 * 1e18; // 100 MMC - _setFeesAsGovernor(expectedFees); - // expected revert if not valid allowance - vm.expectRevert(abi.encodeWithSignature("NoFundsToLock()")); - ICustodianRegistrable(custodianReferendum).register(0, custodian); - } function test_Register_SetValidEnrollmentTime() public { + address custodian = deployCustodian("contentrider.com"); ICustodianInspectable inspectable = ICustodianInspectable(custodianReferendum); ICustodianExpirable expirable = ICustodianExpirable(custodianReferendum); @@ -134,6 +120,7 @@ contract CustodianReferendumTest is BaseTest { function test_Register_SetWaitingState() public { _setFeesAsGovernor(1 * 1e18); + address custodian = deployCustodian("contentrider.com"); // register the custodian expecting the right status. _registerCustodianWithApproval(custodian, 1 * 1e18); assertTrue(ICustodianVerifiable(custodianReferendum).isWaiting(custodian)); @@ -147,6 +134,7 @@ contract CustodianReferendumTest is BaseTest { function test_Approve_ApprovedEventEmitted() public { _setFeesAsGovernor(1 * 1e18); + address custodian = deployCustodian("contentrider.com"); _registerCustodianWithApproval(custodian, 1 * 1e18); vm.prank(governor); // as governor. @@ -158,11 +146,13 @@ contract CustodianReferendumTest is BaseTest { } function test_Approve_SetActiveState() public { + address custodian = deployCustodian("contentrider.com"); _registerAndApproveCustodian(custodian); assertTrue(ICustodianVerifiable(custodianReferendum).isActive(custodian)); } function test_Approve_IncrementEnrollmentCount() public { + address custodian = deployCustodian("contentrider.com"); address custodian2 = deployCustodian("test2.com"); address custodian3 = deployCustodian("test3.com"); @@ -175,6 +165,7 @@ contract CustodianReferendumTest is BaseTest { } function test_Revoke_RevokedEventEmitted() public { + address custodian = deployCustodian("contentrider.com"); _registerAndApproveCustodian(custodian); // still governor prank vm.prank(governor); vm.warp(1641070801); @@ -185,6 +176,7 @@ contract CustodianReferendumTest is BaseTest { } function test_Revoke_DecrementEnrollmentCount() public { + address custodian = deployCustodian("contentrider.com"); _registerAndApproveCustodian(custodian); // still governor prank // valid approvals, increments the total of enrollments vm.prank(governor); @@ -193,6 +185,7 @@ contract CustodianReferendumTest is BaseTest { } function test_Revoke_SetBlockedState() public { + address custodian = deployCustodian("contentrider.com"); _registerAndApproveCustodian(custodian); // still governor prank // custodian get revoked by governance.. vm.prank(governor); @@ -232,6 +225,7 @@ contract CustodianReferendumTest is BaseTest { function _registerCustodianWithGovernorAndApproval() internal { uint256 expectedFees = 100 * 1e18; + address custodian = deployCustodian("contentrider.com"); _setFeesAsGovernor(expectedFees); _registerCustodianWithApproval(custodian, expectedFees); } diff --git a/test/economics/Tollgate.t.sol b/test/economics/Tollgate.t.sol index c9ec732..5a6ba7e 100644 --- a/test/economics/Tollgate.t.sol +++ b/test/economics/Tollgate.t.sol @@ -6,15 +6,29 @@ import { Tollgate } from "contracts/economics/Tollgate.sol"; import { ITollgate } from "contracts/core/interfaces/economics/ITollgate.sol"; import { T } from "contracts/core/primitives/Types.sol"; -contract TollgateTest is BaseTest { +contract TargetA {} + +contract TargetB {} +contract TargetC {} + +contract TargetD { + /// @notice Checks if the given fee scheme is supported in this context. + /// @param scheme The fee scheme to validate. + /// @return True if the scheme is supported. + function isFeeSchemeSupported(T.Scheme scheme) external pure returns (bool) { + // support only FLAT scheme + return scheme == T.Scheme.FLAT; + } +} +contract TollgateTest is BaseTest { function setUp() public initialize { deployTollgate(); } function test_SetFees_ValidFlatFees() public { uint256 expected = 1e18; // expected flat fees - address target = vm.addr(8); + address target = address(new TargetA()); vm.prank(governor); // as governor set fees ITollgate(tollgate).setFees(T.Scheme.FLAT, target, expected, token); @@ -25,7 +39,7 @@ contract TollgateTest is BaseTest { function test_SetFees_ValidBasePointAgreementFees() public { uint256 expected = 5 * 100; // 500 bps = 5% nominal expected base point - address target = vm.addr(8); + address target = address(new TargetA()); vm.prank(governor); // as governor set fees ITollgate(tollgate).setFees(T.Scheme.BPS, target, expected, token); @@ -36,7 +50,7 @@ contract TollgateTest is BaseTest { function test_SetFees_FeesSetEventEmitted() public { uint256 expected = 1e18; // expected flat fees - address target = vm.addr(8); + address target = address(new TargetA()); vm.prank(governor); // as governor set fees vm.expectEmit(true, true, false, true, address(tollgate)); emit Tollgate.FeesSet(target, token, T.Scheme.FLAT, expected); @@ -45,7 +59,7 @@ contract TollgateTest is BaseTest { function test_SetFees_RevertWhen_InvalidBasePointFees() public { uint256 invalidFees = 10_001; // overflowed base points max = 10_000 - address target = vm.addr(8); + address target = address(new TargetA()); vm.prank(governor); // as governor set fees vm.expectRevert(abi.encodeWithSignature("InvalidBasisPointRange(uint256)", invalidFees)); ITollgate(tollgate).setFees(T.Scheme.BPS, target, invalidFees, token); @@ -53,19 +67,28 @@ contract TollgateTest is BaseTest { function test_SetFees_RevertWhen_InvalidNominalFees() public { uint256 invalidFees = 101; // overflowed base points max = 10_000 - address target = vm.addr(8); + address target = address(new TargetA()); vm.prank(governor); // as governor set fees vm.expectRevert(abi.encodeWithSignature("InvalidNominalRange(uint256)", invalidFees)); ITollgate(tollgate).setFees(T.Scheme.NOMINAL, target, invalidFees, token); } + function test_SetFees_RevertIf_NotSupportedSchemeByTarget() public { + vm.startPrank(governor); + // expected revert if not valid allowance + address notSupportedNominal = address(new TargetD()); + vm.expectRevert(abi.encodeWithSignature("InvalidTargetContext(address)", notSupportedNominal)); + ITollgate(tollgate).setFees(T.Scheme.NOMINAL, notSupportedNominal, 1, token); + vm.stopPrank(); + } + function test_GetFees_ValidExpectedFees() public { uint256 expectedFlat = 1e18; // 1MMC expected flat fees uint256 expectedBps = 10 * 100; // = 10% expected bps uint256 expectedNominal = 50; // = 50% expected bps - address targetA = vm.addr(8); - address targetB = vm.addr(9); - address targetC = vm.addr(10); + address targetA = address(new TargetA()); + address targetB = address(new TargetB()); + address targetC = address(new TargetC()); vm.startPrank(governor); // as governor set fees ITollgate(tollgate).setFees(T.Scheme.FLAT, targetA, expectedFlat, token); From a331bbc4af7def2b021fc896f0421c70106ea1a7 Mon Sep 17 00:00:00 2001 From: Geolffrey Mena Date: Sun, 4 May 2025 17:31:27 -0600 Subject: [PATCH 08/16] refactor: trustless escrow as the central module for custody registration fees --- contracts/access/AccessManager.sol | 1 - contracts/assets/AssetOwnership.sol | 2 +- .../core/interfaces/base/IBalanceOperator.sol | 4 +-- contracts/core/interfaces/base/IQuorum.sol | 6 +--- .../custody/ICustodianReferendum.sol | 4 +-- .../custody/ICustodianRegistrable.sol | 1 - contracts/custody/CustodianReferendum.sol | 35 +++++++++++++------ contracts/policies/PolicyAudit.sol | 2 +- 8 files changed, 30 insertions(+), 25 deletions(-) diff --git a/contracts/access/AccessManager.sol b/contracts/access/AccessManager.sol index 784502b..f67cde8 100644 --- a/contracts/access/AccessManager.sol +++ b/contracts/access/AccessManager.sol @@ -63,7 +63,6 @@ contract AccessManager is Initializable, UUPSUpgradeable, AccessManagerUpgradeab _setRoleAdmin(C.REF_ROLE, C.GOV_ROLE); _setRoleAdmin(C.MOD_ROLE, C.ADMIN_ROLE); _setRoleAdmin(C.OPS_ROLE, C.ADMIN_ROLE); - } // TODO pause protocol based on permission and roles diff --git a/contracts/assets/AssetOwnership.sol b/contracts/assets/AssetOwnership.sol index a4fb5ae..19df797 100644 --- a/contracts/assets/AssetOwnership.sol +++ b/contracts/assets/AssetOwnership.sol @@ -112,7 +112,7 @@ contract AssetOwnership is } // TODO: build getURI => from custodian /erc721-metadata - // TODO: Update asset info control version restricted/approved by governance + // TODO: Update asset info control version restricted/approved by governance // TODO: Transfer Ownership Fee: Introducing a fee for transferring // ownership discourages frequent or unnecessary transfers, // adding an economic cost to any potential abuse of the system. Like bypassing content diff --git a/contracts/core/interfaces/base/IBalanceOperator.sol b/contracts/core/interfaces/base/IBalanceOperator.sol index 1ac66d5..61e0fa4 100644 --- a/contracts/core/interfaces/base/IBalanceOperator.sol +++ b/contracts/core/interfaces/base/IBalanceOperator.sol @@ -12,6 +12,4 @@ import { IBalanceVerifiable } from "@synaps3/core/interfaces/base/IBalanceVerifi /// @dev This interface aggregates multiple interfaces to standardize balance-related operations. /// @dev The `IBalanceOperator` interface extends multiple interfaces to provide a comprehensive suite of /// balance-related operations, including deposit, withdrawal, transfer, reserve, and balance verification. -interface IBalanceOperator is IBalanceDepositor, IBalanceWithdrawable, IBalanceTransferable, IBalanceVerifiable { - -} +interface IBalanceOperator is IBalanceDepositor, IBalanceWithdrawable, IBalanceTransferable, IBalanceVerifiable {} diff --git a/contracts/core/interfaces/base/IQuorum.sol b/contracts/core/interfaces/base/IQuorum.sol index d263887..deebe86 100644 --- a/contracts/core/interfaces/base/IQuorum.sol +++ b/contracts/core/interfaces/base/IQuorum.sol @@ -9,8 +9,4 @@ import { IQuorumInspectable } from "@synaps3/core/interfaces/base/IQuorumInspect /// @notice Aggregates the full lifecycle of an FSM-driven entity registration system. /// @dev Combines registration, approval, rejection, revocation, and status inspection. /// Intended for systems that use `QuorumUpgradeable` as FSM logic layer. -interface IQuorum is - IQuorumRegistrable, - IQuorumRevokable, - IQuorumInspectable -{} +interface IQuorum is IQuorumRegistrable, IQuorumRevokable, IQuorumInspectable {} diff --git a/contracts/core/interfaces/custody/ICustodianReferendum.sol b/contracts/core/interfaces/custody/ICustodianReferendum.sol index 753e3a1..75cb022 100644 --- a/contracts/core/interfaces/custody/ICustodianReferendum.sol +++ b/contracts/core/interfaces/custody/ICustodianReferendum.sol @@ -16,6 +16,4 @@ interface ICustodianReferendum is ICustodianExpirable, ICustodianInspectable, ICustodianRevokable -{ - -} +{} diff --git a/contracts/core/interfaces/custody/ICustodianRegistrable.sol b/contracts/core/interfaces/custody/ICustodianRegistrable.sol index 07ad11b..ca7919a 100644 --- a/contracts/core/interfaces/custody/ICustodianRegistrable.sol +++ b/contracts/core/interfaces/custody/ICustodianRegistrable.sol @@ -15,5 +15,4 @@ interface ICustodianRegistrable { /// @notice Approves the data associated with the given identifier. /// @param custodian The address of the custodian to approve. function approve(address custodian) external; - } diff --git a/contracts/custody/CustodianReferendum.sol b/contracts/custody/CustodianReferendum.sol index 7938f63..8a7e8ca 100644 --- a/contracts/custody/CustodianReferendum.sol +++ b/contracts/custody/CustodianReferendum.sol @@ -35,9 +35,6 @@ contract CustodianReferendum is /// @dev Stores the interface ID for ICustodian, ensuring compatibility verification. bytes4 private constant INTERFACE_ID_CUSTODIAN = type(ICustodian).interfaceId; - ///Our immutables behave as constants after deployment //slither-disable-start naming-convention - /// @custom:oz-upgrades-unsafe-allow state-variable-immutable - ITollgate public immutable TOLLGATE; /// @custom:oz-upgrades-unsafe-allow state-variable-immutable IAgreementSettler public immutable AGREEMENT_SETTLER; //slither-disable-end naming-convention @@ -70,9 +67,13 @@ contract CustodianReferendum is /// @param invalid The address of the custodian contract that is invalid error InvalidCustodianContract(address invalid); - /// @notice Error thrown when an invalid param is provided for a referendum registration. - /// @param message A descriptive message explaining the reason for the invalid. - error InvalidRegisterParams(string message); + /// @notice Error thrown when the caller is not authorized as the custodian's manager. + /// @param caller The address attempting the action without being the manager. + error UnauthorizedCustodianManager(address caller); + + /// @notice Error thrown when the custodian does not match the agreement's registered party. + /// @param custodian The custodian provided for the operation. + error CustodianAgreementMismatch(address custodian); /// @notice Modifier to ensure that the given custodian contract supports the ICustodian interface. /// @param custodian The custodian contract address. @@ -83,13 +84,22 @@ contract CustodianReferendum is _; } + /// @notice Modifier to ensure that only the custodian's manager can perform the operation. + /// @param custodian The address of the custodian contract whose manager must authorize this operation. + modifier onlyCustodianManager(address custodian) { + address manager = ICustodian(custodian).getManager(); + if (msg.sender != manager) { + revert UnauthorizedCustodianManager(msg.sender); + } + _; + } + /// @custom:oz-upgrades-unsafe-allow constructor - constructor(address tollgate, address agreementSettler) { + constructor(address agreementSettler) { /// https://forum.openzeppelin.com/t/what-does-disableinitializers-function-mean/28730/5 /// https://forum.openzeppelin.com/t/uupsupgradeable-vulnerability-post-mortem/15680 _disableInitializers(); AGREEMENT_SETTLER = IAgreementSettler(agreementSettler); - TOLLGATE = ITollgate(tollgate); } /// @notice Initializes the proxy state. @@ -164,7 +174,10 @@ contract CustodianReferendum is /// @notice Registers a custodian by sending a payment to the contract. /// @param proof The unique identifier of the agreement to be enforced. /// @param custodian The address of the custodian to register. - function register(uint256 proof, address custodian) external onlyValidCustodian(custodian) { + function register( + uint256 proof, + address custodian + ) external onlyValidCustodian(custodian) onlyCustodianManager(custodian) { /// TODO penalize invalid endpoints, and revoked during referendum // !IMPORTANT: // Fees act as a mechanism to prevent abuse or spam by users @@ -179,7 +192,9 @@ contract CustodianReferendum is // individual actions with the broader sustainability of the network. // !IMPORTANT If tollgate does not support the currency, will revert.. T.Agreement memory agreement = AGREEMENT_SETTLER.settleAgreement(proof, msg.sender); - if (custodian != agreement.parties[0]) revert InvalidRegisterParams("Custodian is not part of the agreement."); + if (agreement.parties[0] != custodian) { + revert CustodianAgreementMismatch(custodian); + } // register custodian as pending approval _register(uint160(custodian)); diff --git a/contracts/policies/PolicyAudit.sol b/contracts/policies/PolicyAudit.sol index 5ace9b8..332f481 100644 --- a/contracts/policies/PolicyAudit.sol +++ b/contracts/policies/PolicyAudit.sol @@ -35,7 +35,7 @@ contract PolicyAudit is Initializable, UUPSUpgradeable, AccessControlledUpgradea /// @param auditor The address of the auditor that revoked the audit. event PolicyRevoked(address indexed policy, address auditor); - /// @dev Error thrown when the policy contract does not implement the IPolicy interface. + /// @dev Error thrown when the policy contract does not implement the IPolicy interface. error InvalidPolicyContract(address); /// @dev Modifier to check that a policy contract implements the IPolicy interface. From b4d4cca9356057f5053bf7f24521b2c6158a6c03 Mon Sep 17 00:00:00 2001 From: Geolffrey Mena Date: Sun, 4 May 2025 17:31:33 -0600 Subject: [PATCH 09/16] refactor: trustless escrow as the central module for custody registration fees --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index bd03934..0a6493a 100644 --- a/Makefile +++ b/Makefile @@ -54,7 +54,7 @@ force-compile: .PHONY: test ## run tests test: - @export CI=true && forge test --show-progress --gas-report -vvvv --fail-fast + @export CI=true && forge test --show-progress --gas-report -vvvv .PHONY: coverage ## run tests coverage report coverage: From 2ad86c025122a9d0265dda021751cfb673b26a69 Mon Sep 17 00:00:00 2001 From: Geolffrey Mena Date: Fri, 9 May 2025 08:48:53 -0600 Subject: [PATCH 10/16] feat: Delegated Weighted Reputation Routing (DWRR) --- contracts/assets/AssetOwnership.sol | 4 +- contracts/assets/AssetReferendum.sol | 8 +- contracts/core/interfaces/hooks/IHook.sol | 15 +- contracts/financial/AgreementManager.sol | 2 +- contracts/financial/AgreementSettler.sol | 9 +- contracts/lifecycle/HookBase.sol | 10 +- contracts/lifecycle/HookRegistry.sol | 4 +- contracts/rights/RightsAssetCustodian.sol | 190 ++++++++++++-------- contracts/rights/RightsPolicyAuthorizer.sol | 2 +- contracts/rights/RightsPolicyManager.sol | 2 +- test/assets/AssetReferendum.t.sol | 8 +- 11 files changed, 155 insertions(+), 99 deletions(-) diff --git a/contracts/assets/AssetOwnership.sol b/contracts/assets/AssetOwnership.sol index 19df797..da5d8c3 100644 --- a/contracts/assets/AssetOwnership.sol +++ b/contracts/assets/AssetOwnership.sol @@ -38,12 +38,12 @@ contract AssetOwnership is /// @dev Emitted when a new asset is registered on the platform. /// @param owner The address of the creator or owner of the registered asset. /// @param assetId The unique identifier for the registered asset. - event RegisteredAsset(address indexed owner, uint256 assetId); + event RegisteredAsset(address indexed owner, uint256 indexed assetId); /// @dev Emitted when an asset is revoked and removed from the platform. /// @param owner The address of the owner of the revoked asset. /// @param assetId The unique identifier for the revoked asset. - event RevokedAsset(address indexed owner, uint256 assetId); + event RevokedAsset(address indexed owner, uint256 indexed assetId); /// @dev Emitted when an asset is transferred from one owner to another. /// @param from The address of the current owner of the asset. diff --git a/contracts/assets/AssetReferendum.sol b/contracts/assets/AssetReferendum.sol index 627b62a..1e58619 100644 --- a/contracts/assets/AssetReferendum.sol +++ b/contracts/assets/AssetReferendum.sol @@ -31,19 +31,19 @@ contract AssetReferendum is /// @dev Event emitted when a content is submitted for referendum. /// @param assetId The ID of the asset that has been submitted. /// @param initiator The address of the initiator who submitted the asset. - event Submitted(address indexed initiator, uint256 assetId); + event Submitted(address indexed initiator, uint256 indexed assetId); /// @dev Event emitted when a content is approved. /// @param assetId The ID of the asset that has been approved. - event Approved(uint256 assetId); + event Approved(uint256 indexed assetId); /// @dev Event emitted when a content is revoked. /// @param assetId The ID of the asset that has been revoked. - event Revoked(uint256 assetId); + event Revoked(uint256 indexed assetId); /// @dev Event emitted when a content is rejected. /// @param assetId The ID of the asset that has been rejected. - event Rejected(uint256 assetId); + event Rejected(uint256 indexed assetId); /// @dev Error thrown when asset submission fails. /// @param initiator The address of the user who attempted to submit the asset. diff --git a/contracts/core/interfaces/hooks/IHook.sol b/contracts/core/interfaces/hooks/IHook.sol index a56bb7e..01dc4d7 100644 --- a/contracts/core/interfaces/hooks/IHook.sol +++ b/contracts/core/interfaces/hooks/IHook.sol @@ -3,11 +3,18 @@ pragma solidity 0.8.26; interface IHook { - // check if the hook its associated with the target - // eg. SubscriptionPolicy its allowed to process this hook - function validate(address caller, address target) external; + // Chain of responsibility + + // pre call to check if the hook its associated with the target + // eg. SubscriptionPolicy its allowed to process this hook + function validate(address caller, address target) external returns (bool); + /// @notice Called by the protocol to signal the use of the hook /// @param caller The address invoking the hook (e.g., the user or distributor) /// @param context Optional: bytes data for hook-specific usage - function execute(address caller, bytes calldata context) external; + function execute(address caller, bytes calldata context) external returns (bytes memory); + + // post call to verify the execution, run logs, etc + // eg. SubscriptionPolicy execution complain well? + function verify(address caller, bytes calldata results) external returns (bool); } diff --git a/contracts/financial/AgreementManager.sol b/contracts/financial/AgreementManager.sol index 3a5ca2b..8622936 100644 --- a/contracts/financial/AgreementManager.sol +++ b/contracts/financial/AgreementManager.sol @@ -46,7 +46,7 @@ contract AgreementManager is Initializable, UUPSUpgradeable, AccessControlledUpg /// @param proof A unique identifier (hash or proof) representing the created agreement. /// @param amount The monetary amount specified in the agreement. /// @param currency The address of the token used as currency in the agreement. - event AgreementCreated(address indexed initiator, uint256 proof, uint256 amount, address currency); + event AgreementCreated(address indexed initiator, uint256 indexed proof, uint256 amount, address currency); /// @notice Error thrown when a flat fee exceeds the total amount. error FlatFeeExceedsTotal(uint256 total, uint256 fee); diff --git a/contracts/financial/AgreementSettler.sol b/contracts/financial/AgreementSettler.sol index 1caef1c..a86c5af 100644 --- a/contracts/financial/AgreementSettler.sol +++ b/contracts/financial/AgreementSettler.sol @@ -56,13 +56,18 @@ contract AgreementSettler is /// @param counterparty The address that received the settlement funds. /// @param proof The unique identifier (hash or proof) of the settled agreement. /// @param collectedFees The amount of fees collected from the settlement process. - event AgreementSettled(address indexed arbiter, address indexed counterparty, uint256 proof, uint256 collectedFees); + event AgreementSettled( + address indexed arbiter, + address indexed counterparty, + uint256 indexed proof, + uint256 collectedFees + ); /// @notice Emitted when an agreement is canceled by the authorized account. /// @param initiator The account that initiated the cancellation. /// @param proof The unique identifier (hash or proof) of the canceled agreement. /// @param collectedFees The amount of fees collected (if any) upon cancellation. - event AgreementCancelled(address indexed initiator, uint256 proof, uint256 collectedFees); + event AgreementCancelled(address indexed initiator, uint256 indexed proof, uint256 collectedFees); /// @notice Error thrown when the agreement proof has already been settled. error AgreementAlreadySettled(); diff --git a/contracts/lifecycle/HookBase.sol b/contracts/lifecycle/HookBase.sol index 540e01e..f084fe1 100644 --- a/contracts/lifecycle/HookBase.sol +++ b/contracts/lifecycle/HookBase.sol @@ -1,2 +1,10 @@ +// SPDX-License-Identifier: BUSL-1.1 +// NatSpec format convention - https://docs.soliditylang.org/en/v0.5.10/natspec-format.html +pragma solidity 0.8.26; + // se podria aprovechar la logica de los modifier para establecer control de ejecucion antes y despues -// crear un onCall modifier para agregar a los metodo y registrar en base a cada hook acciones especificas en el llamado \ No newline at end of file +// crear un onCall modifier para agregar a los metodo y registrar en base a cada hook acciones especificas en el llamado + +// pre - validate +// during - execute +// pos - verify diff --git a/contracts/lifecycle/HookRegistry.sol b/contracts/lifecycle/HookRegistry.sol index 471b4ab..e9c3632 100644 --- a/contracts/lifecycle/HookRegistry.sol +++ b/contracts/lifecycle/HookRegistry.sol @@ -11,7 +11,6 @@ import { IHookRegistry } from "@synaps3/core/interfaces/hooks/IHookRegistry.sol" import { IHook } from "@synaps3/core/interfaces/hooks/IHook.sol"; import { T } from "@synaps3/core/primitives/Types.sol"; - // Hook interfaces can define logic for: // - Access control: e.g. only users holding certain tokens can access content. // - Time-based rules: e.g. access available for a limited period. @@ -75,6 +74,7 @@ contract HookRegistry is Initializable, UUPSUpgradeable, AccessControlledUpgrade /// @param hook The address of the hook contract to register. /// @param interfaceId The interface ID that this hook implements. /// This allows different kinds of hook logic to be categorized and retrieved by interface ID. + // TODO: restricted to MOD_ROLE function submit(address hook, bytes4 interfaceId) external onlyValidHook(hook) restricted { _register(uint160(hook)); _hooks[interfaceId] = hook; @@ -100,7 +100,7 @@ contract HookRegistry is Initializable, UUPSUpgradeable, AccessControlledUpgrade /// @notice Returns the registered hook address for a given interface ID. /// @param interfaceId The interface ID used to look up the registered hook. /// @return The address of the hook associated with the given interface ID. - function lookup(bytes4 interfaceId) external returns (address) { + function lookup(bytes4 interfaceId) external view returns (address) { return _hooks[interfaceId]; } diff --git a/contracts/rights/RightsAssetCustodian.sol b/contracts/rights/RightsAssetCustodian.sol index 8ac6c37..ec247d7 100644 --- a/contracts/rights/RightsAssetCustodian.sol +++ b/contracts/rights/RightsAssetCustodian.sol @@ -8,8 +8,7 @@ import { UUPSUpgradeable } from "@openzeppelin/contracts-upgradeable/proxy/utils import { AccessControlledUpgradeable } from "@synaps3/core/primitives/upgradeable/AccessControlledUpgradeable.sol"; import { ICustodianVerifiable } from "@synaps3/core/interfaces/custody/ICustodianVerifiable.sol"; import { IRightsAssetCustodian } from "@synaps3/core/interfaces/rights/IRightsAssetCustodian.sol"; - -import { C } from "@synaps3/core/primitives/Constants.sol"; +import { LoopOps } from "@synaps3/core/libraries/LoopOps.sol"; /// @title RightsAssetCustodian /// @notice Manages the assignment and verification of custodian rights for content holders. @@ -18,6 +17,7 @@ import { C } from "@synaps3/core/primitives/Constants.sol"; /// to validate the activity status of custodians. contract RightsAssetCustodian is Initializable, UUPSUpgradeable, AccessControlledUpgradeable, IRightsAssetCustodian { using EnumerableSet for EnumerableSet.AddressSet; + using LoopOps for uint256; /// @custom:oz-upgrades-unsafe-allow state-variable-immutable /// Our immutables behave as constants after deployment @@ -25,7 +25,7 @@ contract RightsAssetCustodian is Initializable, UUPSUpgradeable, AccessControlle ICustodianVerifiable public immutable CUSTODIAN_REFERENDUM; /// @dev the max allowed amount of custodians per holder. - uint256 private _maxDistributionRedundancy; + uint256 private _maxCustodianRedundancy; /// @dev Mapping to store the custodiaN address for each content rights holder. mapping(address => EnumerableSet.AddressSet) private _custodiansByHolder; /// @dev Mapping to store a registry of rights holders associated with each custodian. @@ -34,12 +34,14 @@ contract RightsAssetCustodian is Initializable, UUPSUpgradeable, AccessControlle /// @notice Emitted when custodian rights are granted to a custodian. /// @param newCustody The address of the custodian granted custodial rights. /// @param rightsHolder The address of the asset's rights holder. - event CustodialGranted(address indexed newCustody, address indexed rightsHolder); + /// @param demand The total number of holders currently assigned to the custodian (under custody). + event CustodialGranted(address indexed newCustody, address indexed rightsHolder, uint256 demand); /// @notice Emitted when custodian rights are granted to a custodian. /// @param revokedCustody The address of the custodian granted custodial rights. /// @param rightsHolder The address of the asset's rights holder. - event CustodialRevoked(address indexed revokedCustody, address indexed rightsHolder); + /// @param demand The total number of holders currently assigned to the custodian (under custody). + event CustodialRevoked(address indexed revokedCustody, address indexed rightsHolder, uint256 demand); /// @dev Error that is thrown when a content hash is already registered. error InvalidInactiveCustodian(); @@ -65,7 +67,7 @@ contract RightsAssetCustodian is Initializable, UUPSUpgradeable, AccessControlle /// @dev Ensures that the caller does not exceed the maximum redundancy limit for custodians. modifier onlyAvailableRedundancy() { uint256 currentRedundancy = _custodiansByHolder[msg.sender].length(); - if (currentRedundancy >= _maxDistributionRedundancy) { + if (currentRedundancy >= _maxCustodianRedundancy) { revert MaxRedundancyAllowedReached(); } _; @@ -88,7 +90,7 @@ contract RightsAssetCustodian is Initializable, UUPSUpgradeable, AccessControlle // we can use this attribute to control de "stress" in the network // eg: if the network is growing we can adjust this attribute to allow more // redundancy and more backend custodians.. - _maxDistributionRedundancy = 3; // redundancy factor (RF) + _maxCustodianRedundancy = 3; // redundancy factor (RF) } /// @notice Updates the maximum allowed number of custodians per holder. @@ -96,7 +98,7 @@ contract RightsAssetCustodian is Initializable, UUPSUpgradeable, AccessControlle /// providing flexibility based on network conditions. /// @param value The new maximum number of custodians allowed per holder. function setMaxAllowedRedundancy(uint256 value) external restricted { - _maxDistributionRedundancy = value; + _maxCustodianRedundancy = value; } /// @notice Revokes custodial rights of a custodian for the caller's assets. @@ -105,8 +107,9 @@ contract RightsAssetCustodian is Initializable, UUPSUpgradeable, AccessControlle // remove custody from the storage && if does not exist nor granted will revoke bool removedCustodian = _custodiansByHolder[msg.sender].remove(custodian); if (!removedCustodian) revert RevokeCustodyFailed(custodian, msg.sender); - _decrementCustody(custodian); // -1 under custody - emit CustodialRevoked(custodian, msg.sender); + + uint256 demand = _decrementCustody(custodian); // -1 under custody + emit CustodialRevoked(custodian, msg.sender, demand); } /// @notice Grants custodial rights over the asset held by a holder to a custodian. @@ -117,11 +120,11 @@ contract RightsAssetCustodian is Initializable, UUPSUpgradeable, AccessControlle // TODO using the maxAvailable we could limit the number of balanced custodians eg: 5 // to allow add more redundancy like "backup" but under max control to handle balanced // window=[max=[0...5]...10]... later [max=[0...6]...10] <- expanded max to 6 - // TODO grant custody fee to avoid manipulation of the network or abuse -> the more custodian = more fees bool addedCustodian = _custodiansByHolder[msg.sender].add(custodian); if (!addedCustodian) revert GrantCustodyFailed(custodian, msg.sender); - _incrementCustody(custodian); // +1 under custody its analog to "demand" - emit CustodialGranted(custodian, msg.sender); + + uint256 demand = _incrementCustody(custodian); // +1 under custody its analog to "demand" + emit CustodialGranted(custodian, msg.sender, demand); } /// @notice Checks if the given custodian is a custodian for the specified content holder @@ -139,75 +142,43 @@ contract RightsAssetCustodian is Initializable, UUPSUpgradeable, AccessControlle /// @notice Selects a balanced custodian for a given content rights holder based on weighted randomness. /// @dev This function behaves similarly to a load balancer in a network proxy system, where each custodian - /// acts like a server, and the function balances the requests (custody assignments) based on a weighted - /// probability distribution. Custodians with higher weights have a greater chance of being selected, much - /// like how a load balancer directs more traffic to servers with greater capacity. + /// acts like a server, and the function balances the requests (custody assignments) based on a weighted + /// probability distribution. Custodians with higher weights have a greater chance of being selected, much + /// like how a load balancer directs more traffic to servers with greater capacity. /// @param holder The address of the asset rights holder whose custodian is to be selected. function getBalancedCustodian(address holder) external view returns (address chosen) { - uint256 i = 0; - uint256 acc = 0; - bytes32 blockHash = blockhash(block.number - 1); - // This calculation limits the resulting range to 0-9999. - // Example: a % b => 153_000 % 10_000 - // Step 1: 153_000 / 10_000 = 15.3 (integer part = 15) - // Step 2: 15 * 10_000 = 150_000 - // Step 3: 153_000 - 150_000 = [3_000] - // The remainder [3_000] represents the leftover from the division, - // as the divisor covers the largest possible portion of the dividend - // with complete multiples, leaving the rest as the remainder. - // 15 integer parts make up 150_000, while the remaining 3_000 is the residue. + // Adjust 'n' to comply with the maximum distribution redundancy: + // This ensures that no more redundancy than allowed is used, + // even if more custodians are available. + address[] memory custodians = getCustodians(holder); + uint256 n = custodians.length; + if (n == 0) return chosen; + + n = _maxCustodianRedundancy < n ? _maxCustodianRedundancy : n; + (uint256[] memory weights, uint256 totalWeight) = _calcWeights(custodians, n); /// IMPORTANT: The randomness used here is not cryptographically secure, /// but sufficient for this non-critical operation. The random number is generated /// using the block hash and the holder's address, and is used to determine which custodian is selected. // slither-disable-next-line weak-prng - uint256 random = uint256(keccak256(abi.encodePacked(blockHash, holder))) % C.BPS_MAX; - uint256 n = _custodiansByHolder[holder].length(); - // Adjust 'n' to comply with the maximum distribution redundancy: - // This ensures that no more redundancy than allowed is used, - // even if more custodians are available. - n = _maxDistributionRedundancy < n ? _maxDistributionRedundancy : n; - // Arithmetic succession of weights: - // Example: n = total number of nodes - // Node 1 = weight 3 - // Node 2 = weight 2 - // Node 3 = weight 1 - // Total weight = 6 (sum of weights in descending order) - // - // To calculate the sum of all node weights dynamically: - // Adding a new node automatically adjusts both the weights and their total sum. - // Formula for the sum of an arithmetic succession: S = n(n + 1) / 2 - // Example: - // Before: n = 3 -> 1 + 2 + 3 = 6 - // After: n = 4 -> 1 + 2 + 3 + 4 = 10 - uint256 s = (n * (n + 1)) / 2; + bytes32 blockHash = blockhash(block.number - 1); + uint256 randomSeed = uint256(keccak256(abi.encodePacked(blockHash, holder))); + uint256 random = randomSeed % totalWeight; + + uint256 i = 0; + uint256 acc = 0; while (i < n) { - // Calculate the weight for each node based on its index (n - i), where the first node gets - // the highest weight, and the weights decrease as i increases. - // We multiply by BPS_MAX (usually 10,000 bps = 100%) to ensure precision, and divide by - // the total weight sum 's' to normalize. - // Formula: w = ((n - i) * BPS_MAX) / s - // // In a categorical probability distribution, nodes with higher weights have a greater chance // of being selected. The random value is checked against the cumulative weight. // Example distribution: // |------------50------------|--------30--------|-----20------| // | 0 - 50 | 51 - 80 | 81 - 100 | <- acc <> random hit range // The first node (50%) has the highest chance, followed by the second (30%) and the third (20%). - - // += weight for node i - // Each node's weight is calculated as a proportion of the total weight (`s`), - // based on its position (order). The first node has the highest weight, with - // subsequent nodes receiving progressively smaller weights. - // Example: First node = weight 3 * 10,000 / total weight (s) - // This ensures nodes with higher weights (closer to the start) have a greater - // probability of being selected. - address candidate = _custodiansByHolder[holder].at(i); - // uint256 demand = _holdersUnderCustodian[candidate]; - acc += ((n - i) * C.BPS_MAX) / s; // TODO add "demand" as variable to calc weight - if (acc >= random && _isValidActiveCustodian(candidate)) { - chosen = candidate; + acc += weights[i]; + if (random < acc) { + chosen = custodians[i]; + break; } // i can't overflow n @@ -217,11 +188,39 @@ contract RightsAssetCustodian is Initializable, UUPSUpgradeable, AccessControlle } } - /// @notice Retrieves the addresses of the custodians assigned to a specific content holder. - /// @dev Is not guaranteed that returned custodians are actives. use `getBalancedCustodian` in place. + // function fallbackCustodian(){} + + /// @notice Retrieves the addresses of the active custodians assigned to a specific content holder. /// @param holder The address of the asset holder whose custodians are being retrieved. - function getCustodians(address holder) external view returns (address[] memory) { - return _custodiansByHolder[holder].values(); + function getCustodians(address holder) public view returns (address[] memory) { + address[] memory custodians = _custodiansByHolder[holder].values(); + address[] memory filtered = new address[](custodians.length); + uint256 custodiansLen = custodians.length; + uint256 j = 0; + + for (uint256 i; i < custodiansLen; i = i.uncheckedInc()) { + if (!_isValidActiveCustodian(custodians[i])) continue; + filtered[j] = custodians[i]; + + // safe unchecked + // limited to i increment = max custodian len + j = j.uncheckedInc(); + } + + // Explanation: + // - The `filtered` array was initially created with the same length as `custodians`, meaning + // it may contain uninitialized elements (`address(0)`) if some custodians were invalid. + // - The variable `j` represents the number of valid custodians that passed the filtering process. + // - To ensure that the returned array contains only these valid custodians and no extra default values, + // we call `slice(j)`, which creates a new array of exact length `j` and copies only + // the first `j` elements from `filtered`. + // - This prevents returning an array with trailing `address(0)` values, ensuring data integrity + // and reducing unnecessary gas costs when the array is processed elsewhere. + assembly { + mstore(filtered, j) + } + + return filtered; } /// @dev Authorizes the upgrade of the contract. @@ -236,17 +235,54 @@ contract RightsAssetCustodian is Initializable, UUPSUpgradeable, AccessControlle return custodian != address(0) && CUSTODIAN_REFERENDUM.isActive(custodian); } - /// @dev Increments the count of holders under a given custodian's custody. - /// @param custodian The address of the custodian whose custody count will be incremented. - function _incrementCustody(address custodian) private { + /// @dev Increases the count of holders served by the custodian. + /// @param custodian The custodian to increment. + /// @return The new demand value for this custodian. + function _incrementCustody(address custodian) private returns (uint256) { _holdersUnderCustodian[custodian] += 1; + return _holdersUnderCustodian[custodian]; } - /// @dev Decrements the count of holders under a given custodian's custody. - /// @param custodian The address of the custodian whose custody count will be decremented. - function _decrementCustody(address custodian) private { + /// @dev Decreases the count of holders served by the custodian. + /// @param custodian The custodian to decrement. + /// @return The new demand value after the update. + function _decrementCustody(address custodian) private returns (uint256) { if (_holdersUnderCustodian[custodian] > 0) { _holdersUnderCustodian[custodian] -= 1; } + + return _holdersUnderCustodian[custodian]; + } + + /// @notice Calculates the effective weights for each custodian in the selection window. + /// @dev The formula used is: (window - i) * (demand + 1), where: + /// - 'window - i' reflects the custodian's positional priority (as selected by the creator), + /// - 'demand + 1' reflects its current engagement level (i.e., how many holders trust it). + /// This approach increases the probability of selecting custodians who are both + /// preferred by the holder and already trusted by more participants, effectively + /// reinforcing reputation and operational reliability. + /// @param custodians The list of candidate custodians (already filtered for activeness). + /// @param window The number of custodians to consider (typically capped by redundancy factor). + /// @return weights An array of computed weights for each custodian. + /// @return totalWeight The sum of all weights, used for normalization or random selection. + function _calcWeights( + address[] memory custodians, + uint256 window + ) private view returns (uint256[] memory weights, uint256 totalWeight) { + weights = new uint256[](window); + + for (uint256 i = 0; i < window; i = i.uncheckedInc()) { + // assign higher weight to earlier positions (creator priority), but adjust for demand. + uint256 d = _holdersUnderCustodian[custodians[i]]; + // EffectiveWeight_i = (n - i) * (Demand_i + 1) + uint256 w = (window - i) * (d + 1); + + // safe + // limited to window + unchecked { + totalWeight += w; + weights[i] = w; + } + } } } diff --git a/contracts/rights/RightsPolicyAuthorizer.sol b/contracts/rights/RightsPolicyAuthorizer.sol index 392373a..51a4313 100644 --- a/contracts/rights/RightsPolicyAuthorizer.sol +++ b/contracts/rights/RightsPolicyAuthorizer.sol @@ -88,7 +88,7 @@ contract RightsPolicyAuthorizer is /// @notice Initializes and authorizes a policy contract for content held by the holder. /// @param policy The address of the policy contract to be initialized and authorized. - /// @param data The data to initialize policy. + /// @param data The data to initialize policy. e.g., prices, timeframes.. function authorizePolicy(address policy, bytes calldata data) external onlyAuditedPolicies(policy) { _initializePolicy(policy, data); _authorizedPolicies[msg.sender].add(policy); diff --git a/contracts/rights/RightsPolicyManager.sol b/contracts/rights/RightsPolicyManager.sol index 0c9a4c2..e0f9160 100644 --- a/contracts/rights/RightsPolicyManager.sol +++ b/contracts/rights/RightsPolicyManager.sol @@ -42,7 +42,7 @@ contract RightsPolicyManager is Initializable, UUPSUpgradeable, AccessControlled /// @param proof A unique identifier for the agreement between holder and account. /// @param attestationId A unique identifier for the attestation that confirms the registration. /// @param policy The address of the registered policy governing the access rights. - event Registered(address indexed account, uint256 proof, uint256 attestationId, address policy); + event Registered(address indexed account, uint256 indexed proof, uint256 attestationId, address policy); /// @dev Error thrown when a policy registration fails. /// @param account The address of the account for which the policy registration failed. diff --git a/test/assets/AssetReferendum.t.sol b/test/assets/AssetReferendum.t.sol index bd42994..dbd0a38 100644 --- a/test/assets/AssetReferendum.t.sol +++ b/test/assets/AssetReferendum.t.sol @@ -20,7 +20,7 @@ contract AssetReferendumTest is BaseTest { function test_Submit_SubmittedEventEmitted() public { vm.warp(1641070800); vm.prank(user); - vm.expectEmit(true, false, false, true, address(assetReferendum)); + vm.expectEmit(true, true, false, true, address(assetReferendum)); emit AssetReferendum.Submitted(user, 1); IAssetRegistrable(assetReferendum).submit(1); } @@ -36,7 +36,7 @@ contract AssetReferendumTest is BaseTest { _submitContentAsUser(assetId); vm.warp(1641070805); vm.startPrank(governor); // approve by governance.. - vm.expectEmit(false, false, false, true, address(assetReferendum)); + vm.expectEmit(true, false, false, true, address(assetReferendum)); emit AssetReferendum.Approved(assetId); IAssetRegistrable(assetReferendum).approve(assetId); vm.stopPrank(); @@ -57,7 +57,7 @@ contract AssetReferendumTest is BaseTest { _submitContentAsUser(assetId); vm.warp(1641070805); vm.prank(governor); // approve by governance.. - vm.expectEmit(false, false, false, true, address(assetReferendum)); + vm.expectEmit(true, false, false, true, address(assetReferendum)); emit AssetReferendum.Rejected(assetId); IAssetRegistrable(assetReferendum).reject(assetId); } @@ -81,7 +81,7 @@ contract AssetReferendumTest is BaseTest { // then a revoke should ve done IAssetRegistrable(assetReferendum).approve(assetId); - vm.expectEmit(false, false, false, true, address(assetReferendum)); + vm.expectEmit(true, false, false, true, address(assetReferendum)); emit AssetReferendum.Revoked(assetId); IAssetRegistrable(assetReferendum).revoke(assetId); vm.stopPrank(); // reject by governance.. From a3e5c33e8b7cf8ee5cfd4ed0e4e89e24a9ef4021 Mon Sep 17 00:00:00 2001 From: Geolffrey Mena Date: Fri, 9 May 2025 17:28:41 -0600 Subject: [PATCH 11/16] feat: Delegated Weighted Reputation Routing (DWRR) --- contracts/access/AccessManager.sol | 18 ++++++---- contracts/custody/CustodianImpl.sol | 1 - contracts/custody/CustodianReferendum.sol | 1 - contracts/economics/Tollgate.sol | 4 +-- contracts/financial/AgreementManager.sol | 5 +-- contracts/rights/RightsAssetCustodian.sol | 9 ++--- test/custody/CustodianReferendum.t.sol | 41 +++++++++-------------- 7 files changed, 36 insertions(+), 43 deletions(-) diff --git a/contracts/access/AccessManager.sol b/contracts/access/AccessManager.sol index f67cde8..783035b 100644 --- a/contracts/access/AccessManager.sol +++ b/contracts/access/AccessManager.sol @@ -34,16 +34,22 @@ contract AccessManager is Initializable, UUPSUpgradeable, AccessManagerUpgradeab // Strategic roles for governance classification within the protocol: // // Community Governance Role: - // - GOV_ROLE: Represents decentralized community governance. Decisions are made through collective voting mechanisms (e.g., token-weighted, quadratic). + // - GOV_ROLE: Represents decentralized community governance. + // Decisions are made through collective voting mechanisms (e.g., token-weighted, quadratic). // // Group/Sub-DAO Based Roles: - // - ADMIN_ROLE: Managed by a smart account or sub-DAO. Handles protocol upgrades, pause mechanisms, and operational role assignments. - // - MOD_ROLE: Managed by a smart account or sub-DAO. Approves policy submissions and moderates hook operations. - // - REF_ROLE: Managed by a smart account or sub-DAO. Participates in governance referenda for content curation and distributor selection. + // - ADMIN_ROLE: Managed by a smart account or sub-DAO. + // Handles protocol upgrades, pause mechanisms, and operational role assignments. + // - MOD_ROLE: Managed by a smart account or sub-DAO. + // Approves policy submissions and moderates hook operations. + // - REF_ROLE: Managed by a smart account or sub-DAO. + // Participates in governance referenda for content curation and distributor selection. // // Individual/Contract Based Roles: - // - OPS_ROLE: Internal operational role assigned to protocol-trusted contracts for direct module interactions. No human involvement. - // - VER_ROLE: Individual role assigned to trusted creators, enabling content uploads without conventional verification. + // - OPS_ROLE: Internal operational role assigned to protocol-trusted contracts + // for direct module interactions. No human involvement. + // - VER_ROLE: Individual role assigned to trusted creators, enabling + // content uploads without conventional verification. /* GOV_ROLE (Community Governance) diff --git a/contracts/custody/CustodianImpl.sol b/contracts/custody/CustodianImpl.sol index 0554d79..63d2c26 100644 --- a/contracts/custody/CustodianImpl.sol +++ b/contracts/custody/CustodianImpl.sol @@ -19,7 +19,6 @@ import { FinancialOps } from "@synaps3/core/libraries/FinancialOps.sol"; /// - Calls to this contract are made through a `BeaconProxy`, allowing upgrades at the beacon level. /// - This contract itself is NOT upgradeable directly; its updates are managed by the beacon. /// - It inherits from upgradeable contracts **ONLY** to maintain compatibility with their storage layout (ERC-7201). -/// - This approach ensures that future improvement to the implementation do not break the beacon proxy's storage layout. contract CustodianImpl is Initializable, ERC165Upgradeable, OwnableUpgradeable, ICustodian { using FinancialOps for address; diff --git a/contracts/custody/CustodianReferendum.sol b/contracts/custody/CustodianReferendum.sol index 8a7e8ca..e960a19 100644 --- a/contracts/custody/CustodianReferendum.sol +++ b/contracts/custody/CustodianReferendum.sol @@ -8,7 +8,6 @@ import { Initializable } from "@openzeppelin/contracts-upgradeable/proxy/utils/I // solhint-disable-next-line max-line-length import { AccessControlledUpgradeable } from "@synaps3/core/primitives/upgradeable/AccessControlledUpgradeable.sol"; import { QuorumUpgradeable } from "@synaps3/core/primitives/upgradeable/QuorumUpgradeable.sol"; -import { ITollgate } from "@synaps3/core/interfaces/economics/ITollgate.sol"; import { ICustodian } from "@synaps3/core/interfaces/custody/ICustodian.sol"; import { IAgreementSettler } from "@synaps3/core/interfaces/financial/IAgreementSettler.sol"; import { IFeeSchemeValidator } from "@synaps3/core/interfaces/economics/IFeeSchemeValidator.sol"; diff --git a/contracts/economics/Tollgate.sol b/contracts/economics/Tollgate.sol index c831b72..782c232 100644 --- a/contracts/economics/Tollgate.sol +++ b/contracts/economics/Tollgate.sol @@ -63,9 +63,9 @@ contract Tollgate is Initializable, UUPSUpgradeable, AccessControlledUpgradeable } /// @notice Allows execution if the scheme is accepted or if support cannot be determined. - /// @dev This modifier checks whether the `target` contract explicitly supports the scheme via `isFeeSchemeSupported`. + /// @dev This modifier checks whether the `target` contract explicitly supports the scheme. /// If `isFeeSchemeSupported` exists and returns `false`, the call is reverted with InvalidTargetContext. - /// If the call to `v` fails (e.g., target does not implement the function or reverts), execution is allowed by default. + /// If the call to `v` fails (e.g., target does not implement the function or reverts) allowed by default. /// This enables compatibility with contracts that do not implement scheme validation. /// @param scheme The scheme to validate. /// @param target The address of the contract expected to support the scheme. diff --git a/contracts/financial/AgreementManager.sol b/contracts/financial/AgreementManager.sol index 8622936..724cfe9 100644 --- a/contracts/financial/AgreementManager.sol +++ b/contracts/financial/AgreementManager.sol @@ -132,8 +132,9 @@ contract AgreementManager is Initializable, UUPSUpgradeable, AccessControlledUpg revert NoPartiesInAgreement(); } - // TODO Even if we are covered by gas fees, during executing a good way to avoid abuse is penalize parties after N length - // eg. The max parties allowed is 5, any extra parties are charged with a extra * fee + // TODO Even if we are covered by gas fees, during execution a good way to avoid abuse + // is penalize parties after N length eg. The max parties allowed is 5, any extra + // parties are charged with a extra * fee // IMPORTANT: // Agreements transport value and represent a defined commitment between parties. diff --git a/contracts/rights/RightsAssetCustodian.sol b/contracts/rights/RightsAssetCustodian.sol index ec247d7..edcb94e 100644 --- a/contracts/rights/RightsAssetCustodian.sol +++ b/contracts/rights/RightsAssetCustodian.sol @@ -147,16 +147,13 @@ contract RightsAssetCustodian is Initializable, UUPSUpgradeable, AccessControlle /// like how a load balancer directs more traffic to servers with greater capacity. /// @param holder The address of the asset rights holder whose custodian is to be selected. function getBalancedCustodian(address holder) external view returns (address chosen) { + address[] memory custodians = getCustodians(holder); + if (custodians.length == 0) return chosen; // TODO fallback custodian // Adjust 'n' to comply with the maximum distribution redundancy: // This ensures that no more redundancy than allowed is used, // even if more custodians are available. - address[] memory custodians = getCustodians(holder); - uint256 n = custodians.length; - if (n == 0) return chosen; - - n = _maxCustodianRedundancy < n ? _maxCustodianRedundancy : n; + uint256 n = _maxCustodianRedundancy < custodians.length ? _maxCustodianRedundancy : custodians.length; (uint256[] memory weights, uint256 totalWeight) = _calcWeights(custodians, n); - /// IMPORTANT: The randomness used here is not cryptographically secure, /// but sufficient for this non-critical operation. The random number is generated /// using the block hash and the holder's address, and is used to determine which custodian is selected. diff --git a/test/custody/CustodianReferendum.t.sol b/test/custody/CustodianReferendum.t.sol index 708cf84..3763d58 100644 --- a/test/custody/CustodianReferendum.t.sol +++ b/test/custody/CustodianReferendum.t.sol @@ -19,7 +19,6 @@ import { CustodianReferendum } from "contracts/custody/CustodianReferendum.sol"; import { T } from "contracts/core/primitives/Types.sol"; contract CustodianReferendumTest is BaseTest { - function setUp() public initialize { deployCustodianReferendum(); deployCustodianFactory(); @@ -68,20 +67,9 @@ contract CustodianReferendumTest is BaseTest { vm.warp(1641070803); vm.startPrank(admin); // approve fees payment: admin default account - IERC20(token).approve(ledger, expectedFees); - ILedgerVault(ledger).deposit(admin, expectedFees, token); - ILedgerVault(ledger).approve(address(agreementManager), expectedFees, token); - address[] memory parties = new address[](1); parties[0] = custodian; - - uint256 proof = IAgreementManager(agreementManager).createAgreement( - expectedFees, - token, - address(custodianReferendum), - parties, - "" - ); + uint256 proof = _createAgreement(expectedFees, parties); vm.expectEmit(true, false, false, true, address(custodianReferendum)); emit CustodianReferendum.Registered(custodian, expectedFees); @@ -89,7 +77,6 @@ contract CustodianReferendumTest is BaseTest { vm.stopPrank(); } - function test_Register_RevertIf_InvalidAgreement() public { uint256 expectedFees = 100 * 1e18; // 100 MMC address custodian = deployCustodian("contentrider.com"); @@ -100,7 +87,6 @@ contract CustodianReferendumTest is BaseTest { ICustodianRegistrable(custodianReferendum).register(0, custodian); } - function test_Register_SetValidEnrollmentTime() public { address custodian = deployCustodian("contentrider.com"); ICustodianInspectable inspectable = ICustodianInspectable(custodianReferendum); @@ -204,33 +190,38 @@ contract CustodianReferendumTest is BaseTest { // only manager can pay enrollment.. vm.startPrank(admin); // approve approval to ledger to deposit funds - IERC20(token).approve(ledger, approval); - ILedgerVault(ledger).deposit(admin, approval, token); - ILedgerVault(ledger).approve(address(agreementManager), approval, token); - address[] memory parties = new address[](1); parties[0] = d9r; + uint256 proof = _createAgreement(approval, parties); + // operate over msg.sender ledger registered funds + ICustodianRegistrable(custodianReferendum).register(proof, d9r); + vm.stopPrank(); + } + + function _createAgreement(uint256 amount, address[] memory parties) private returns (uint256) { + IERC20(token).approve(ledger, amount); + ILedgerVault(ledger).deposit(admin, amount, token); + uint256 proof = IAgreementManager(agreementManager).createAgreement( - approval, + amount, token, address(custodianReferendum), parties, "" ); - // operate over msg.sender ledger registered funds - ICustodianRegistrable(custodianReferendum).register(proof, d9r); - vm.stopPrank(); + + return proof; } - function _registerCustodianWithGovernorAndApproval() internal { + function _registerCustodianWithGovernorAndApproval() private { uint256 expectedFees = 100 * 1e18; address custodian = deployCustodian("contentrider.com"); _setFeesAsGovernor(expectedFees); _registerCustodianWithApproval(custodian, expectedFees); } - function _registerAndApproveCustodian(address d9r) internal { + function _registerAndApproveCustodian(address d9r) private { // intially the balance = 0 _setFeesAsGovernor(1 * 1e18); // register the custodian with fees = 100 MMC From 0a9d1dd431409ad3d465d9c874464e8d1438a2b7 Mon Sep 17 00:00:00 2001 From: Geolffrey Mena Date: Mon, 12 May 2025 15:03:49 -0600 Subject: [PATCH 12/16] refactor(custodian): removed erc165 checker --- .../interfaces/custody/ICustodianFactory.sol | 5 ++ contracts/custody/CustodianFactory.sol | 8 ++- contracts/custody/CustodianImpl.sol | 2 + contracts/custody/CustodianReferendum.sol | 54 +++++++------------ contracts/economics/Treasury.sol | 2 +- contracts/financial/AgreementManager.sol | 2 +- contracts/financial/AgreementSettler.sol | 11 ++-- contracts/rights/RightsPolicyAuthorizer.sol | 19 +++---- contracts/rights/RightsPolicyManager.sol | 14 ++++- ...0_Deploy_Custody_CustodianReferendum.s.sol | 3 +- ..._Upgrade_Custody_CustodianReferendum.s.sol | 3 +- test/BaseTest.t.sol | 1 + test/custody/CustodianReferendum.t.sol | 12 +++-- 13 files changed, 75 insertions(+), 61 deletions(-) diff --git a/contracts/core/interfaces/custody/ICustodianFactory.sol b/contracts/core/interfaces/custody/ICustodianFactory.sol index 095321c..d2e0956 100644 --- a/contracts/core/interfaces/custody/ICustodianFactory.sol +++ b/contracts/core/interfaces/custody/ICustodianFactory.sol @@ -15,4 +15,9 @@ interface ICustodianFactory { /// @param custodian The address of the custodian contract. /// @return The address of the entity that created the custodian. function getCreator(address custodian) external view returns (address); + + /// @notice Checks whether a given custodian contract has been registered. + /// @param custodian The address of the custodian contract to check. + /// @return True if the custodian is registered; false otherwise. + function isRegistered(address custodian) external view returns (bool); } diff --git a/contracts/custody/CustodianFactory.sol b/contracts/custody/CustodianFactory.sol index 8cd724a..b151a65 100644 --- a/contracts/custody/CustodianFactory.sol +++ b/contracts/custody/CustodianFactory.sol @@ -51,7 +51,13 @@ contract CustodianFactory is UpgradeableBeacon, ICustodianFactory { return _manager[custodian]; } - // TODO: isRegistered(endpoint) + /// @notice Checks whether a given custodian contract has been registered. + /// @dev A custodian is considered registered if its address is mapped to a creator in the `_manager` mapping. + /// @param custodian The address of the custodian contract to check. + /// @return True if the custodian is registered; false otherwise. + function isRegistered(address custodian) external view returns (bool) { + return _manager[custodian] != address(0); + } /// @notice Function to create a new custodian contract. /// @dev Ensures that the same endpoint is not registered twice. diff --git a/contracts/custody/CustodianImpl.sol b/contracts/custody/CustodianImpl.sol index 63d2c26..92a5ed4 100644 --- a/contracts/custody/CustodianImpl.sol +++ b/contracts/custody/CustodianImpl.sol @@ -83,6 +83,8 @@ contract CustodianImpl is Initializable, ERC165Upgradeable, OwnableUpgradeable, return amount; } + // TODO allow deposits to stake balance + /// @notice Retrieves the contract's balance for a given currency. /// @param currency The token address to check the balance of (use `address(0)` for native currency). /// @dev This function is restricted to the contract owner. diff --git a/contracts/custody/CustodianReferendum.sol b/contracts/custody/CustodianReferendum.sol index e960a19..eec05ba 100644 --- a/contracts/custody/CustodianReferendum.sol +++ b/contracts/custody/CustodianReferendum.sol @@ -12,6 +12,7 @@ import { ICustodian } from "@synaps3/core/interfaces/custody/ICustodian.sol"; import { IAgreementSettler } from "@synaps3/core/interfaces/financial/IAgreementSettler.sol"; import { IFeeSchemeValidator } from "@synaps3/core/interfaces/economics/IFeeSchemeValidator.sol"; import { ICustodianReferendum } from "@synaps3/core/interfaces/custody/ICustodianReferendum.sol"; +import { ICustodianFactory } from "@synaps3/core/interfaces/custody/ICustodianFactory.sol"; import { FinancialOps } from "@synaps3/core/libraries/FinancialOps.sol"; import { T } from "@synaps3/core/primitives/Types.sol"; @@ -31,11 +32,9 @@ contract CustodianReferendum is using FinancialOps for address; using ERC165Checker for address; - /// @dev Stores the interface ID for ICustodian, ensuring compatibility verification. - bytes4 private constant INTERFACE_ID_CUSTODIAN = type(ICustodian).interfaceId; - /// @custom:oz-upgrades-unsafe-allow state-variable-immutable IAgreementSettler public immutable AGREEMENT_SETTLER; + ICustodianFactory public immutable CUSTODIAN_FACTORY; //slither-disable-end naming-convention /// @dev Defines the expiration period for enrollment, determining how long a custodian remains active. @@ -62,43 +61,31 @@ contract CustodianReferendum is /// @param newPeriod The new period that is set, could be in seconds, blocks, or any other unit event PeriodSet(uint256 newPeriod); - /// @notice Error thrown when a custodian contract is invalid - /// @param invalid The address of the custodian contract that is invalid - error InvalidCustodianContract(address invalid); - - /// @notice Error thrown when the caller is not authorized as the custodian's manager. - /// @param caller The address attempting the action without being the manager. - error UnauthorizedCustodianManager(address caller); + /// @notice Error thrown when the custodian is not recognized by the factory. + /// @param custodian The address of the unregistered custodian contract. + error UnregisteredCustodian(address custodian); /// @notice Error thrown when the custodian does not match the agreement's registered party. - /// @param custodian The custodian provided for the operation. + /// @param custodian The custodian provided for the operation.s error CustodianAgreementMismatch(address custodian); - /// @notice Modifier to ensure that the given custodian contract supports the ICustodian interface. - /// @param custodian The custodian contract address. + /// @notice Modifier to ensure the custodian was deployed through the trusted factory and is registered in the system. + /// @param custodian The address of the custodian contract to verify. modifier onlyValidCustodian(address custodian) { - if (!custodian.supportsInterface(INTERFACE_ID_CUSTODIAN)) { - revert InvalidCustodianContract(custodian); - } - _; - } - - /// @notice Modifier to ensure that only the custodian's manager can perform the operation. - /// @param custodian The address of the custodian contract whose manager must authorize this operation. - modifier onlyCustodianManager(address custodian) { - address manager = ICustodian(custodian).getManager(); - if (msg.sender != manager) { - revert UnauthorizedCustodianManager(msg.sender); + // ensure the custodian was deployed through the trusted factory and is known to the protocol + if (!CUSTODIAN_FACTORY.isRegistered(custodian)) { + revert UnregisteredCustodian(msg.sender); } _; } /// @custom:oz-upgrades-unsafe-allow constructor - constructor(address agreementSettler) { + constructor(address agreementSettler, address custodianFactory) { /// https://forum.openzeppelin.com/t/what-does-disableinitializers-function-mean/28730/5 /// https://forum.openzeppelin.com/t/uupsupgradeable-vulnerability-post-mortem/15680 _disableInitializers(); AGREEMENT_SETTLER = IAgreementSettler(agreementSettler); + CUSTODIAN_FACTORY = ICustodianFactory(custodianFactory); } /// @notice Initializes the proxy state. @@ -137,7 +124,7 @@ contract CustodianReferendum is /// @notice Checks if the entity is active. /// @dev This function verifies the active status of the custodian. /// @param custodian The custodian's address to check. - function isActive(address custodian) external view onlyValidCustodian(custodian) returns (bool) { + function isActive(address custodian) external view returns (bool) { // TODO a renovation mechanism is needed to update the enrollment time /// It ensures that custodians remain engaged and do not become inactive for extended periods. /// The enrollment deadline enforces a time-based mechanism where custodians must renew @@ -159,24 +146,21 @@ contract CustodianReferendum is /// @notice Checks if the entity is waiting. /// @dev This function verifies the waiting status of the custodian. /// @param custodian The custodian's address to check. - function isWaiting(address custodian) external view onlyValidCustodian(custodian) returns (bool) { + function isWaiting(address custodian) external view returns (bool) { return _status(uint160(custodian)) == T.Status.Waiting; } /// @notice Checks if the entity is blocked. /// @dev This function verifies the blocked status of the custodian. /// @param custodian The custodian's address to check. - function isBlocked(address custodian) external view onlyValidCustodian(custodian) returns (bool) { + function isBlocked(address custodian) external view returns (bool) { return _status(uint160(custodian)) == T.Status.Blocked; } /// @notice Registers a custodian by sending a payment to the contract. /// @param proof The unique identifier of the agreement to be enforced. /// @param custodian The address of the custodian to register. - function register( - uint256 proof, - address custodian - ) external onlyValidCustodian(custodian) onlyCustodianManager(custodian) { + function register(uint256 proof, address custodian) external onlyValidCustodian(custodian) { /// TODO penalize invalid endpoints, and revoked during referendum // !IMPORTANT: // Fees act as a mechanism to prevent abuse or spam by users @@ -205,7 +189,7 @@ contract CustodianReferendum is /// @notice Approves a custodian's registration. /// @param custodian The address of the custodian to approve. - function approve(address custodian) external restricted onlyValidCustodian(custodian) { + function approve(address custodian) external restricted { _enrollmentsCount++; _approve(uint160(custodian)); emit Approved(custodian); @@ -213,7 +197,7 @@ contract CustodianReferendum is /// @notice Revokes the registration of a custodian. /// @param custodian The address of the custodian to revoke. - function revoke(address custodian) external restricted onlyValidCustodian(custodian) { + function revoke(address custodian) external restricted { _enrollmentsCount--; _revoke(uint160(custodian)); emit Revoked(custodian); diff --git a/contracts/economics/Treasury.sol b/contracts/economics/Treasury.sol index 3697641..dd77581 100644 --- a/contracts/economics/Treasury.sol +++ b/contracts/economics/Treasury.sol @@ -73,7 +73,7 @@ contract Treasury is /// Only the governor can execute this function, ensuring controlled fee collection. /// @param collector The address of an authorized fee collector. /// @param currency The address of the ERC20 token for which fees are being collected. - function collectFees(address collector, address currency) external restricted { + function collectFees(address collector, address currency) external restricted nonReentrant { IFeesCollector feesCollector = IFeesCollector(collector); uint256 collected = feesCollector.disburse(currency); _sumLedgerEntry(address(this), collected, currency); diff --git a/contracts/financial/AgreementManager.sol b/contracts/financial/AgreementManager.sol index 724cfe9..ea8b3f1 100644 --- a/contracts/financial/AgreementManager.sol +++ b/contracts/financial/AgreementManager.sol @@ -134,7 +134,7 @@ contract AgreementManager is Initializable, UUPSUpgradeable, AccessControlledUpg // TODO Even if we are covered by gas fees, during execution a good way to avoid abuse // is penalize parties after N length eg. The max parties allowed is 5, any extra - // parties are charged with a extra * fee + // parties are charged with a extra * fee. Denial of Service risk // IMPORTANT: // Agreements transport value and represent a defined commitment between parties. diff --git a/contracts/financial/AgreementSettler.sol b/contracts/financial/AgreementSettler.sol index a86c5af..181c4c5 100644 --- a/contracts/financial/AgreementSettler.sol +++ b/contracts/financial/AgreementSettler.sol @@ -7,6 +7,8 @@ import { Initializable } from "@openzeppelin/contracts-upgradeable/proxy/utils/I import { UUPSUpgradeable } from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; import { AccessControlledUpgradeable } from "@synaps3/core/primitives/upgradeable/AccessControlledUpgradeable.sol"; // solhint-disable-next-line max-line-length +import { ReentrancyGuardTransientUpgradeable } from "@openzeppelin/contracts-upgradeable/utils/ReentrancyGuardTransientUpgradeable.sol"; +// solhint-disable-next-line max-line-length import { FeesCollectorUpgradeable } from "@synaps3/core/primitives/upgradeable/FeesCollectorUpgradeable.sol"; import { ILedgerVault } from "@synaps3/core/interfaces/financial/ILedgerVault.sol"; @@ -27,6 +29,7 @@ contract AgreementSettler is UUPSUpgradeable, AccessControlledUpgradeable, FeesCollectorUpgradeable, + ReentrancyGuardTransientUpgradeable, IAgreementSettler { using FeesOps for uint256; @@ -83,7 +86,7 @@ contract AgreementSettler is /// @notice Ensures the agreement associated with the provided `proof` is valid and active. modifier onlyValidAgreement(uint256 proof) { - if (_settledProofs[proof]) { + if (_settledProofs[proof] == true) { revert AgreementAlreadySettled(); } _; @@ -102,6 +105,7 @@ contract AgreementSettler is /// Initialize the proxy state. function initialize(address accessManager) public initializer { __UUPSUpgradeable_init(); + __ReentrancyGuardTransient_init(); __AccessControlled_init(accessManager); __FeesCollector_init(address(TREASURY)); } @@ -120,7 +124,7 @@ contract AgreementSettler is /// @notice Allows the initiator to quit the agreement and receive the committed funds. /// @param proof The unique identifier of the agreement. - function quitAgreement(uint256 proof) external onlyValidAgreement(proof) returns (T.Agreement memory) { + function quitAgreement(uint256 proof) external onlyValidAgreement(proof) nonReentrant returns (T.Agreement memory) { T.Agreement memory agreement = AGREEMENT_MANAGER.getAgreement(proof); if (agreement.initiator != msg.sender) revert UnauthorizedInitiator(); @@ -134,7 +138,6 @@ contract AgreementSettler is // (as defined in `previewAgreement`).This design disincentives manipulation, // ensuring that no changes can occur later to unfairly benefit or harm the initiator or other parties involved. - // // Penalty fees retained here also help maintain the protocol's economic balance // and ensure that the system operates sustainably over time. uint256 fees = agreement.fees; // keep fees as penalty @@ -160,7 +163,7 @@ contract AgreementSettler is function settleAgreement( uint256 proof, address counterparty - ) public onlyValidAgreement(proof) returns (T.Agreement memory) { + ) public onlyValidAgreement(proof) nonReentrant returns (T.Agreement memory) { // retrieve the agreement to storage to inactivate it and return it T.Agreement memory agreement = AGREEMENT_MANAGER.getAgreement(proof); if (agreement.arbiter != msg.sender) revert UnauthorizedEscrowAgent(); diff --git a/contracts/rights/RightsPolicyAuthorizer.sol b/contracts/rights/RightsPolicyAuthorizer.sol index 51a4313..b884326 100644 --- a/contracts/rights/RightsPolicyAuthorizer.sol +++ b/contracts/rights/RightsPolicyAuthorizer.sol @@ -6,6 +6,8 @@ import { Initializable } from "@openzeppelin/contracts-upgradeable/proxy/utils/I import { EnumerableSet } from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; import { UUPSUpgradeable } from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; import { AccessControlledUpgradeable } from "@synaps3/core/primitives/upgradeable/AccessControlledUpgradeable.sol"; +// solhint-disable-next-line max-line-length +import { ReentrancyGuardTransientUpgradeable } from "@openzeppelin/contracts-upgradeable/utils/ReentrancyGuardTransientUpgradeable.sol"; import { IRightsPolicyAuthorizer } from "@synaps3/core/interfaces/rights/IRightsPolicyAuthorizer.sol"; import { IPolicyAuditorVerifiable } from "@synaps3/core/interfaces/policies/IPolicyAuditorVerifiable.sol"; import { IPolicy } from "@synaps3/core/interfaces/policies/IPolicy.sol"; @@ -20,6 +22,7 @@ contract RightsPolicyAuthorizer is Initializable, UUPSUpgradeable, AccessControlledUpgradeable, + ReentrancyGuardTransientUpgradeable, IRightsPolicyAuthorizer { using LoopOps for uint256; @@ -83,14 +86,17 @@ contract RightsPolicyAuthorizer is /// @notice Initializes the proxy state. function initialize(address accessManager) public initializer { __UUPSUpgradeable_init(); + __ReentrancyGuardTransient_init(); __AccessControlled_init(accessManager); } /// @notice Initializes and authorizes a policy contract for content held by the holder. /// @param policy The address of the policy contract to be initialized and authorized. /// @param data The data to initialize policy. e.g., prices, timeframes.. - function authorizePolicy(address policy, bytes calldata data) external onlyAuditedPolicies(policy) { - _initializePolicy(policy, data); + function authorizePolicy(address policy, bytes calldata data) external onlyAuditedPolicies(policy) nonReentrant { + // type safe low level call to policy, call policy initialization with provided data.. + (bool success, ) = policy.call(abi.encodeCall(IPolicy.setup, (msg.sender, data))); + if (!success) revert InvalidPolicyInitialization("Error during policy initialization call"); _authorizedPolicies[msg.sender].add(policy); emit RightsGranted(policy, msg.sender, data); } @@ -163,13 +169,4 @@ contract RightsPolicyAuthorizer is function _isValidPolicy(address policy) private view returns (bool) { return (policy != address(0) && POLICY_AUDIT.isAudited(policy)); } - - /// @dev Initializes a policy by calling its `initialize` function. - /// @param policy The address of the policy contract. - /// @param data The data to initialize the policy. - function _initializePolicy(address policy, bytes calldata data) private { - // type safe low level call to policy, call policy initialization with provided data.. - (bool success, ) = policy.call(abi.encodeCall(IPolicy.setup, (msg.sender, data))); - if (!success) revert InvalidPolicyInitialization("Error during policy initialization call"); - } } diff --git a/contracts/rights/RightsPolicyManager.sol b/contracts/rights/RightsPolicyManager.sol index e0f9160..d50db04 100644 --- a/contracts/rights/RightsPolicyManager.sol +++ b/contracts/rights/RightsPolicyManager.sol @@ -6,10 +6,13 @@ import { Initializable } from "@openzeppelin/contracts-upgradeable/proxy/utils/I import { UUPSUpgradeable } from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; import { EnumerableSet } from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; import { AccessControlledUpgradeable } from "@synaps3/core/primitives/upgradeable/AccessControlledUpgradeable.sol"; +// solhint-disable-next-line max-line-length +import { ReentrancyGuardTransientUpgradeable } from "@openzeppelin/contracts-upgradeable/utils/ReentrancyGuardTransientUpgradeable.sol"; import { IPolicy } from "@synaps3/core/interfaces/policies/IPolicy.sol"; import { IAgreementSettler } from "@synaps3/core/interfaces/financial/IAgreementSettler.sol"; import { IRightsPolicyManager } from "@synaps3/core/interfaces/rights/IRightsPolicyManager.sol"; + // solhint-disable-next-line max-line-length import { IRightsPolicyAuthorizerVerifiable } from "@synaps3/core/interfaces/rights/IRightsPolicyAuthorizerVerifiable.sol"; import { LoopOps } from "@synaps3/core/libraries/LoopOps.sol"; @@ -21,7 +24,13 @@ import { T } from "@synaps3/core/primitives/Types.sol"; /// @dev This contract ensures that policies are properly authorized before being enforced. /// It interacts with the `RightsPolicyAuthorizer` to verify delegation and `AgreementSettler` /// to manage agreements. -contract RightsPolicyManager is Initializable, UUPSUpgradeable, AccessControlledUpgradeable, IRightsPolicyManager { +contract RightsPolicyManager is + Initializable, + UUPSUpgradeable, + AccessControlledUpgradeable, + ReentrancyGuardTransientUpgradeable, + IRightsPolicyManager +{ using EnumerableSet for EnumerableSet.AddressSet; using ArrayOps for address[]; using LoopOps for uint256; @@ -83,6 +92,7 @@ contract RightsPolicyManager is Initializable, UUPSUpgradeable, AccessControlled /// @notice Initializes the proxy state. function initialize(address accessManager) public initializer { __UUPSUpgradeable_init(); + __ReentrancyGuardTransient_init(); __AccessControlled_init(accessManager); } @@ -95,7 +105,7 @@ contract RightsPolicyManager is Initializable, UUPSUpgradeable, AccessControlled uint256 proof, address holder, address policy - ) external onlyAuthorizedPolicy(holder, policy) returns (uint256[] memory) { + ) external onlyAuthorizedPolicy(holder, policy) nonReentrant returns (uint256[] memory) { // 1- retrieves the agreement and marks it as settled.. T.Agreement memory agreement = AGREEMENT_SETTLER.settleAgreement(proof, holder); bytes memory callData = abi.encodeCall(IPolicy.enforce, (holder, agreement)); diff --git a/script/deployment/10_Deploy_Custody_CustodianReferendum.s.sol b/script/deployment/10_Deploy_Custody_CustodianReferendum.s.sol index 894aa7b..67d2ad4 100644 --- a/script/deployment/10_Deploy_Custody_CustodianReferendum.s.sol +++ b/script/deployment/10_Deploy_Custody_CustodianReferendum.s.sol @@ -9,8 +9,9 @@ contract DeployCustodianReferendum is DeployBase { function run() external returns (address) { vm.startBroadcast(getAdminPK()); address agreementSettler = computeCreate3Address("SALT_AGREEMENT_SETTLER"); + address custodianFactory = computeCreate3Address("SALT_CUSTODIAN_FACTORY"); address accessManager = computeCreate3Address("SALT_ACCESS_MANAGER"); - address impl = address(new CustodianReferendum(agreementSettler)); + address impl = address(new CustodianReferendum(agreementSettler, custodianFactory)); bytes memory init = abi.encodeCall(CustodianReferendum.initialize, (accessManager)); address referendum = deployUUPS(impl, init, "SALT_CUSTODIAN_REFERENDUM"); vm.stopBroadcast(); diff --git a/script/upgrades/10_Upgrade_Custody_CustodianReferendum.s.sol b/script/upgrades/10_Upgrade_Custody_CustodianReferendum.s.sol index a751562..ad0ae96 100644 --- a/script/upgrades/10_Upgrade_Custody_CustodianReferendum.s.sol +++ b/script/upgrades/10_Upgrade_Custody_CustodianReferendum.s.sol @@ -9,7 +9,8 @@ contract UpgradeCustodianReferendum is UpgradeBase { function run() external returns (address) { vm.startBroadcast(getAdminPK()); address agreementSettler = vm.envAddress("AGREEMENT_SETTLER"); - address impl = address(new CustodianReferendum(agreementSettler)); + address custodianFactory = vm.envAddress("CUSTODIAN_FACTORY"); + address impl = address(new CustodianReferendum(agreementSettler, custodianFactory)); address referendumProxy = vm.envAddress("CUSTODIAN_REFERENDUM"); // address accessManager = vm.envAddress("ACCESS_MANAGER"); //!IMPORTANT: This is not a safe upgrade, take any caution or 2-check needed before run this method diff --git a/test/BaseTest.t.sol b/test/BaseTest.t.sol index 806df9d..fef8f70 100644 --- a/test/BaseTest.t.sol +++ b/test/BaseTest.t.sol @@ -183,6 +183,7 @@ abstract contract BaseTest is Test { // 09_DeployCustodianReferendum function deployCustodianReferendum() public { + deployCustodianFactory(); deployAgreementSettler(); // set default admin as deployer.. diff --git a/test/custody/CustodianReferendum.t.sol b/test/custody/CustodianReferendum.t.sol index 3763d58..6a08d92 100644 --- a/test/custody/CustodianReferendum.t.sol +++ b/test/custody/CustodianReferendum.t.sol @@ -16,6 +16,7 @@ import { ICustodianFactory } from "contracts/core/interfaces/custody/ICustodianF import { BaseTest } from "test/BaseTest.t.sol"; import { CustodianReferendum } from "contracts/custody/CustodianReferendum.sol"; +import { CustodianImpl } from "contracts/custody/CustodianImpl.sol"; import { T } from "contracts/core/primitives/Types.sol"; contract CustodianReferendumTest is BaseTest { @@ -83,7 +84,7 @@ contract CustodianReferendumTest is BaseTest { _setFeesAsGovernor(expectedFees); // expected revert if not valid allowance vm.prank(user); - vm.expectRevert(abi.encodeWithSignature("UnauthorizedCustodianManager(address)", user)); + vm.expectRevert(abi.encodeWithSignature("UnauthorizedEscrowAgent()")); ICustodianRegistrable(custodianReferendum).register(0, custodian); } @@ -113,9 +114,12 @@ contract CustodianReferendumTest is BaseTest { } function test_Register_RevertIf_InvalidCustodian() public { - // register the custodian expecting the right status. - vm.expectRevert(abi.encodeWithSignature("InvalidCustodianContract(address)", address(0))); - ICustodianRegistrable(custodianReferendum).register(0, address(0)); + vm.prank(user); + address custodian = address(new CustodianImpl()); + + vm.prank(admin); // + vm.expectRevert(abi.encodeWithSignature("UnregisteredCustodian(address)", admin)); + ICustodianRegistrable(custodianReferendum).register(0, custodian); } function test_Approve_ApprovedEventEmitted() public { From bb0d6695b3b01fa8e5d0f088cc27c33335e3e1c8 Mon Sep 17 00:00:00 2001 From: Geolffrey Mena Date: Mon, 12 May 2025 15:03:56 -0600 Subject: [PATCH 13/16] refactor(custodian): removed erc165 checker --- contracts/custody/CustodianReferendum.sol | 2 -- 1 file changed, 2 deletions(-) diff --git a/contracts/custody/CustodianReferendum.sol b/contracts/custody/CustodianReferendum.sol index eec05ba..a74d06e 100644 --- a/contracts/custody/CustodianReferendum.sol +++ b/contracts/custody/CustodianReferendum.sol @@ -2,7 +2,6 @@ // NatSpec format convention - https://docs.soliditylang.org/en/v0.5.10/natspec-format.html pragma solidity 0.8.26; -import { ERC165Checker } from "@openzeppelin/contracts/utils/introspection/ERC165Checker.sol"; import { UUPSUpgradeable } from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; import { Initializable } from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; // solhint-disable-next-line max-line-length @@ -30,7 +29,6 @@ contract CustodianReferendum is IFeeSchemeValidator { using FinancialOps for address; - using ERC165Checker for address; /// @custom:oz-upgrades-unsafe-allow state-variable-immutable IAgreementSettler public immutable AGREEMENT_SETTLER; From bc4db9c4cb141efd30097e9bc33c2333000175a5 Mon Sep 17 00:00:00 2001 From: Geolffrey Mena Date: Mon, 12 May 2025 15:09:35 -0600 Subject: [PATCH 14/16] refactor(custodian): removed erc165 checker --- contracts/custody/CustodianReferendum.sol | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/contracts/custody/CustodianReferendum.sol b/contracts/custody/CustodianReferendum.sol index a74d06e..42a3c52 100644 --- a/contracts/custody/CustodianReferendum.sol +++ b/contracts/custody/CustodianReferendum.sol @@ -7,7 +7,6 @@ import { Initializable } from "@openzeppelin/contracts-upgradeable/proxy/utils/I // solhint-disable-next-line max-line-length import { AccessControlledUpgradeable } from "@synaps3/core/primitives/upgradeable/AccessControlledUpgradeable.sol"; import { QuorumUpgradeable } from "@synaps3/core/primitives/upgradeable/QuorumUpgradeable.sol"; -import { ICustodian } from "@synaps3/core/interfaces/custody/ICustodian.sol"; import { IAgreementSettler } from "@synaps3/core/interfaces/financial/IAgreementSettler.sol"; import { IFeeSchemeValidator } from "@synaps3/core/interfaces/economics/IFeeSchemeValidator.sol"; import { ICustodianReferendum } from "@synaps3/core/interfaces/custody/ICustodianReferendum.sol"; @@ -67,7 +66,7 @@ contract CustodianReferendum is /// @param custodian The custodian provided for the operation.s error CustodianAgreementMismatch(address custodian); - /// @notice Modifier to ensure the custodian was deployed through the trusted factory and is registered in the system. + /// @notice Modifier to ensure the custodian was deployed through the trusted factory. /// @param custodian The address of the custodian contract to verify. modifier onlyValidCustodian(address custodian) { // ensure the custodian was deployed through the trusted factory and is known to the protocol From b841b9c2a70050d04cc2acb7540324992695e07f Mon Sep 17 00:00:00 2001 From: Geolffrey Mena Date: Tue, 13 May 2025 14:21:42 -0600 Subject: [PATCH 15/16] refactor(custodian): removed erc165 checker --- contracts/access/eg.PauseManager | 2 ++ contracts/assets/eg.AssetDisputes | 1 + .../interfaces/{hooks => lifecycle}/IHook.sol | 0 .../{hooks => lifecycle}/IHookRegistry.sol | 0 .../IHookRegistryRegistrable.sol | 0 .../IHookRegistryVerifiable.sol | 0 contracts/custody/CustodianImpl.sol | 22 +++++++++---------- contracts/lifecycle/HookBase.sol | 18 +++++++++++++-- contracts/lifecycle/HookRegistry.sol | 10 ++++----- 9 files changed, 35 insertions(+), 18 deletions(-) create mode 100644 contracts/access/eg.PauseManager create mode 100644 contracts/assets/eg.AssetDisputes rename contracts/core/interfaces/{hooks => lifecycle}/IHook.sol (100%) rename contracts/core/interfaces/{hooks => lifecycle}/IHookRegistry.sol (100%) rename contracts/core/interfaces/{hooks => lifecycle}/IHookRegistryRegistrable.sol (100%) rename contracts/core/interfaces/{hooks => lifecycle}/IHookRegistryVerifiable.sol (100%) diff --git a/contracts/access/eg.PauseManager b/contracts/access/eg.PauseManager new file mode 100644 index 0000000..1657871 --- /dev/null +++ b/contracts/access/eg.PauseManager @@ -0,0 +1,2 @@ +// pause protocol using: +// pause manager + pausable \ No newline at end of file diff --git a/contracts/assets/eg.AssetDisputes b/contracts/assets/eg.AssetDisputes new file mode 100644 index 0000000..8b72219 --- /dev/null +++ b/contracts/assets/eg.AssetDisputes @@ -0,0 +1 @@ +// dispute rights via governance \ No newline at end of file diff --git a/contracts/core/interfaces/hooks/IHook.sol b/contracts/core/interfaces/lifecycle/IHook.sol similarity index 100% rename from contracts/core/interfaces/hooks/IHook.sol rename to contracts/core/interfaces/lifecycle/IHook.sol diff --git a/contracts/core/interfaces/hooks/IHookRegistry.sol b/contracts/core/interfaces/lifecycle/IHookRegistry.sol similarity index 100% rename from contracts/core/interfaces/hooks/IHookRegistry.sol rename to contracts/core/interfaces/lifecycle/IHookRegistry.sol diff --git a/contracts/core/interfaces/hooks/IHookRegistryRegistrable.sol b/contracts/core/interfaces/lifecycle/IHookRegistryRegistrable.sol similarity index 100% rename from contracts/core/interfaces/hooks/IHookRegistryRegistrable.sol rename to contracts/core/interfaces/lifecycle/IHookRegistryRegistrable.sol diff --git a/contracts/core/interfaces/hooks/IHookRegistryVerifiable.sol b/contracts/core/interfaces/lifecycle/IHookRegistryVerifiable.sol similarity index 100% rename from contracts/core/interfaces/hooks/IHookRegistryVerifiable.sol rename to contracts/core/interfaces/lifecycle/IHookRegistryVerifiable.sol diff --git a/contracts/custody/CustodianImpl.sol b/contracts/custody/CustodianImpl.sol index 92a5ed4..cac6aee 100644 --- a/contracts/custody/CustodianImpl.sol +++ b/contracts/custody/CustodianImpl.sol @@ -23,7 +23,7 @@ contract CustodianImpl is Initializable, ERC165Upgradeable, OwnableUpgradeable, using FinancialOps for address; /// @notice The custodian endpoint. - string private endpoint; + string private _endpoint; /// @notice Event emitted when the distribution endpoint is updated. /// @param oldEndpoint The previous endpoint before the update. @@ -34,14 +34,14 @@ contract CustodianImpl is Initializable, ERC165Upgradeable, OwnableUpgradeable, error InvalidEndpoint(); /// @notice Initializes the Custodian contract with the specified endpoint and owner. - /// @param endpoint_ The distribution endpoint URL. - /// @param owner_ The address of the owner who will manage the custodian. + /// @param endpoint The distribution endpoint URL. + /// @param owner The address of the owner who will manage the custodian. /// @dev Ensures that the provided endpoint is valid and initializes ERC165 and Ownable contracts. - function initialize(string calldata endpoint_, address owner_) external initializer { - if (bytes(endpoint_).length == 0) revert InvalidEndpoint(); + function initialize(string calldata endpoint, address owner) external initializer { + if (bytes(endpoint).length == 0) revert InvalidEndpoint(); __ERC165_init(); - __Ownable_init(owner_); - endpoint = endpoint_; + __Ownable_init(owner); + _endpoint = endpoint; } /// @notice Checks if the contract supports a specific interface based on its ID. @@ -57,7 +57,7 @@ contract CustodianImpl is Initializable, ERC165Upgradeable, OwnableUpgradeable, /// @notice Returns the current distribution endpoint URL. function getEndpoint() external view returns (string memory) { - return endpoint; + return _endpoint; } /// @notice Updates the distribution endpoint URL. @@ -65,8 +65,8 @@ contract CustodianImpl is Initializable, ERC165Upgradeable, OwnableUpgradeable, /// @dev Reverts if the provided endpoint is an empty string. Emits an {EndpointUpdated} event. function setEndpoint(string calldata endpoint_) external onlyOwner { if (bytes(endpoint_).length == 0) revert InvalidEndpoint(); - string memory oldEndpoint = endpoint; - endpoint = endpoint_; + string memory oldEndpoint = _endpoint; + _endpoint = endpoint_; emit EndpointUpdated(oldEndpoint, endpoint_); } @@ -84,7 +84,7 @@ contract CustodianImpl is Initializable, ERC165Upgradeable, OwnableUpgradeable, } // TODO allow deposits to stake balance - + /// @notice Retrieves the contract's balance for a given currency. /// @param currency The token address to check the balance of (use `address(0)` for native currency). /// @dev This function is restricted to the contract owner. diff --git a/contracts/lifecycle/HookBase.sol b/contracts/lifecycle/HookBase.sol index f084fe1..010b7ac 100644 --- a/contracts/lifecycle/HookBase.sol +++ b/contracts/lifecycle/HookBase.sol @@ -2,9 +2,23 @@ // NatSpec format convention - https://docs.soliditylang.org/en/v0.5.10/natspec-format.html pragma solidity 0.8.26; -// se podria aprovechar la logica de los modifier para establecer control de ejecucion antes y despues -// crear un onCall modifier para agregar a los metodo y registrar en base a cada hook acciones especificas en el llamado +import { IHook } from "contracts/core/interfaces/lifecycle/IHook.sol"; // pre - validate // during - execute // pos - verify + +abstract contract HookBase is IHook { + // pre call to check if the hook its associated with the target + // eg. SubscriptionPolicy its allowed to process this hook + function validate(address caller, address target) external returns (bool) {} + + /// @notice Called by the protocol to signal the use of the hook + /// @param caller The address invoking the hook (e.g., the user or distributor) + /// @param context Optional: bytes data for hook-specific usage + function execute(address caller, bytes calldata context) external returns (bytes memory) {} + + // post call to verify the execution, run logs, etc + // eg. SubscriptionPolicy execution complain well? + function verify(address caller, bytes calldata results) external returns (bool) {} +} diff --git a/contracts/lifecycle/HookRegistry.sol b/contracts/lifecycle/HookRegistry.sol index e9c3632..bfb43b1 100644 --- a/contracts/lifecycle/HookRegistry.sol +++ b/contracts/lifecycle/HookRegistry.sol @@ -5,11 +5,11 @@ pragma solidity 0.8.26; import { ERC165Checker } from "@openzeppelin/contracts/utils/introspection/ERC165Checker.sol"; import { Initializable } from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; import { UUPSUpgradeable } from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; -import { AccessControlledUpgradeable } from "@synaps3/core/primitives/upgradeable/AccessControlledUpgradeable.sol"; -import { QuorumUpgradeable } from "@synaps3/core/primitives/upgradeable/QuorumUpgradeable.sol"; -import { IHookRegistry } from "@synaps3/core/interfaces/hooks/IHookRegistry.sol"; -import { IHook } from "@synaps3/core/interfaces/hooks/IHook.sol"; -import { T } from "@synaps3/core/primitives/Types.sol"; +import { AccessControlledUpgradeable } from "contracts/core/primitives/upgradeable/AccessControlledUpgradeable.sol"; +import { QuorumUpgradeable } from "contracts/core/primitives/upgradeable/QuorumUpgradeable.sol"; +import { IHookRegistry } from "contracts/core/interfaces/lifecycle/IHookRegistry.sol"; +import { IHook } from "contracts/core/interfaces/lifecycle/IHook.sol"; +import { T } from "contracts/core/primitives/Types.sol"; // Hook interfaces can define logic for: // - Access control: e.g. only users holding certain tokens can access content. From daec7723a0cfa524d3b693027108ba8c03de7009 Mon Sep 17 00:00:00 2001 From: Geolffrey Mena Date: Tue, 13 May 2025 14:22:00 -0600 Subject: [PATCH 16/16] refactor(custodian): removed erc165 checker --- contracts/lifecycle/HookBase.sol | 2 +- contracts/lifecycle/HookRegistry.sol | 10 +++++----- contracts/rights/RightsAssetCustodian.sol | 13 +++++++++++++ 3 files changed, 19 insertions(+), 6 deletions(-) diff --git a/contracts/lifecycle/HookBase.sol b/contracts/lifecycle/HookBase.sol index 010b7ac..816199c 100644 --- a/contracts/lifecycle/HookBase.sol +++ b/contracts/lifecycle/HookBase.sol @@ -2,7 +2,7 @@ // NatSpec format convention - https://docs.soliditylang.org/en/v0.5.10/natspec-format.html pragma solidity 0.8.26; -import { IHook } from "contracts/core/interfaces/lifecycle/IHook.sol"; +import { IHook } from "@synaps3/core/interfaces/lifecycle/IHook.sol"; // pre - validate // during - execute diff --git a/contracts/lifecycle/HookRegistry.sol b/contracts/lifecycle/HookRegistry.sol index bfb43b1..880bda0 100644 --- a/contracts/lifecycle/HookRegistry.sol +++ b/contracts/lifecycle/HookRegistry.sol @@ -5,11 +5,11 @@ pragma solidity 0.8.26; import { ERC165Checker } from "@openzeppelin/contracts/utils/introspection/ERC165Checker.sol"; import { Initializable } from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; import { UUPSUpgradeable } from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; -import { AccessControlledUpgradeable } from "contracts/core/primitives/upgradeable/AccessControlledUpgradeable.sol"; -import { QuorumUpgradeable } from "contracts/core/primitives/upgradeable/QuorumUpgradeable.sol"; -import { IHookRegistry } from "contracts/core/interfaces/lifecycle/IHookRegistry.sol"; -import { IHook } from "contracts/core/interfaces/lifecycle/IHook.sol"; -import { T } from "contracts/core/primitives/Types.sol"; +import { AccessControlledUpgradeable } from "@synaps3/core/primitives/upgradeable/AccessControlledUpgradeable.sol"; +import { QuorumUpgradeable } from "@synaps3/core/primitives/upgradeable/QuorumUpgradeable.sol"; +import { IHookRegistry } from "@synaps3/core/interfaces/lifecycle/IHookRegistry.sol"; +import { IHook } from "@synaps3/core/interfaces/lifecycle/IHook.sol"; +import { T } from "@synaps3/core/primitives/Types.sol"; // Hook interfaces can define logic for: // - Access control: e.g. only users holding certain tokens can access content. diff --git a/contracts/rights/RightsAssetCustodian.sol b/contracts/rights/RightsAssetCustodian.sol index edcb94e..ec40f0e 100644 --- a/contracts/rights/RightsAssetCustodian.sol +++ b/contracts/rights/RightsAssetCustodian.sol @@ -123,6 +123,7 @@ contract RightsAssetCustodian is Initializable, UUPSUpgradeable, AccessControlle bool addedCustodian = _custodiansByHolder[msg.sender].add(custodian); if (!addedCustodian) revert GrantCustodyFailed(custodian, msg.sender); + // TODO assoc weight here uint256 demand = _incrementCustody(custodian); // +1 under custody its analog to "demand" emit CustodialGranted(custodian, msg.sender, demand); } @@ -165,6 +166,16 @@ contract RightsAssetCustodian is Initializable, UUPSUpgradeable, AccessControlle uint256 i = 0; uint256 acc = 0; + // TODO el orden de seleccion reemplazarlo por pondersaciones dadas directamente por el creador + // inicialmente se dan en base al orden, pero pueden ser sobreescritas, si quisiera de esta manera + // dar preferencia a algun custodio, de lo contrario la demanda establece dinamicamente el balance + + // factors: + // p = priority (given by creator) + // d = demand (merit) + // b = balance in custodian contract (economic) + // formula p * (d + 1) * (log2(b + 1) + 1) + while (i < n) { // In a categorical probability distribution, nodes with higher weights have a greater chance // of being selected. The random value is checked against the cumulative weight. @@ -272,6 +283,8 @@ contract RightsAssetCustodian is Initializable, UUPSUpgradeable, AccessControlle // assign higher weight to earlier positions (creator priority), but adjust for demand. uint256 d = _holdersUnderCustodian[custodians[i]]; // EffectiveWeight_i = (n - i) * (Demand_i + 1) + // TODO weights[custodian[i]] <- por defecto es 1 hasta que no se establezca port el creador + // w = 1 * d+1 * log2(balance + 1) + 1 uint256 w = (window - i) * (d + 1); // safe