Skip to content

Commit 3f3ab1f

Browse files
committed
tests: invariant balance operator
1 parent ba185ab commit 3f3ab1f

19 files changed

Lines changed: 275 additions & 114 deletions

contracts/access/AccessManager.sol

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -39,12 +39,10 @@ contract AccessManager is Initializable, UUPSUpgradeable, AccessManagerUpgradeab
3939
//
4040
// Group/Council Based Roles:
4141
// - ADMIN_ROLE: Managed by a smart account or council.
42-
// Handles protocol upgrades, pause mechanisms, and operational role assignments.
43-
// - MOD_ROLE: Managed by a smart account or council.
4442
// Approves policy submissions and moderates hook operations.
4543
// - CONTENT_COUNCIL_ROLE: Managed by a smart account or council.
4644
// Participates in governance referenda for content curation.
47-
// - NODE_VALIDATOR_ROLE: Managed by a smart account or council.
45+
// - CUSTODY_COUNCIL_ROLE: Managed by a smart account or council.
4846
// Participates in governance referenda for nodes validation.
4947
//
5048
// Individual/Contract Based Roles:
@@ -58,22 +56,18 @@ contract AccessManager is Initializable, UUPSUpgradeable, AccessManagerUpgradeab
5856
5957
├── ADMIN_ROLE (Smart Account / Council)
6058
│ │
61-
│ ├── MOD_ROLE (Smart Account / Council)
62-
│ │
6359
│ └── OPS_ROLE (Internal Contract Role)
6460
6561
├── CONTENT_COUNCIL_ROLE (Smart Account / Council)
6662
67-
├── NODE_VALIDATOR_ROLE (Smart Account / Council)
63+
├── CUSTODY_COUNCIL_ROLE (Smart Account / Council)
6864
6965
├── VER_ROLE (Individual Trusted Creator)
7066
*/
7167

72-
_setRoleAdmin(C.MOD_ROLE, C.ADMIN_ROLE);
7368
_setRoleAdmin(C.OPS_ROLE, C.ADMIN_ROLE);
74-
7569
_setRoleAdmin(C.VER_ROLE, C.GOV_ROLE);
76-
_setRoleAdmin(C.NODE_VALIDATOR_ROLE, C.GOV_ROLE);
70+
_setRoleAdmin(C.CUSTODY_COUNCIL_ROLE, C.GOV_ROLE);
7771
_setRoleAdmin(C.CONTENT_COUNCIL_ROLE, C.GOV_ROLE);
7872
}
7973

contracts/assets/AssetOwnership.sol

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,6 @@ contract AssetOwnership is
111111
return super.supportsInterface(interfaceId);
112112
}
113113

114-
// TODO: build getURI => from custodian /erc721-metadata
115114
// TODO: Update asset info control version restricted/approved by governance
116115
// TODO: Transfer Ownership Fee: Introducing a fee for transferring
117116
// ownership discourages frequent or unnecessary transfers,
@@ -125,8 +124,7 @@ contract AssetOwnership is
125124
/// @dev Requires approval before an asset can be registered.
126125
/// @param to The address that will own the minted NFT.
127126
/// @param assetId The unique identifier for the asset, serving as the NFT ID.
128-
// TODO pause
129-
function register(address to, uint256 assetId) external onlyApprovedAsset(to, assetId) {
127+
function register(address to, uint256 assetId) external whenNotPaused onlyApprovedAsset(to, assetId) {
130128
_mint(to, assetId);
131129
_enableAsset(assetId);
132130
emit RegisteredAsset(to, assetId);

contracts/core/interfaces/base/ILockLocker.sol

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ pragma solidity 0.8.26;
44
/// @title ILockLocker
55
/// @notice Interface for locking funds in an account.
66
interface ILockLocker {
7-
87
/// @notice Emitted when funds are locked.
98
/// @param initiator Address that initiates the operation.
109
/// @param from Account whose funds are being locked.

contracts/core/interfaces/base/ILockOperator.sol

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,4 @@ import { ILockLocker } from "@synaps3/core/interfaces/base/ILockLocker.sol";
88
/// @title ILockOperator
99
/// @notice Unified interface that composes locker, releaser, and claimer capabilities.
1010
/// @dev Adds a common read method to query the locked balance.
11-
interface ILockOperator is ILockClaimer, ILockLocker, ILockReleaser {
12-
13-
}
11+
interface ILockOperator is ILockClaimer, ILockLocker, ILockReleaser {}

contracts/core/interfaces/financial/ILedgerVault.sol

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,4 @@ import { ILockOperator } from "@synaps3/core/interfaces/base/ILockOperator.sol";
99
/// @title ILedgerVault
1010
/// @notice Interface for managing locked funds and their operations.
1111
/// @dev Extends IBalanceOperator for managing user balances in a vault-like system.
12-
interface ILedgerVault is IBalanceOperator, IAllowanceOperator, ILockOperator {
13-
14-
}
12+
interface ILedgerVault is IBalanceOperator, IAllowanceOperator, ILockOperator {}

contracts/core/primitives/Constants.sol

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,11 @@ library C {
1313

1414
uint64 internal constant ADMIN_ROLE = 0; // alias type(uint64).min AccessManager
1515
uint64 internal constant GOV_ROLE = 1; // governance role
16-
uint64 internal constant MOD_ROLE = 2; // moderator role
16+
uint64 internal constant OPS_ROLE = 2; // operations roles
1717
uint64 internal constant VER_ROLE = 3; // account verified role
18-
uint64 internal constant OPS_ROLE = 4; // operations roles
1918

2019
uint64 internal constant CONTENT_COUNCIL_ROLE = 5; // content validation/curation roles
21-
uint64 internal constant NODE_VALIDATOR_ROLE = 6; // nodes validations roles
20+
uint64 internal constant CUSTODY_COUNCIL_ROLE = 6; // nodes validations roles
2221

2322
bytes32 internal constant REFERENDUM_SUBMIT_TYPEHASH =
2423
keccak256("Submission(uint256 assetId, address initiator, uint256 nonce)");

contracts/core/primitives/Types.sol

Lines changed: 23 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -36,17 +36,30 @@ library T {
3636
}
3737

3838
/// @title Agreement
39-
/// @dev Represents an agreement between multiple parties regarding the distribution and management of asset.
40-
/// @notice This struct captures the total amount involved, net amount after deductions, distribution fees,
41-
/// and the relevant addresses involved in the agreement.
39+
/// @dev Represents an escrow-backed agreement involving payment, distribution or access rights.
40+
/// @notice This struct supports flexible interaction models, including 1:1 and 1:N transfers,
41+
/// purchases, access control, and delegated rights via arbitration.
4242
struct Agreement {
43-
address arbiter; // the designated escrow agent enforcing the agreement.
44-
address currency; // the currency used in transaction
45-
address initiator; // the initiator of the transaction
46-
uint256 total; // the transaction total amount
47-
uint256 fees; // the agreement protocol fees
48-
address[] parties; // the accounts or beneficiaries bounded to the agreement
49-
bytes payload; // any additional data needed during agreement execution
43+
/// @notice The authorized arbiter that enforces the agreement logic (e.g., asset transfer or access).
44+
address arbiter;
45+
/// @notice The currency used for settlement (e.g., ETH or ERC20 address).
46+
address currency;
47+
/// @notice The address that initiated the agreement and provided the funds.
48+
/// @dev In a purchase scenario, the initiator is the asset buyer and final recipient of the asset.
49+
/// In access-based agreements, the initiator may be the funder but not necessarily the consumer.
50+
address initiator;
51+
/// @notice Total amount involved in the agreement, including fees.
52+
uint256 total;
53+
/// @notice Protocol or arbitration fees deducted from the total.
54+
uint256 fees;
55+
/// @notice Protocol internal property to handle total locked amount during agreement lifecycle
56+
uint256 locked;
57+
/// @notice List of accounts involved as beneficiaries or consumers of rights.
58+
/// @dev In purchase scenarios, this may be empty (use `initiator` as beneficiary).
59+
/// In access-sharing modelsor multiple agreement this may contain multiple users granted access to content or rights.
60+
address[] parties;
61+
/// @notice Arbitrary data passed for context, such as assetId, license type, content hash, etc.
62+
bytes payload;
5063
}
5164

5265
/// @title TimeFrame

contracts/core/primitives/upgradeable/LockOperatorUpgradeable.sol

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,12 @@ abstract contract LockOperatorUpgradeable is Initializable, LedgerUpgradeable, I
1414
struct LockOperatorStorage {
1515
/// @dev Holds the relation between approved funds, the currency, and amount
1616
/// @dev Holds the registry of locked funds for accounts.
17-
mapping(address => mapping(address => uint256)) _locked;
17+
mapping(address => mapping(address => uint256)) _locked;
1818
}
1919

2020
/// @dev Storage slot for LockOperatorUpgradeable, calculated using a unique namespace to avoid conflicts.
2121
/// The `LOCK_OPERATOR_SLOT` constant is used to point to the location of the storage.
22-
bytes32 private constant LOCK_OPERATOR_SLOT =
23-
0xece3ff917f3a3127e521e0c3f2f90ff09a3c8199be32f9b40bff79e776960800;
22+
bytes32 private constant LOCK_OPERATOR_SLOT = 0xece3ff917f3a3127e521e0c3f2f90ff09a3c8199be32f9b40bff79e776960800;
2423

2524
/// @dev Initializes the contract and ensures it is upgradeable.
2625
/// Even if the initialization is harmless, this ensures the contract follows upgradeable contract patterns.

contracts/custody/CustodianImpl.sol

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ contract CustodianImpl is
4848
/// @dev Ensures that the provided endpoint is valid and initializes ERC165 and Ownable contracts.
4949
function initialize(string calldata endpoint, address owner) external initializer {
5050
if (bytes(endpoint).length == 0) revert InvalidEndpoint();
51-
51+
5252
__ERC165_init();
5353
__Ownable_init(owner);
5454
_endpoint = endpoint;

contracts/financial/AgreementManager.sol

Lines changed: 66 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
// NatSpec format convention - https://docs.soliditylang.org/en/v0.5.10/natspec-format.html
33
pragma solidity 0.8.26;
44

5-
import { EnumerableSet } from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol";
65
import { Initializable } from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
76
import { UUPSUpgradeable } from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
87
import { AccessControlledUpgradeable } from "@synaps3/core/primitives/upgradeable/AccessControlledUpgradeable.sol";
@@ -13,8 +12,7 @@ import { ITollgate } from "@synaps3/core/interfaces/economics/ITollgate.sol";
1312
import { FinancialOps } from "@synaps3/core/libraries/FinancialOps.sol";
1413
import { FeesOps } from "@synaps3/core/libraries/FeesOps.sol";
1514
import { T } from "@synaps3/core/primitives/Types.sol";
16-
17-
// TODO Doc: Trustless escrow system - modular escrow framework - escrow mechanism (agreement <arbitrer> settlement)
15+
import { C } from "@synaps3/core/primitives/Constants.sol";
1816

1917
/// @title AgreementManager
2018
/// @notice Manages the lifecycle (trustless escrow system) of agreements, including creation and retrieval.
@@ -23,7 +21,6 @@ import { T } from "@synaps3/core/primitives/Types.sol";
2321
contract AgreementManager is Initializable, UUPSUpgradeable, AccessControlledUpgradeable, IAgreementManager {
2422
using FeesOps for uint256;
2523
using FinancialOps for address;
26-
using EnumerableSet for EnumerableSet.UintSet;
2724

2825
/// KIM: any initialization here is ephemeral and not included in bytecode..
2926
/// so the code within a logic contract’s constructor or global declaration
@@ -38,6 +35,10 @@ contract AgreementManager is Initializable, UUPSUpgradeable, AccessControlledUpg
3835
ILedgerVault public immutable LEDGER_VAULT;
3936
//slither-disable-end naming-convention
4037

38+
uint256 constant MAX_EXCESS = 13; // 1..13=91%, 14=>105%
39+
/// @notice Maximum allowed number of parties per agreement.
40+
/// @dev Can be updated by admin to adapt system limits.
41+
uint256 private _maxParties;
4142
/// @dev Holds a bounded key expressing the agreement between the parts.
4243
mapping(uint256 => T.Agreement) private _agreementsByProof;
4344

@@ -54,10 +55,13 @@ contract AgreementManager is Initializable, UUPSUpgradeable, AccessControlledUpg
5455
/// @notice Error thrown when a currency is not supported by the specified target.
5556
/// @param target The address or context for which the currency is unsupported.
5657
/// @param currency The address of the unsupported currency.
57-
error UnsupportedAgreementCurrency(address target, address currency);
58+
error UnsupportedAgreementTarget(address target, address currency);
59+
60+
/// @notice Error thrown when trying to set an invalid maximum number of parties.
61+
error InvalidMaxParties(uint256 value);
5862

59-
/// @notice Error thrown when an agreement includes no parties.
60-
error NoPartiesInAgreement();
63+
/// @notice Error thrown when the number of parties exceeds the protocol limit.
64+
error ExceedsMaxParties();
6165

6266
/// @notice Ensures that the specified currency is supported for the given target.
6367
/// @dev This modifier verifies if the `currency` is accepted under the context of `target`.
@@ -66,7 +70,7 @@ contract AgreementManager is Initializable, UUPSUpgradeable, AccessControlledUpg
6670
/// @param currency The address of the currency being checked.
6771
modifier onlySupportedCurrency(address target, address currency) {
6872
bool isCurrencySupported = TOLLGATE.isSupportedCurrency(target, currency);
69-
if (!isCurrencySupported) revert UnsupportedAgreementCurrency(target, currency);
73+
if (!isCurrencySupported) revert UnsupportedAgreementTarget(target, currency);
7074
_;
7175
}
7276

@@ -84,6 +88,19 @@ contract AgreementManager is Initializable, UUPSUpgradeable, AccessControlledUpg
8488
function initialize(address accessManager) public initializer {
8589
__UUPSUpgradeable_init();
8690
__AccessControlled_init(accessManager);
91+
_maxParties = 5;
92+
}
93+
94+
/// @notice Updates the maximum number of allowed parties.
95+
/// @param newMax The new maximum number of parties.
96+
function setMaxParties(uint256 newMax) external onlyAdmin {
97+
if (newMax == 0) revert InvalidMaxParties(newMax);
98+
_maxParties = newMax;
99+
}
100+
101+
/// @notice Retrieves the current max number of parties.
102+
function maxParties() external view returns (uint256) {
103+
return _maxParties;
87104
}
88105

89106
/// @notice Creates and stores a new agreement.
@@ -100,12 +117,13 @@ contract AgreementManager is Initializable, UUPSUpgradeable, AccessControlledUpg
100117
bytes calldata payload
101118
) external onlySupportedCurrency(arbiter, currency) returns (uint256) {
102119
// IMPORTANT: The process of distributing funds to accounts should be handled within the settlement logic.
103-
uint256 confirmed = LEDGER_VAULT.lock(msg.sender, amount, currency);
104-
T.Agreement memory agreement = previewAgreement(confirmed, currency, arbiter, parties, payload);
120+
T.Agreement memory agreement = previewAgreement(amount, currency, arbiter, parties, payload);
121+
uint256 confirmed = LEDGER_VAULT.lock(msg.sender, agreement.locked, currency);
122+
105123
// only the initiator can operate with this agreement proof, or transfer the proof to the other party..
106124
// each agreement is unique and immutable, ensuring that it cannot be modified or reconstructed.
107125
uint256 proof = _createAndStoreProof(agreement);
108-
emit AgreementCreated(msg.sender, proof, amount, currency);
126+
emit AgreementCreated(msg.sender, proof, confirmed, currency);
109127
return proof;
110128
}
111129

@@ -128,14 +146,6 @@ contract AgreementManager is Initializable, UUPSUpgradeable, AccessControlledUpg
128146
address[] calldata parties,
129147
bytes calldata payload
130148
) public view onlySupportedCurrency(arbiter, currency) returns (T.Agreement memory) {
131-
if (parties.length == 0) {
132-
revert NoPartiesInAgreement();
133-
}
134-
135-
// TODO Even if we are covered by gas fees, during execution a good way to avoid abuse
136-
// is penalize parties after N length eg. The max parties allowed is 5, any extra
137-
// parties are charged with a extra * fee. Denial of Service risk
138-
139149
// IMPORTANT:
140150
// Agreements transport value and represent a defined commitment between parties.
141151
// Think of an agreement as similar to a bonus, gift card, prepaid card, or check:
@@ -148,7 +158,13 @@ contract AgreementManager is Initializable, UUPSUpgradeable, AccessControlledUpg
148158
// By locking in fees during agreement creation, the protocol avoids scenarios
149159
// where fee structures change (favorably or unfavorably) after creation,
150160
// which could lead to abuse or exploitation.
151-
uint256 deductions = _calcFees(amount, arbiter, currency);
161+
uint256 baseFees = _calcFees(amount, arbiter, currency);
162+
// Even if we are covered by gas fees, during execution a good way to avoid abuse
163+
// is penalize parties after N length eg. The max parties allowed is 5, any extra
164+
// parties are charged with a extra * fee. Denial of Service risk mitigation..
165+
uint256 penalization = _calculatePenalization(parties.length, amount);
166+
uint256 totalToLock = amount + penalization;
167+
152168
// This design ensures fairness and transparency by preventing any future
153169
// adjustments to fees or protocol conditions from affecting the terms of this agreement.
154170
return
@@ -157,8 +173,9 @@ contract AgreementManager is Initializable, UUPSUpgradeable, AccessControlledUpg
157173
currency: currency, // the currency used in transaction
158174
initiator: msg.sender, // the tx initiator
159175
total: amount, // the transaction amount
160-
fees: deductions, // the protocol fees of the agreement
161-
parties: parties, // the accounts related to agreement
176+
fees: baseFees, // the protocol fees of the agreement
177+
locked: totalToLock, // the total to lock, may contain penalization
178+
parties: parties, // the additional accounts related to agreement 1:N agreement
162179
payload: payload // any additional data needed during agreement execution
163180
});
164181
}
@@ -177,6 +194,33 @@ contract AgreementManager is Initializable, UUPSUpgradeable, AccessControlledUpg
177194
return proof;
178195
}
179196

197+
/// @dev Calculates the penalization based on parties len and total amount
198+
function _calculatePenalization(uint256 partiesLen, uint256 amount) private view returns (uint256 penalization) {
199+
uint256 hardCap = _maxParties + MAX_EXCESS;
200+
if (partiesLen > hardCap) revert ExceedsMaxParties();
201+
202+
// soft cap validation, economic penalization
203+
if (partiesLen > _maxParties) {
204+
uint256 excess = partiesLen - _maxParties;
205+
uint256 multiplierBps = _penaltyBps(excess);
206+
penalization = amount.perOf(multiplierBps);
207+
}
208+
}
209+
210+
/// @dev Computes the penalty BPS as a arithmetic succession.
211+
/// 1st extra = 1%, 2nd extra = +2%, 3rd extra = +3%, ...
212+
/// Formula: (N * (N + 1) / 2) * 100
213+
/// @param excess Number of parties beyond the allowed max.
214+
/// @return penaltyBps Total penalty in basis points.
215+
function _penaltyBps(uint256 excess) private pure returns (uint256 penaltyBps) {
216+
if (excess == 0) return 0;
217+
// Formula for the sum of an arithmetic succession: S = n(n + 1) / 2
218+
// Example: excess = 3 -> 1 + 2 + 3 = 6 %
219+
unchecked {
220+
penaltyBps = ((excess * (excess + 1)) / 2) * 100;
221+
}
222+
}
223+
180224
/// @notice Calculates the fee based on the provided total amount, agent, and currency.
181225
/// @dev Reverts if the currency is not supported by the Tollgate or if no fee scheme is defined for the agent.
182226
/// @param total The total amount from which the fee will be calculated.
@@ -185,7 +229,6 @@ contract AgreementManager is Initializable, UUPSUpgradeable, AccessControlledUpg
185229
/// @return The calculated fee amount based on the applicable fee scheme.
186230
function _calcFees(uint256 total, address target, address currency) private view returns (uint256) {
187231
// !IMPORTANT if fees manager does not support the currency or the target, will revert..
188-
// TODO avoid revert, just check if the currency is supported and return 0
189232
(uint256 fees, T.Scheme scheme) = TOLLGATE.getFees(target, currency);
190233
if (scheme == T.Scheme.BPS) return total.perOf(fees); // bps calc
191234
if (scheme == T.Scheme.NOMINAL) return total.perOf(fees.calcBps()); // nominal to bps

0 commit comments

Comments
 (0)