From d58ab1a3818acff077b81f239e41bd7aed4ca0e4 Mon Sep 17 00:00:00 2001 From: Sikkra <159844544+Sikkra@users.noreply.github.com> Date: Tue, 19 May 2026 18:23:32 -0500 Subject: [PATCH] feat: accumulate and withdraw fees --- src/IntuitionFeeProxy.sol | 969 +++++++++++++++++---------------- test/IntuitionFeeProxy.test.ts | 847 ++++++++++++++-------------- 2 files changed, 944 insertions(+), 872 deletions(-) diff --git a/src/IntuitionFeeProxy.sol b/src/IntuitionFeeProxy.sol index 35993d7..28b2b7f 100644 --- a/src/IntuitionFeeProxy.sol +++ b/src/IntuitionFeeProxy.sol @@ -1,190 +1,197 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.21; - -import {IEthMultiVault} from "./interfaces/IEthMultiVault.sol"; -import {Errors} from "./libraries/Errors.sol"; - -/// @title IntuitionFeeProxy -/// @notice Proxy contract for Intuition MultiVault with fee collection -/// @dev Collects fees on deposits and forwards them to a configurable recipient -contract IntuitionFeeProxy { - // ============ Constants ============ - - /// @notice Fee denominator for percentage calculations (10000 = 100%) - uint256 public constant FEE_DENOMINATOR = 10000; - - /// @notice Maximum allowed fee percentage (100%) - uint256 public constant MAX_FEE_PERCENTAGE = 10000; - - // ============ Immutables ============ - - /// @notice Reference to the Intuition MultiVault contract - IEthMultiVault public immutable ethMultiVault; - - // ============ State Variables ============ - - /// @notice Address receiving collected fees - address public feeRecipient; - - /// @notice Fixed fee per deposit operation in wei - /// @dev Default: 0.1 TRUST = 10^17 wei - uint256 public depositFixedFee; - - /// @notice Percentage fee for deposits (base 10000) - /// @dev Default: 500 = 5% - uint256 public depositPercentageFee; - - /// @notice Mapping of whitelisted admin addresses - mapping(address => bool) public whitelistedAdmins; - - // ============ Events ============ - - /// @notice Emitted when fee recipient is updated - event FeeRecipientUpdated(address indexed oldRecipient, address indexed newRecipient); - - /// @notice Emitted when deposit fixed fee is updated - event DepositFixedFeeUpdated(uint256 oldFee, uint256 newFee); - - /// @notice Emitted when deposit percentage fee is updated - event DepositPercentageFeeUpdated(uint256 oldFee, uint256 newFee); - - /// @notice Emitted when admin whitelist status is updated - event AdminWhitelistUpdated(address indexed admin, bool status); - - /// @notice Emitted when fees are collected +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.21; + +import {IEthMultiVault} from "./interfaces/IEthMultiVault.sol"; +import {Errors} from "./libraries/Errors.sol"; + +/// @title IntuitionFeeProxy +/// @notice Proxy contract for Intuition MultiVault with fee collection +/// @dev Collects fees on deposits and forwards them to a configurable recipient +contract IntuitionFeeProxy { + // ============ Constants ============ + + /// @notice Fee denominator for percentage calculations (10000 = 100%) + uint256 public constant FEE_DENOMINATOR = 10000; + + /// @notice Maximum allowed fee percentage (100%) + uint256 public constant MAX_FEE_PERCENTAGE = 10000; + + // ============ Immutables ============ + + /// @notice Reference to the Intuition MultiVault contract + IEthMultiVault public immutable ethMultiVault; + + // ============ State Variables ============ + + /// @notice Address receiving collected fees + address public feeRecipient; + + /// @notice Fixed fee per deposit operation in wei + /// @dev Default: 0.1 TRUST = 10^17 wei + uint256 public depositFixedFee; + + /// @notice Percentage fee for deposits (base 10000) + /// @dev Default: 500 = 5% + uint256 public depositPercentageFee; + + /// @notice Mapping of whitelisted admin addresses + mapping(address => bool) public whitelistedAdmins; + + // ============ Events ============ + + /// @notice Emitted when fee recipient is updated + event FeeRecipientUpdated(address indexed oldRecipient, address indexed newRecipient); + + /// @notice Emitted when deposit fixed fee is updated + event DepositFixedFeeUpdated(uint256 oldFee, uint256 newFee); + + /// @notice Emitted when deposit percentage fee is updated + event DepositPercentageFeeUpdated(uint256 oldFee, uint256 newFee); + + /// @notice Emitted when admin whitelist status is updated + event AdminWhitelistUpdated(address indexed admin, bool status); + + /// @notice Emitted when fees are collected event FeesCollected( address indexed user, uint256 amount, string operation ); - /// @notice Emitted when a transaction is forwarded to MultiVault (debug) - event TransactionForwarded( - string operation, - address indexed user, - uint256 fee, - uint256 multiVaultValue, - uint256 totalReceived - ); - - /// @notice Emitted when MultiVault returns results (debug) - event MultiVaultSuccess( - string operation, - uint256 resultCount + /// @notice Emitted when accumulated fees are withdrawn + event FeesWithdrawn( + address indexed caller, + address indexed recipient, + uint256 amount ); - // ============ Modifiers ============ - - /// @notice Restricts function to whitelisted admins only - modifier onlyWhitelistedAdmin() { - if (!whitelistedAdmins[msg.sender]) { - revert Errors.IntuitionFeeProxy_NotWhitelistedAdmin(); - } - _; - } - - // ============ Constructor ============ - - /// @notice Initializes the IntuitionFeeProxy contract - /// @param _ethMultiVault Address of the Intuition MultiVault contract - /// @param _feeRecipient Address to receive collected fees - /// @param _depositFixedFee Initial fixed fee per deposit (in wei) - /// @param _depositPercentageFee Initial percentage fee for deposits (base 10000) - /// @param _initialAdmins Array of initial admin addresses to whitelist - constructor( - address _ethMultiVault, - address _feeRecipient, - uint256 _depositFixedFee, - uint256 _depositPercentageFee, - address[] memory _initialAdmins - ) { - if (_ethMultiVault == address(0)) { - revert Errors.IntuitionFeeProxy_InvalidMultiVaultAddress(); - } - if (_feeRecipient == address(0)) { - revert Errors.IntuitionFeeProxy_InvalidMultisigAddress(); - } - if (_depositPercentageFee > MAX_FEE_PERCENTAGE) { - revert Errors.IntuitionFeeProxy_FeePercentageTooHigh(); - } - - ethMultiVault = IEthMultiVault(_ethMultiVault); - feeRecipient = _feeRecipient; - depositFixedFee = _depositFixedFee; - depositPercentageFee = _depositPercentageFee; - - // Whitelist initial admins - for (uint256 i = 0; i < _initialAdmins.length; i++) { - if (_initialAdmins[i] != address(0)) { - whitelistedAdmins[_initialAdmins[i]] = true; - emit AdminWhitelistUpdated(_initialAdmins[i], true); - } - } - } - - // ============ Fee Calculation Functions ============ - - /// @notice Calculate fee for deposits - /// @param depositCount Number of deposits (non-zero assets) - /// @param totalDeposit Total amount being deposited - /// @return Total fee (fixed per deposit + percentage of total) - function calculateDepositFee(uint256 depositCount, uint256 totalDeposit) public view returns (uint256) { - uint256 fixedFee = depositFixedFee * depositCount; - uint256 percentageFee = (totalDeposit * depositPercentageFee) / FEE_DENOMINATOR; - return fixedFee + percentageFee; - } - - /// @notice Helper for frontend - get total cost for a single deposit - /// @param depositAmount Amount user wants to deposit - /// @return Total amount user needs to send (deposit + fees) - function getTotalDepositCost(uint256 depositAmount) external view returns (uint256) { - return depositAmount + calculateDepositFee(1, depositAmount); - } - - /// @notice Helper for frontend - get total cost for createAtoms/createTriples - /// @param depositCount Number of non-zero deposits - /// @param totalDeposit Sum of all deposit amounts - /// @param multiVaultCost Total cost required by MultiVault (atomCost/tripleCost * count + totalDeposit) - /// @return Total amount user needs to send - function getTotalCreationCost(uint256 depositCount, uint256 totalDeposit, uint256 multiVaultCost) external view returns (uint256) { - return multiVaultCost + calculateDepositFee(depositCount, totalDeposit); - } - - // ============ Admin Functions ============ - - /// @notice Update the deposit fixed fee - /// @param newFee New fee in wei - function setDepositFixedFee(uint256 newFee) external onlyWhitelistedAdmin { - uint256 oldFee = depositFixedFee; - depositFixedFee = newFee; - emit DepositFixedFeeUpdated(oldFee, newFee); - } - - /// @notice Update the deposit percentage fee - /// @param newFee New fee (base 10000, e.g., 500 = 5%) - function setDepositPercentageFee(uint256 newFee) external onlyWhitelistedAdmin { - if (newFee > MAX_FEE_PERCENTAGE) { - revert Errors.IntuitionFeeProxy_FeePercentageTooHigh(); - } - uint256 oldFee = depositPercentageFee; - depositPercentageFee = newFee; - emit DepositPercentageFeeUpdated(oldFee, newFee); - } - - /// @notice Update the fee recipient address - /// @param newRecipient New recipient address - function setFeeRecipient(address newRecipient) external onlyWhitelistedAdmin { - if (newRecipient == address(0)) { - revert Errors.IntuitionFeeProxy_ZeroAddress(); - } - address oldRecipient = feeRecipient; - feeRecipient = newRecipient; - emit FeeRecipientUpdated(oldRecipient, newRecipient); - } - - /// @notice Update admin whitelist status - /// @param admin Address to update - /// @param status New whitelist status + /// @notice Emitted when a transaction is forwarded to MultiVault (debug) + event TransactionForwarded( + string operation, + address indexed user, + uint256 fee, + uint256 multiVaultValue, + uint256 totalReceived + ); + + /// @notice Emitted when MultiVault returns results (debug) + event MultiVaultSuccess( + string operation, + uint256 resultCount + ); + + // ============ Modifiers ============ + + /// @notice Restricts function to whitelisted admins only + modifier onlyWhitelistedAdmin() { + if (!whitelistedAdmins[msg.sender]) { + revert Errors.IntuitionFeeProxy_NotWhitelistedAdmin(); + } + _; + } + + // ============ Constructor ============ + + /// @notice Initializes the IntuitionFeeProxy contract + /// @param _ethMultiVault Address of the Intuition MultiVault contract + /// @param _feeRecipient Address to receive collected fees + /// @param _depositFixedFee Initial fixed fee per deposit (in wei) + /// @param _depositPercentageFee Initial percentage fee for deposits (base 10000) + /// @param _initialAdmins Array of initial admin addresses to whitelist + constructor( + address _ethMultiVault, + address _feeRecipient, + uint256 _depositFixedFee, + uint256 _depositPercentageFee, + address[] memory _initialAdmins + ) { + if (_ethMultiVault == address(0)) { + revert Errors.IntuitionFeeProxy_InvalidMultiVaultAddress(); + } + if (_feeRecipient == address(0)) { + revert Errors.IntuitionFeeProxy_InvalidMultisigAddress(); + } + if (_depositPercentageFee > MAX_FEE_PERCENTAGE) { + revert Errors.IntuitionFeeProxy_FeePercentageTooHigh(); + } + + ethMultiVault = IEthMultiVault(_ethMultiVault); + feeRecipient = _feeRecipient; + depositFixedFee = _depositFixedFee; + depositPercentageFee = _depositPercentageFee; + + // Whitelist initial admins + for (uint256 i = 0; i < _initialAdmins.length; i++) { + if (_initialAdmins[i] != address(0)) { + whitelistedAdmins[_initialAdmins[i]] = true; + emit AdminWhitelistUpdated(_initialAdmins[i], true); + } + } + } + + // ============ Fee Calculation Functions ============ + + /// @notice Calculate fee for deposits + /// @param depositCount Number of deposits (non-zero assets) + /// @param totalDeposit Total amount being deposited + /// @return Total fee (fixed per deposit + percentage of total) + function calculateDepositFee(uint256 depositCount, uint256 totalDeposit) public view returns (uint256) { + uint256 fixedFee = depositFixedFee * depositCount; + uint256 percentageFee = (totalDeposit * depositPercentageFee) / FEE_DENOMINATOR; + return fixedFee + percentageFee; + } + + /// @notice Helper for frontend - get total cost for a single deposit + /// @param depositAmount Amount user wants to deposit + /// @return Total amount user needs to send (deposit + fees) + function getTotalDepositCost(uint256 depositAmount) external view returns (uint256) { + return depositAmount + calculateDepositFee(1, depositAmount); + } + + /// @notice Helper for frontend - get total cost for createAtoms/createTriples + /// @param depositCount Number of non-zero deposits + /// @param totalDeposit Sum of all deposit amounts + /// @param multiVaultCost Total cost required by MultiVault (atomCost/tripleCost * count + totalDeposit) + /// @return Total amount user needs to send + function getTotalCreationCost(uint256 depositCount, uint256 totalDeposit, uint256 multiVaultCost) external view returns (uint256) { + return multiVaultCost + calculateDepositFee(depositCount, totalDeposit); + } + + // ============ Admin Functions ============ + + /// @notice Update the deposit fixed fee + /// @param newFee New fee in wei + function setDepositFixedFee(uint256 newFee) external onlyWhitelistedAdmin { + uint256 oldFee = depositFixedFee; + depositFixedFee = newFee; + emit DepositFixedFeeUpdated(oldFee, newFee); + } + + /// @notice Update the deposit percentage fee + /// @param newFee New fee (base 10000, e.g., 500 = 5%) + function setDepositPercentageFee(uint256 newFee) external onlyWhitelistedAdmin { + if (newFee > MAX_FEE_PERCENTAGE) { + revert Errors.IntuitionFeeProxy_FeePercentageTooHigh(); + } + uint256 oldFee = depositPercentageFee; + depositPercentageFee = newFee; + emit DepositPercentageFeeUpdated(oldFee, newFee); + } + + /// @notice Update the fee recipient address + /// @param newRecipient New recipient address + function setFeeRecipient(address newRecipient) external onlyWhitelistedAdmin { + if (newRecipient == address(0)) { + revert Errors.IntuitionFeeProxy_ZeroAddress(); + } + address oldRecipient = feeRecipient; + feeRecipient = newRecipient; + emit FeeRecipientUpdated(oldRecipient, newRecipient); + } + + /// @notice Update admin whitelist status + /// @param admin Address to update + /// @param status New whitelist status function setWhitelistedAdmin(address admin, bool status) external onlyWhitelistedAdmin { if (admin == address(0)) { revert Errors.IntuitionFeeProxy_ZeroAddress(); @@ -193,316 +200,324 @@ contract IntuitionFeeProxy { emit AdminWhitelistUpdated(admin, status); } - // ============ Proxy Functions (Payable) ============ - - /// @notice Create atoms with fee collection and deposit to receiver - /// @dev Receiver must have approved this proxy on MultiVault for DEPOSIT - /// @param receiver Address to receive the shares (the real user) - /// @param data Array of atom data (IPFS URIs as bytes) - /// @param assets Array of deposit amounts for each atom (on top of creation cost) - /// @param curveId Bonding curve ID for deposits (1 = linear, 2 = progressive) - /// @return atomIds Array of created atom IDs - function createAtoms( - address receiver, - bytes[] calldata data, - uint256[] calldata assets, - uint256 curveId - ) external payable returns (bytes32[] memory atomIds) { - if (data.length != assets.length) { - revert Errors.IntuitionFeeProxy_WrongArrayLengths(); + /// @notice Withdraw accumulated fees from the proxy balance + /// @dev Callable by whitelisted admins or the current fee recipient + /// @param recipient Address to receive withdrawn fees + /// @param amount Amount of fees to withdraw + function withdrawFees(address payable recipient, uint256 amount) external { + if (!whitelistedAdmins[msg.sender] && msg.sender != feeRecipient) { + revert Errors.IntuitionFeeProxy_NotWhitelistedAdmin(); } - - uint256 count = data.length; - uint256 atomCost = ethMultiVault.getAtomCost(); - uint256 totalDeposit = _sumArray(assets); - - // Calculate fee: fixed fee per deposit + percentage of total deposits - uint256 depositCount = _countNonZero(assets); - uint256 fee = calculateDepositFee(depositCount, totalDeposit); - - uint256 multiVaultCost = (atomCost * count) + totalDeposit; - uint256 totalRequired = fee + multiVaultCost; - - if (msg.value < totalRequired) { - revert Errors.IntuitionFeeProxy_InsufficientValue(); + if (recipient == address(0)) { + revert Errors.IntuitionFeeProxy_ZeroAddress(); } - - _transferFee(fee); - emit FeesCollected(msg.sender, fee, "createAtoms"); - emit TransactionForwarded("createAtoms", msg.sender, fee, multiVaultCost, msg.value); - - // Create atoms with minimum cost (just atomCost per atom) - uint256[] memory minAssets = new uint256[](count); - for (uint256 i = 0; i < count; i++) { - minAssets[i] = atomCost; + if (amount > address(this).balance) { + revert Errors.IntuitionFeeProxy_InsufficientValue(); } - atomIds = ethMultiVault.createAtoms{value: atomCost * count}(data, minAssets); - // Deposit remaining assets to receiver for each atom - for (uint256 i = 0; i < count; i++) { - if (assets[i] > 0) { - ethMultiVault.deposit{value: assets[i]}( - receiver, - atomIds[i], - curveId, - 0 // minShares - ); - } + (bool success, ) = recipient.call{value: amount}(""); + if (!success) { + revert Errors.IntuitionFeeProxy_TransferFailed(); } - emit MultiVaultSuccess("createAtoms", count); - return atomIds; + emit FeesWithdrawn(msg.sender, recipient, amount); } - /// @notice Create triples with fee collection and deposit to receiver - /// @dev Receiver must have approved this proxy on MultiVault for DEPOSIT - /// @param receiver Address to receive the shares (the real user) - /// @param subjectIds Array of subject atom IDs - /// @param predicateIds Array of predicate atom IDs - /// @param objectIds Array of object atom IDs - /// @param assets Array of deposit amounts for each triple (on top of creation cost) - /// @param curveId Bonding curve ID for deposits (1 = linear, 2 = progressive) - /// @return tripleIds Array of created triple IDs - function createTriples( - address receiver, - bytes32[] calldata subjectIds, - bytes32[] calldata predicateIds, - bytes32[] calldata objectIds, - uint256[] calldata assets, - uint256 curveId - ) external payable returns (bytes32[] memory tripleIds) { - if (subjectIds.length != predicateIds.length || - predicateIds.length != objectIds.length || - objectIds.length != assets.length) { - revert Errors.IntuitionFeeProxy_WrongArrayLengths(); - } - - uint256 count = subjectIds.length; - uint256 tripleCost = ethMultiVault.getTripleCost(); - uint256 totalDeposit = _sumArray(assets); - - // Calculate fee: fixed fee per deposit + percentage of total deposits - uint256 depositCount = _countNonZero(assets); - uint256 fee = calculateDepositFee(depositCount, totalDeposit); - - uint256 multiVaultCost = (tripleCost * count) + totalDeposit; - uint256 totalRequired = fee + multiVaultCost; - - if (msg.value < totalRequired) { - revert Errors.IntuitionFeeProxy_InsufficientValue(); - } - - _transferFee(fee); + // ============ Proxy Functions (Payable) ============ + + /// @notice Create atoms with fee collection and deposit to receiver + /// @dev Receiver must have approved this proxy on MultiVault for DEPOSIT + /// @param receiver Address to receive the shares (the real user) + /// @param data Array of atom data (IPFS URIs as bytes) + /// @param assets Array of deposit amounts for each atom (on top of creation cost) + /// @param curveId Bonding curve ID for deposits (1 = linear, 2 = progressive) + /// @return atomIds Array of created atom IDs + function createAtoms( + address receiver, + bytes[] calldata data, + uint256[] calldata assets, + uint256 curveId + ) external payable returns (bytes32[] memory atomIds) { + if (data.length != assets.length) { + revert Errors.IntuitionFeeProxy_WrongArrayLengths(); + } + + uint256 count = data.length; + uint256 atomCost = ethMultiVault.getAtomCost(); + uint256 totalDeposit = _sumArray(assets); + + // Calculate fee: fixed fee per deposit + percentage of total deposits + uint256 depositCount = _countNonZero(assets); + uint256 fee = calculateDepositFee(depositCount, totalDeposit); + + uint256 multiVaultCost = (atomCost * count) + totalDeposit; + uint256 totalRequired = fee + multiVaultCost; + + if (msg.value < totalRequired) { + revert Errors.IntuitionFeeProxy_InsufficientValue(); + } + + emit FeesCollected(msg.sender, fee, "createAtoms"); + emit TransactionForwarded("createAtoms", msg.sender, fee, multiVaultCost, msg.value); + + // Create atoms with minimum cost (just atomCost per atom) + uint256[] memory minAssets = new uint256[](count); + for (uint256 i = 0; i < count; i++) { + minAssets[i] = atomCost; + } + atomIds = ethMultiVault.createAtoms{value: atomCost * count}(data, minAssets); + + // Deposit remaining assets to receiver for each atom + for (uint256 i = 0; i < count; i++) { + if (assets[i] > 0) { + ethMultiVault.deposit{value: assets[i]}( + receiver, + atomIds[i], + curveId, + 0 // minShares + ); + } + } + + emit MultiVaultSuccess("createAtoms", count); + return atomIds; + } + + /// @notice Create triples with fee collection and deposit to receiver + /// @dev Receiver must have approved this proxy on MultiVault for DEPOSIT + /// @param receiver Address to receive the shares (the real user) + /// @param subjectIds Array of subject atom IDs + /// @param predicateIds Array of predicate atom IDs + /// @param objectIds Array of object atom IDs + /// @param assets Array of deposit amounts for each triple (on top of creation cost) + /// @param curveId Bonding curve ID for deposits (1 = linear, 2 = progressive) + /// @return tripleIds Array of created triple IDs + function createTriples( + address receiver, + bytes32[] calldata subjectIds, + bytes32[] calldata predicateIds, + bytes32[] calldata objectIds, + uint256[] calldata assets, + uint256 curveId + ) external payable returns (bytes32[] memory tripleIds) { + if (subjectIds.length != predicateIds.length || + predicateIds.length != objectIds.length || + objectIds.length != assets.length) { + revert Errors.IntuitionFeeProxy_WrongArrayLengths(); + } + + uint256 count = subjectIds.length; + uint256 tripleCost = ethMultiVault.getTripleCost(); + uint256 totalDeposit = _sumArray(assets); + + // Calculate fee: fixed fee per deposit + percentage of total deposits + uint256 depositCount = _countNonZero(assets); + uint256 fee = calculateDepositFee(depositCount, totalDeposit); + + uint256 multiVaultCost = (tripleCost * count) + totalDeposit; + uint256 totalRequired = fee + multiVaultCost; + + if (msg.value < totalRequired) { + revert Errors.IntuitionFeeProxy_InsufficientValue(); + } + emit FeesCollected(msg.sender, fee, "createTriples"); emit TransactionForwarded("createTriples", msg.sender, fee, multiVaultCost, msg.value); - - // Create triples with minimum cost (just tripleCost per triple) - uint256[] memory minAssets = new uint256[](count); - for (uint256 i = 0; i < count; i++) { - minAssets[i] = tripleCost; - } - tripleIds = ethMultiVault.createTriples{value: tripleCost * count}( - subjectIds, - predicateIds, - objectIds, - minAssets - ); - - // Deposit remaining assets to receiver for each triple - for (uint256 i = 0; i < count; i++) { - if (assets[i] > 0) { - ethMultiVault.deposit{value: assets[i]}( - receiver, - tripleIds[i], - curveId, - 0 // minShares - ); - } - } - - emit MultiVaultSuccess("createTriples", count); - return tripleIds; - } - - /// @notice Deposit with fee collection - SAME SIGNATURE AS MULTIVAULT - /// @dev Fee is calculated from msg.value using inverse formula - /// @param receiver Address to receive shares - /// @param termId Vault ID (atom or triple) - /// @param curveId Bonding curve ID - /// @param minShares Minimum shares expected - /// @return shares Amount of shares minted - function deposit( - address receiver, - bytes32 termId, - uint256 curveId, - uint256 minShares - ) external payable returns (uint256 shares) { - // Must send more than just the fixed fee - if (msg.value <= depositFixedFee) { - revert Errors.IntuitionFeeProxy_InsufficientValue(); - } - - // Inverse calculation: how much to send to MultiVault - // Formula: multiVaultAmount = (msg.value - fixedFee) * 10000 / (10000 + percentage) - uint256 multiVaultAmount = (msg.value - depositFixedFee) * FEE_DENOMINATOR - / (FEE_DENOMINATOR + depositPercentageFee); - uint256 fee = msg.value - multiVaultAmount; - - _transferFee(fee); + + // Create triples with minimum cost (just tripleCost per triple) + uint256[] memory minAssets = new uint256[](count); + for (uint256 i = 0; i < count; i++) { + minAssets[i] = tripleCost; + } + tripleIds = ethMultiVault.createTriples{value: tripleCost * count}( + subjectIds, + predicateIds, + objectIds, + minAssets + ); + + // Deposit remaining assets to receiver for each triple + for (uint256 i = 0; i < count; i++) { + if (assets[i] > 0) { + ethMultiVault.deposit{value: assets[i]}( + receiver, + tripleIds[i], + curveId, + 0 // minShares + ); + } + } + + emit MultiVaultSuccess("createTriples", count); + return tripleIds; + } + + /// @notice Deposit with fee collection - SAME SIGNATURE AS MULTIVAULT + /// @dev Fee is calculated from msg.value using inverse formula + /// @param receiver Address to receive shares + /// @param termId Vault ID (atom or triple) + /// @param curveId Bonding curve ID + /// @param minShares Minimum shares expected + /// @return shares Amount of shares minted + function deposit( + address receiver, + bytes32 termId, + uint256 curveId, + uint256 minShares + ) external payable returns (uint256 shares) { + // Must send more than just the fixed fee + if (msg.value <= depositFixedFee) { + revert Errors.IntuitionFeeProxy_InsufficientValue(); + } + + // Inverse calculation: how much to send to MultiVault + // Formula: multiVaultAmount = (msg.value - fixedFee) * 10000 / (10000 + percentage) + uint256 multiVaultAmount = (msg.value - depositFixedFee) * FEE_DENOMINATOR + / (FEE_DENOMINATOR + depositPercentageFee); + uint256 fee = msg.value - multiVaultAmount; + emit FeesCollected(msg.sender, fee, "deposit"); emit TransactionForwarded("deposit", msg.sender, fee, multiVaultAmount, msg.value); - - uint256 result = ethMultiVault.deposit{value: multiVaultAmount}( - receiver, - termId, - curveId, - minShares - ); - emit MultiVaultSuccess("deposit", 1); - return result; - } - - /// @notice Calculate how much MultiVault will receive for a given msg.value - /// @param msgValue The value that will be sent with the transaction - /// @return Amount that will be forwarded to MultiVault - function getMultiVaultAmountFromValue(uint256 msgValue) public view returns (uint256) { - if (msgValue <= depositFixedFee) return 0; - return (msgValue - depositFixedFee) * FEE_DENOMINATOR / (FEE_DENOMINATOR + depositPercentageFee); - } - - /// @notice Batch deposit with fee collection - /// @param receiver Address to receive shares - /// @param termIds Array of vault IDs - /// @param curveIds Array of curve IDs - /// @param assets Array of deposit amounts - /// @param minShares Array of minimum shares expected - /// @return shares Array of shares minted - function depositBatch( - address receiver, - bytes32[] calldata termIds, - uint256[] calldata curveIds, - uint256[] calldata assets, - uint256[] calldata minShares - ) external payable returns (uint256[] memory shares) { - if (termIds.length != curveIds.length || - curveIds.length != assets.length || - assets.length != minShares.length) { - revert Errors.IntuitionFeeProxy_WrongArrayLengths(); - } - - uint256 totalDeposit = _sumArray(assets); - // Fee: fixed fee per deposit + percentage of total - uint256 fee = calculateDepositFee(termIds.length, totalDeposit); - uint256 totalRequired = totalDeposit + fee; - - if (msg.value < totalRequired) { - revert Errors.IntuitionFeeProxy_InsufficientValue(); - } - - _transferFee(fee); + + uint256 result = ethMultiVault.deposit{value: multiVaultAmount}( + receiver, + termId, + curveId, + minShares + ); + emit MultiVaultSuccess("deposit", 1); + return result; + } + + /// @notice Calculate how much MultiVault will receive for a given msg.value + /// @param msgValue The value that will be sent with the transaction + /// @return Amount that will be forwarded to MultiVault + function getMultiVaultAmountFromValue(uint256 msgValue) public view returns (uint256) { + if (msgValue <= depositFixedFee) return 0; + return (msgValue - depositFixedFee) * FEE_DENOMINATOR / (FEE_DENOMINATOR + depositPercentageFee); + } + + /// @notice Batch deposit with fee collection + /// @param receiver Address to receive shares + /// @param termIds Array of vault IDs + /// @param curveIds Array of curve IDs + /// @param assets Array of deposit amounts + /// @param minShares Array of minimum shares expected + /// @return shares Array of shares minted + function depositBatch( + address receiver, + bytes32[] calldata termIds, + uint256[] calldata curveIds, + uint256[] calldata assets, + uint256[] calldata minShares + ) external payable returns (uint256[] memory shares) { + if (termIds.length != curveIds.length || + curveIds.length != assets.length || + assets.length != minShares.length) { + revert Errors.IntuitionFeeProxy_WrongArrayLengths(); + } + + uint256 totalDeposit = _sumArray(assets); + // Fee: fixed fee per deposit + percentage of total + uint256 fee = calculateDepositFee(termIds.length, totalDeposit); + uint256 totalRequired = totalDeposit + fee; + + if (msg.value < totalRequired) { + revert Errors.IntuitionFeeProxy_InsufficientValue(); + } + emit FeesCollected(msg.sender, fee, "depositBatch"); emit TransactionForwarded("depositBatch", msg.sender, fee, totalDeposit, msg.value); - - uint256[] memory result = ethMultiVault.depositBatch{value: totalDeposit}( - receiver, - termIds, - curveIds, - assets, - minShares - ); - emit MultiVaultSuccess("depositBatch", result.length); - return result; - } - - // ============ View Functions (Passthrough) ============ - - /// @notice Get atom creation cost from MultiVault - function getAtomCost() external view returns (uint256) { - return ethMultiVault.getAtomCost(); - } - - /// @notice Get triple creation cost from MultiVault - function getTripleCost() external view returns (uint256) { - return ethMultiVault.getTripleCost(); - } - - /// @notice Calculate atom ID (passthrough to MultiVault) - function calculateAtomId(bytes calldata data) external pure returns (bytes32) { - return keccak256(data); - } - - /// @notice Calculate triple ID (passthrough to MultiVault) - function calculateTripleId( - bytes32 subjectId, - bytes32 predicateId, - bytes32 objectId - ) external view returns (bytes32) { - return ethMultiVault.calculateTripleId(subjectId, predicateId, objectId); - } - - /// @notice Get triple components (passthrough to MultiVault) - function getTriple(bytes32 tripleId) - external view returns (bytes32, bytes32, bytes32) - { - return ethMultiVault.getTriple(tripleId); - } - - /// @notice Get user shares (passthrough to MultiVault) - function getShares( - address account, - bytes32 termId, - uint256 curveId - ) external view returns (uint256) { - return ethMultiVault.getShares(account, termId, curveId); - } - - /// @notice Check if term exists (passthrough to MultiVault) - function isTermCreated(bytes32 id) external view returns (bool) { - return ethMultiVault.isTermCreated(id); - } - - /// @notice Preview deposit (passthrough to MultiVault) - function previewDeposit( - bytes32 termId, - uint256 curveId, - uint256 assets - ) external view returns (uint256, uint256) { - return ethMultiVault.previewDeposit(termId, curveId, assets); - } - - // ============ Internal Functions ============ - - /// @notice Transfer collected fees to recipient - /// @param amount Amount to transfer - function _transferFee(uint256 amount) internal { - if (amount > 0) { - (bool success, ) = feeRecipient.call{value: amount}(""); - if (!success) { - revert Errors.IntuitionFeeProxy_TransferFailed(); - } - } - } - + + uint256[] memory result = ethMultiVault.depositBatch{value: totalDeposit}( + receiver, + termIds, + curveIds, + assets, + minShares + ); + emit MultiVaultSuccess("depositBatch", result.length); + return result; + } + + // ============ View Functions (Passthrough) ============ + + /// @notice Get atom creation cost from MultiVault + function getAtomCost() external view returns (uint256) { + return ethMultiVault.getAtomCost(); + } + + /// @notice Get triple creation cost from MultiVault + function getTripleCost() external view returns (uint256) { + return ethMultiVault.getTripleCost(); + } + + /// @notice Calculate atom ID (passthrough to MultiVault) + function calculateAtomId(bytes calldata data) external pure returns (bytes32) { + return keccak256(data); + } + + /// @notice Calculate triple ID (passthrough to MultiVault) + function calculateTripleId( + bytes32 subjectId, + bytes32 predicateId, + bytes32 objectId + ) external view returns (bytes32) { + return ethMultiVault.calculateTripleId(subjectId, predicateId, objectId); + } + + /// @notice Get triple components (passthrough to MultiVault) + function getTriple(bytes32 tripleId) + external view returns (bytes32, bytes32, bytes32) + { + return ethMultiVault.getTriple(tripleId); + } + + /// @notice Get user shares (passthrough to MultiVault) + function getShares( + address account, + bytes32 termId, + uint256 curveId + ) external view returns (uint256) { + return ethMultiVault.getShares(account, termId, curveId); + } + + /// @notice Check if term exists (passthrough to MultiVault) + function isTermCreated(bytes32 id) external view returns (bool) { + return ethMultiVault.isTermCreated(id); + } + + /// @notice Preview deposit (passthrough to MultiVault) + function previewDeposit( + bytes32 termId, + uint256 curveId, + uint256 assets + ) external view returns (uint256, uint256) { + return ethMultiVault.previewDeposit(termId, curveId, assets); + } + + // ============ Internal Functions ============ + /// @notice Sum array of uint256 values - /// @param arr Array to sum - /// @return sum Total sum - function _sumArray(uint256[] calldata arr) internal pure returns (uint256 sum) { - for (uint256 i = 0; i < arr.length; i++) { - sum += arr[i]; - } - } - - /// @notice Count non-zero elements in array - /// @param arr Array to count - /// @return count Number of non-zero elements - function _countNonZero(uint256[] calldata arr) internal pure returns (uint256 count) { - for (uint256 i = 0; i < arr.length; i++) { - if (arr[i] > 0) { - count++; - } - } - } - - /// @notice Receive function to accept ETH (for refunds) - receive() external payable {} -} + /// @param arr Array to sum + /// @return sum Total sum + function _sumArray(uint256[] calldata arr) internal pure returns (uint256 sum) { + for (uint256 i = 0; i < arr.length; i++) { + sum += arr[i]; + } + } + + /// @notice Count non-zero elements in array + /// @param arr Array to count + /// @return count Number of non-zero elements + function _countNonZero(uint256[] calldata arr) internal pure returns (uint256 count) { + for (uint256 i = 0; i < arr.length; i++) { + if (arr[i] > 0) { + count++; + } + } + } + + /// @notice Receive function to accept ETH (for refunds) + receive() external payable {} +} diff --git a/test/IntuitionFeeProxy.test.ts b/test/IntuitionFeeProxy.test.ts index 9365c5e..4b4ad1d 100644 --- a/test/IntuitionFeeProxy.test.ts +++ b/test/IntuitionFeeProxy.test.ts @@ -1,426 +1,483 @@ -import { expect } from "chai"; -import { ethers } from "hardhat"; -import { loadFixture } from "@nomicfoundation/hardhat-toolbox/network-helpers"; -import { IntuitionFeeProxy, MockMultiVault } from "../typechain-types"; -import { SignerWithAddress } from "@nomicfoundation/hardhat-ethers/signers"; - -describe("IntuitionFeeProxy", function () { - // Constants - customize these for your deployment - const FEE_RECIPIENT = "0x0000000000000000000000000000000000000001"; // Replace with your address - const DEPOSIT_FEE = ethers.parseEther("0.1"); // 0.1 TRUST per deposit - const DEPOSIT_PERCENTAGE = 500n; // 5% - const FEE_DENOMINATOR = 10000n; - - // Fixture to deploy contracts - async function deployFixture() { - const [owner, admin1, admin2, admin3, user, nonAdmin] = await ethers.getSigners(); - - // Deploy MockMultiVault - const MockMultiVaultFactory = await ethers.getContractFactory("MockMultiVault"); - const mockMultiVault = await MockMultiVaultFactory.deploy(); - await mockMultiVault.waitForDeployment(); - - // Deploy IntuitionFeeProxy - const IntuitionFeeProxyFactory = await ethers.getContractFactory("IntuitionFeeProxy"); - const proxy = await IntuitionFeeProxyFactory.deploy( - await mockMultiVault.getAddress(), - FEE_RECIPIENT, - DEPOSIT_FEE, - DEPOSIT_PERCENTAGE, - [admin1.address, admin2.address, admin3.address] - ); - await proxy.waitForDeployment(); - - return { proxy, mockMultiVault, owner, admin1, admin2, admin3, user, nonAdmin }; - } - - describe("Initialization", function () { - it("Should set correct MultiVault address", async function () { - const { proxy, mockMultiVault } = await loadFixture(deployFixture); - expect(await proxy.ethMultiVault()).to.equal(await mockMultiVault.getAddress()); - }); - - it("Should set correct fee recipient", async function () { - const { proxy } = await loadFixture(deployFixture); - expect(await proxy.feeRecipient()).to.equal(FEE_RECIPIENT); - }); - - it("Should set correct deposit fees", async function () { - const { proxy } = await loadFixture(deployFixture); - expect(await proxy.depositFixedFee()).to.equal(DEPOSIT_FEE); - expect(await proxy.depositPercentageFee()).to.equal(DEPOSIT_PERCENTAGE); - }); - - it("Should whitelist initial admins", async function () { - const { proxy, admin1, admin2, admin3 } = await loadFixture(deployFixture); - expect(await proxy.whitelistedAdmins(admin1.address)).to.be.true; - expect(await proxy.whitelistedAdmins(admin2.address)).to.be.true; - expect(await proxy.whitelistedAdmins(admin3.address)).to.be.true; - }); - - it("Should not whitelist non-admins", async function () { - const { proxy, nonAdmin } = await loadFixture(deployFixture); - expect(await proxy.whitelistedAdmins(nonAdmin.address)).to.be.false; - }); - - it("Should revert on zero MultiVault address", async function () { - const [admin] = await ethers.getSigners(); - const IntuitionFeeProxyFactory = await ethers.getContractFactory("IntuitionFeeProxy"); - - await expect( - IntuitionFeeProxyFactory.deploy( - ethers.ZeroAddress, - FEE_RECIPIENT, - DEPOSIT_FEE, - DEPOSIT_PERCENTAGE, - [admin.address] - ) - ).to.be.revertedWithCustomError(IntuitionFeeProxyFactory, "IntuitionFeeProxy_InvalidMultiVaultAddress"); - }); - - it("Should revert on zero fee recipient address", async function () { - const { mockMultiVault } = await loadFixture(deployFixture); - const [admin] = await ethers.getSigners(); - const IntuitionFeeProxyFactory = await ethers.getContractFactory("IntuitionFeeProxy"); - - await expect( - IntuitionFeeProxyFactory.deploy( - await mockMultiVault.getAddress(), - ethers.ZeroAddress, - DEPOSIT_FEE, - DEPOSIT_PERCENTAGE, - [admin.address] - ) - ).to.be.revertedWithCustomError(IntuitionFeeProxyFactory, "IntuitionFeeProxy_InvalidMultisigAddress"); - }); - }); - - describe("Fee Calculations", function () { - it("Should calculate deposit fee correctly (single deposit)", async function () { - const { proxy } = await loadFixture(deployFixture); - const depositAmount = ethers.parseEther("10"); - - // Fee = 0.1 + (10 * 5%) = 0.1 + 0.5 = 0.6 TRUST - const expectedFee = DEPOSIT_FEE + (depositAmount * DEPOSIT_PERCENTAGE / FEE_DENOMINATOR); - expect(await proxy.calculateDepositFee(1n, depositAmount)).to.equal(expectedFee); - expect(expectedFee).to.equal(ethers.parseEther("0.6")); - }); - - it("Should calculate deposit fee correctly (multiple deposits)", async function () { - const { proxy } = await loadFixture(deployFixture); - const depositCount = 3n; - const totalDeposit = ethers.parseEther("30"); - - // Fee = (0.1 * 3) + (30 * 5%) = 0.3 + 1.5 = 1.8 TRUST - const expectedFee = (DEPOSIT_FEE * depositCount) + (totalDeposit * DEPOSIT_PERCENTAGE / FEE_DENOMINATOR); - expect(await proxy.calculateDepositFee(depositCount, totalDeposit)).to.equal(expectedFee); - expect(expectedFee).to.equal(ethers.parseEther("1.8")); - }); - - it("Should calculate total deposit cost correctly", async function () { - const { proxy } = await loadFixture(deployFixture); - const depositAmount = ethers.parseEther("10"); - - const fee = await proxy.calculateDepositFee(1n, depositAmount); - const totalCost = await proxy.getTotalDepositCost(depositAmount); - expect(totalCost).to.equal(depositAmount + fee); - }); - - it("Should calculate total creation cost correctly", async function () { - const { proxy } = await loadFixture(deployFixture); - const depositCount = 3n; - const totalDeposit = ethers.parseEther("0.03"); - const multiVaultCost = ethers.parseEther("1"); - - const fee = await proxy.calculateDepositFee(depositCount, totalDeposit); - const totalCost = await proxy.getTotalCreationCost(depositCount, totalDeposit, multiVaultCost); - expect(totalCost).to.equal(multiVaultCost + fee); - }); - }); - - describe("Admin Functions", function () { - it("Should allow admin to set deposit fixed fee", async function () { - const { proxy, admin1 } = await loadFixture(deployFixture); - const newFee = ethers.parseEther("0.2"); - - await expect(proxy.connect(admin1).setDepositFixedFee(newFee)) - .to.emit(proxy, "DepositFixedFeeUpdated") - .withArgs(DEPOSIT_FEE, newFee); - - expect(await proxy.depositFixedFee()).to.equal(newFee); - }); - - it("Should allow admin to set deposit percentage", async function () { - const { proxy, admin2 } = await loadFixture(deployFixture); - const newPercentage = 1000n; // 10% - - await expect(proxy.connect(admin2).setDepositPercentageFee(newPercentage)) - .to.emit(proxy, "DepositPercentageFeeUpdated") - .withArgs(DEPOSIT_PERCENTAGE, newPercentage); - - expect(await proxy.depositPercentageFee()).to.equal(newPercentage); - }); - - it("Should allow admin to set fee recipient", async function () { - const { proxy, admin1, user } = await loadFixture(deployFixture); - - await expect(proxy.connect(admin1).setFeeRecipient(user.address)) - .to.emit(proxy, "FeeRecipientUpdated") - .withArgs(FEE_RECIPIENT, user.address); - - expect(await proxy.feeRecipient()).to.equal(user.address); - }); - - it("Should allow admin to whitelist new admin", async function () { - const { proxy, admin1, nonAdmin } = await loadFixture(deployFixture); - - await expect(proxy.connect(admin1).setWhitelistedAdmin(nonAdmin.address, true)) - .to.emit(proxy, "AdminWhitelistUpdated") - .withArgs(nonAdmin.address, true); - - expect(await proxy.whitelistedAdmins(nonAdmin.address)).to.be.true; - }); - - it("Should allow admin to remove admin", async function () { - const { proxy, admin1, admin2 } = await loadFixture(deployFixture); - - await proxy.connect(admin1).setWhitelistedAdmin(admin2.address, false); - expect(await proxy.whitelistedAdmins(admin2.address)).to.be.false; - }); - - it("Should revert when non-admin tries to set fees", async function () { - const { proxy, nonAdmin } = await loadFixture(deployFixture); - - await expect(proxy.connect(nonAdmin).setDepositFixedFee(ethers.parseEther("0.5"))) - .to.be.revertedWithCustomError(proxy, "IntuitionFeeProxy_NotWhitelistedAdmin"); - }); - - it("Should revert when setting fee recipient to zero address", async function () { - const { proxy, admin1 } = await loadFixture(deployFixture); - - await expect(proxy.connect(admin1).setFeeRecipient(ethers.ZeroAddress)) - .to.be.revertedWithCustomError(proxy, "IntuitionFeeProxy_ZeroAddress"); - }); - +import { expect } from "chai"; +import { ethers } from "hardhat"; +import { loadFixture } from "@nomicfoundation/hardhat-toolbox/network-helpers"; +import { IntuitionFeeProxy, MockMultiVault } from "../typechain-types"; +import { SignerWithAddress } from "@nomicfoundation/hardhat-ethers/signers"; + +describe("IntuitionFeeProxy", function () { + // Constants - customize these for your deployment + const FEE_RECIPIENT = "0x0000000000000000000000000000000000000001"; // Replace with your address + const DEPOSIT_FEE = ethers.parseEther("0.1"); // 0.1 TRUST per deposit + const DEPOSIT_PERCENTAGE = 500n; // 5% + const FEE_DENOMINATOR = 10000n; + + // Fixture to deploy contracts + async function deployFixture() { + const [owner, admin1, admin2, admin3, user, nonAdmin] = await ethers.getSigners(); + + // Deploy MockMultiVault + const MockMultiVaultFactory = await ethers.getContractFactory("MockMultiVault"); + const mockMultiVault = await MockMultiVaultFactory.deploy(); + await mockMultiVault.waitForDeployment(); + + // Deploy IntuitionFeeProxy + const IntuitionFeeProxyFactory = await ethers.getContractFactory("IntuitionFeeProxy"); + const proxy = await IntuitionFeeProxyFactory.deploy( + await mockMultiVault.getAddress(), + FEE_RECIPIENT, + DEPOSIT_FEE, + DEPOSIT_PERCENTAGE, + [admin1.address, admin2.address, admin3.address] + ); + await proxy.waitForDeployment(); + + return { proxy, mockMultiVault, owner, admin1, admin2, admin3, user, nonAdmin }; + } + + describe("Initialization", function () { + it("Should set correct MultiVault address", async function () { + const { proxy, mockMultiVault } = await loadFixture(deployFixture); + expect(await proxy.ethMultiVault()).to.equal(await mockMultiVault.getAddress()); + }); + + it("Should set correct fee recipient", async function () { + const { proxy } = await loadFixture(deployFixture); + expect(await proxy.feeRecipient()).to.equal(FEE_RECIPIENT); + }); + + it("Should set correct deposit fees", async function () { + const { proxy } = await loadFixture(deployFixture); + expect(await proxy.depositFixedFee()).to.equal(DEPOSIT_FEE); + expect(await proxy.depositPercentageFee()).to.equal(DEPOSIT_PERCENTAGE); + }); + + it("Should whitelist initial admins", async function () { + const { proxy, admin1, admin2, admin3 } = await loadFixture(deployFixture); + expect(await proxy.whitelistedAdmins(admin1.address)).to.be.true; + expect(await proxy.whitelistedAdmins(admin2.address)).to.be.true; + expect(await proxy.whitelistedAdmins(admin3.address)).to.be.true; + }); + + it("Should not whitelist non-admins", async function () { + const { proxy, nonAdmin } = await loadFixture(deployFixture); + expect(await proxy.whitelistedAdmins(nonAdmin.address)).to.be.false; + }); + + it("Should revert on zero MultiVault address", async function () { + const [admin] = await ethers.getSigners(); + const IntuitionFeeProxyFactory = await ethers.getContractFactory("IntuitionFeeProxy"); + + await expect( + IntuitionFeeProxyFactory.deploy( + ethers.ZeroAddress, + FEE_RECIPIENT, + DEPOSIT_FEE, + DEPOSIT_PERCENTAGE, + [admin.address] + ) + ).to.be.revertedWithCustomError(IntuitionFeeProxyFactory, "IntuitionFeeProxy_InvalidMultiVaultAddress"); + }); + + it("Should revert on zero fee recipient address", async function () { + const { mockMultiVault } = await loadFixture(deployFixture); + const [admin] = await ethers.getSigners(); + const IntuitionFeeProxyFactory = await ethers.getContractFactory("IntuitionFeeProxy"); + + await expect( + IntuitionFeeProxyFactory.deploy( + await mockMultiVault.getAddress(), + ethers.ZeroAddress, + DEPOSIT_FEE, + DEPOSIT_PERCENTAGE, + [admin.address] + ) + ).to.be.revertedWithCustomError(IntuitionFeeProxyFactory, "IntuitionFeeProxy_InvalidMultisigAddress"); + }); + }); + + describe("Fee Calculations", function () { + it("Should calculate deposit fee correctly (single deposit)", async function () { + const { proxy } = await loadFixture(deployFixture); + const depositAmount = ethers.parseEther("10"); + + // Fee = 0.1 + (10 * 5%) = 0.1 + 0.5 = 0.6 TRUST + const expectedFee = DEPOSIT_FEE + (depositAmount * DEPOSIT_PERCENTAGE / FEE_DENOMINATOR); + expect(await proxy.calculateDepositFee(1n, depositAmount)).to.equal(expectedFee); + expect(expectedFee).to.equal(ethers.parseEther("0.6")); + }); + + it("Should calculate deposit fee correctly (multiple deposits)", async function () { + const { proxy } = await loadFixture(deployFixture); + const depositCount = 3n; + const totalDeposit = ethers.parseEther("30"); + + // Fee = (0.1 * 3) + (30 * 5%) = 0.3 + 1.5 = 1.8 TRUST + const expectedFee = (DEPOSIT_FEE * depositCount) + (totalDeposit * DEPOSIT_PERCENTAGE / FEE_DENOMINATOR); + expect(await proxy.calculateDepositFee(depositCount, totalDeposit)).to.equal(expectedFee); + expect(expectedFee).to.equal(ethers.parseEther("1.8")); + }); + + it("Should calculate total deposit cost correctly", async function () { + const { proxy } = await loadFixture(deployFixture); + const depositAmount = ethers.parseEther("10"); + + const fee = await proxy.calculateDepositFee(1n, depositAmount); + const totalCost = await proxy.getTotalDepositCost(depositAmount); + expect(totalCost).to.equal(depositAmount + fee); + }); + + it("Should calculate total creation cost correctly", async function () { + const { proxy } = await loadFixture(deployFixture); + const depositCount = 3n; + const totalDeposit = ethers.parseEther("0.03"); + const multiVaultCost = ethers.parseEther("1"); + + const fee = await proxy.calculateDepositFee(depositCount, totalDeposit); + const totalCost = await proxy.getTotalCreationCost(depositCount, totalDeposit, multiVaultCost); + expect(totalCost).to.equal(multiVaultCost + fee); + }); + }); + + describe("Admin Functions", function () { + it("Should allow admin to set deposit fixed fee", async function () { + const { proxy, admin1 } = await loadFixture(deployFixture); + const newFee = ethers.parseEther("0.2"); + + await expect(proxy.connect(admin1).setDepositFixedFee(newFee)) + .to.emit(proxy, "DepositFixedFeeUpdated") + .withArgs(DEPOSIT_FEE, newFee); + + expect(await proxy.depositFixedFee()).to.equal(newFee); + }); + + it("Should allow admin to set deposit percentage", async function () { + const { proxy, admin2 } = await loadFixture(deployFixture); + const newPercentage = 1000n; // 10% + + await expect(proxy.connect(admin2).setDepositPercentageFee(newPercentage)) + .to.emit(proxy, "DepositPercentageFeeUpdated") + .withArgs(DEPOSIT_PERCENTAGE, newPercentage); + + expect(await proxy.depositPercentageFee()).to.equal(newPercentage); + }); + + it("Should allow admin to set fee recipient", async function () { + const { proxy, admin1, user } = await loadFixture(deployFixture); + + await expect(proxy.connect(admin1).setFeeRecipient(user.address)) + .to.emit(proxy, "FeeRecipientUpdated") + .withArgs(FEE_RECIPIENT, user.address); + + expect(await proxy.feeRecipient()).to.equal(user.address); + }); + + it("Should allow admin to whitelist new admin", async function () { + const { proxy, admin1, nonAdmin } = await loadFixture(deployFixture); + + await expect(proxy.connect(admin1).setWhitelistedAdmin(nonAdmin.address, true)) + .to.emit(proxy, "AdminWhitelistUpdated") + .withArgs(nonAdmin.address, true); + + expect(await proxy.whitelistedAdmins(nonAdmin.address)).to.be.true; + }); + + it("Should allow admin to remove admin", async function () { + const { proxy, admin1, admin2 } = await loadFixture(deployFixture); + + await proxy.connect(admin1).setWhitelistedAdmin(admin2.address, false); + expect(await proxy.whitelistedAdmins(admin2.address)).to.be.false; + }); + + it("Should revert when non-admin tries to set fees", async function () { + const { proxy, nonAdmin } = await loadFixture(deployFixture); + + await expect(proxy.connect(nonAdmin).setDepositFixedFee(ethers.parseEther("0.5"))) + .to.be.revertedWithCustomError(proxy, "IntuitionFeeProxy_NotWhitelistedAdmin"); + }); + + it("Should revert when setting fee recipient to zero address", async function () { + const { proxy, admin1 } = await loadFixture(deployFixture); + + await expect(proxy.connect(admin1).setFeeRecipient(ethers.ZeroAddress)) + .to.be.revertedWithCustomError(proxy, "IntuitionFeeProxy_ZeroAddress"); + }); + it("Should revert when percentage fee is too high", async function () { const { proxy, admin1 } = await loadFixture(deployFixture); await expect(proxy.connect(admin1).setDepositPercentageFee(10001n)) .to.be.revertedWithCustomError(proxy, "IntuitionFeeProxy_FeePercentageTooHigh"); }); - }); - - describe("Proxy Functions - createAtoms", function () { - it("Should collect fees on createAtoms (fees based on deposits)", async function () { - const { proxy, mockMultiVault, user } = await loadFixture(deployFixture); - - const data = [ethers.toUtf8Bytes("ipfs://atom1"), ethers.toUtf8Bytes("ipfs://atom2")]; - const assets = [ethers.parseEther("0.01"), ethers.parseEther("0.01")]; - const curveId = 1n; - - const atomCost = await mockMultiVault.getAtomCost(); - const totalDeposit = ethers.parseEther("0.02"); - const depositCount = 2n; // Both have non-zero deposits - const fee = await proxy.calculateDepositFee(depositCount, totalDeposit); - const multiVaultCost = (atomCost * 2n) + totalDeposit; - const totalRequired = fee + multiVaultCost; - - const initialBalance = await ethers.provider.getBalance(FEE_RECIPIENT); - - await expect(proxy.connect(user).createAtoms(user.address, data, assets, curveId, { value: totalRequired })) - .to.emit(proxy, "FeesCollected") - .withArgs(user.address, fee, "createAtoms"); - const finalBalance = await ethers.provider.getBalance(FEE_RECIPIENT); - expect(finalBalance - initialBalance).to.equal(fee); - }); - - it("Should not charge fees for zero deposits in createAtoms", async function () { - const { proxy, mockMultiVault, user } = await loadFixture(deployFixture); - - const data = [ethers.toUtf8Bytes("ipfs://atom1"), ethers.toUtf8Bytes("ipfs://atom2")]; - const assets = [0n, 0n]; // No deposits - const curveId = 1n; - - const atomCost = await mockMultiVault.getAtomCost(); - const fee = 0n; // No deposits = no fee - const multiVaultCost = atomCost * 2n; - const totalRequired = fee + multiVaultCost; - - const initialBalance = await ethers.provider.getBalance(FEE_RECIPIENT); - - await proxy.connect(user).createAtoms(user.address, data, assets, curveId, { value: totalRequired }); - - const finalBalance = await ethers.provider.getBalance(FEE_RECIPIENT); - expect(finalBalance - initialBalance).to.equal(0n); - }); - - it("Should revert on insufficient value for createAtoms", async function () { - const { proxy, user } = await loadFixture(deployFixture); - - const data = [ethers.toUtf8Bytes("ipfs://atom1")]; - const assets = [ethers.parseEther("0.01")]; - const curveId = 1n; - - await expect( - proxy.connect(user).createAtoms(user.address, data, assets, curveId, { value: ethers.parseEther("0.01") }) - ).to.be.revertedWithCustomError(proxy, "IntuitionFeeProxy_InsufficientValue"); - }); - }); - - describe("Proxy Functions - createTriples", function () { - it("Should collect fees on createTriples (fees based on deposits)", async function () { - const { proxy, mockMultiVault, user } = await loadFixture(deployFixture); - - const subjectIds = [ethers.zeroPadValue("0x01", 32)]; - const predicateIds = [ethers.zeroPadValue("0x02", 32)]; - const objectIds = [ethers.zeroPadValue("0x03", 32)]; - const assets = [ethers.parseEther("0.01")]; - const curveId = 1n; - - const tripleCost = await mockMultiVault.getTripleCost(); - const totalDeposit = ethers.parseEther("0.01"); - const depositCount = 1n; - const fee = await proxy.calculateDepositFee(depositCount, totalDeposit); - const multiVaultCost = tripleCost + totalDeposit; - const totalRequired = fee + multiVaultCost; - - const initialBalance = await ethers.provider.getBalance(FEE_RECIPIENT); - - await expect(proxy.connect(user).createTriples(user.address, subjectIds, predicateIds, objectIds, assets, curveId, { value: totalRequired })) - .to.emit(proxy, "FeesCollected") - .withArgs(user.address, fee, "createTriples"); - - const finalBalance = await ethers.provider.getBalance(FEE_RECIPIENT); - expect(finalBalance - initialBalance).to.equal(fee); - }); - - it("Should revert on wrong array lengths", async function () { - const { proxy, user } = await loadFixture(deployFixture); - - const subjectIds = [ethers.zeroPadValue("0x01", 32), ethers.zeroPadValue("0x04", 32)]; - const predicateIds = [ethers.zeroPadValue("0x02", 32)]; // Wrong length - const objectIds = [ethers.zeroPadValue("0x03", 32), ethers.zeroPadValue("0x05", 32)]; - const assets = [ethers.parseEther("0.01"), ethers.parseEther("0.01")]; - const curveId = 1n; - - await expect( - proxy.connect(user).createTriples(user.address, subjectIds, predicateIds, objectIds, assets, curveId, { value: ethers.parseEther("10") }) - ).to.be.revertedWithCustomError(proxy, "IntuitionFeeProxy_WrongArrayLengths"); - }); - }); - - describe("Proxy Functions - deposit", function () { - it("Should collect fees on deposit (inverse calculation)", async function () { - const { proxy, user } = await loadFixture(deployFixture); + it("Should allow an admin to withdraw accumulated fees", async function () { + const { proxy, admin1, user } = await loadFixture(deployFixture); - const desiredDepositAmount = ethers.parseEther("10"); + const desiredDepositAmount = ethers.parseEther("1"); const totalToSend = await proxy.getTotalDepositCost(desiredDepositAmount); - - const initialBalance = await ethers.provider.getBalance(FEE_RECIPIENT); - - const termId = ethers.zeroPadValue("0x01", 32); - - await expect(proxy.connect(user).deposit(user.address, termId, 1n, 0n, { value: totalToSend })) - .to.emit(proxy, "FeesCollected"); - - const finalBalance = await ethers.provider.getBalance(FEE_RECIPIENT); - const collectedFee = finalBalance - initialBalance; const expectedFee = await proxy.calculateDepositFee(1n, desiredDepositAmount); - expect(collectedFee).to.be.closeTo(expectedFee, 1); - }); - - it("Should calculate multiVaultAmount correctly", async function () { - const { proxy } = await loadFixture(deployFixture); - - const totalSent = ethers.parseEther("10.6"); // 10 + 0.1 fixed + 0.5 (5% of 10) - const multiVaultAmount = await proxy.getMultiVaultAmountFromValue(totalSent); - - expect(multiVaultAmount).to.be.closeTo(ethers.parseEther("10"), ethers.parseEther("0.001")); - }); - - it("Should revert when sending only fixed fee or less", async function () { - const { proxy, user } = await loadFixture(deployFixture); - const termId = ethers.zeroPadValue("0x01", 32); - await expect( - proxy.connect(user).deposit(user.address, termId, 1n, 0n, { value: DEPOSIT_FEE }) - ).to.be.revertedWithCustomError(proxy, "IntuitionFeeProxy_InsufficientValue"); + await proxy.connect(user).deposit(user.address, termId, 1n, 0n, { value: totalToSend }); + expect(await ethers.provider.getBalance(await proxy.getAddress())).to.equal(expectedFee); - await expect( - proxy.connect(user).deposit(user.address, termId, 1n, 0n, { value: ethers.parseEther("0.05") }) - ).to.be.revertedWithCustomError(proxy, "IntuitionFeeProxy_InsufficientValue"); - }); + const initialRecipientBalance = await ethers.provider.getBalance(FEE_RECIPIENT); - it("Should return 0 from getMultiVaultAmountFromValue for insufficient value", async function () { - const { proxy } = await loadFixture(deployFixture); + await expect(proxy.connect(admin1).withdrawFees(FEE_RECIPIENT, expectedFee)) + .to.emit(proxy, "FeesWithdrawn") + .withArgs(admin1.address, FEE_RECIPIENT, expectedFee); - expect(await proxy.getMultiVaultAmountFromValue(DEPOSIT_FEE)).to.equal(0n); - expect(await proxy.getMultiVaultAmountFromValue(ethers.parseEther("0.05"))).to.equal(0n); + expect(await ethers.provider.getBalance(await proxy.getAddress())).to.equal(0n); + expect(await ethers.provider.getBalance(FEE_RECIPIENT)).to.equal(initialRecipientBalance + expectedFee); }); - }); - describe("Proxy Functions - depositBatch", function () { - it("Should collect fees on depositBatch", async function () { - const { proxy, user } = await loadFixture(deployFixture); + it("Should allow the current fee recipient to withdraw accumulated fees", async function () { + const { proxy, admin1, user, nonAdmin } = await loadFixture(deployFixture); - const termIds = [ethers.zeroPadValue("0x01", 32), ethers.zeroPadValue("0x02", 32)]; - const curveIds = [1n, 1n]; - const assets = [ethers.parseEther("5"), ethers.parseEther("5")]; - const minShares = [0n, 0n]; + await proxy.connect(admin1).setFeeRecipient(nonAdmin.address); - const totalDeposit = ethers.parseEther("10"); - const fee = await proxy.calculateDepositFee(2n, totalDeposit); - const totalRequired = totalDeposit + fee; + const desiredDepositAmount = ethers.parseEther("1"); + const totalToSend = await proxy.getTotalDepositCost(desiredDepositAmount); + const expectedFee = await proxy.calculateDepositFee(1n, desiredDepositAmount); + const termId = ethers.zeroPadValue("0x01", 32); - const initialBalance = await ethers.provider.getBalance(FEE_RECIPIENT); + await proxy.connect(user).deposit(user.address, termId, 1n, 0n, { value: totalToSend }); - await expect(proxy.connect(user).depositBatch(user.address, termIds, curveIds, assets, minShares, { value: totalRequired })) - .to.emit(proxy, "FeesCollected") - .withArgs(user.address, fee, "depositBatch"); + await expect(proxy.connect(nonAdmin).withdrawFees(nonAdmin.address, expectedFee)) + .to.emit(proxy, "FeesWithdrawn") + .withArgs(nonAdmin.address, nonAdmin.address, expectedFee); - const finalBalance = await ethers.provider.getBalance(FEE_RECIPIENT); - expect(finalBalance - initialBalance).to.equal(fee); + expect(await ethers.provider.getBalance(await proxy.getAddress())).to.equal(0n); }); - it("Should revert on wrong array lengths in depositBatch", async function () { - const { proxy, user } = await loadFixture(deployFixture); - - const termIds = [ethers.zeroPadValue("0x01", 32), ethers.zeroPadValue("0x02", 32)]; - const curveIds = [1n]; // Wrong length - const assets = [ethers.parseEther("5"), ethers.parseEther("5")]; - const minShares = [0n, 0n]; - - await expect( - proxy.connect(user).depositBatch(user.address, termIds, curveIds, assets, minShares, { value: ethers.parseEther("20") }) - ).to.be.revertedWithCustomError(proxy, "IntuitionFeeProxy_WrongArrayLengths"); - }); - }); + it("Should reject unauthorized fee withdrawals", async function () { + const { proxy, nonAdmin } = await loadFixture(deployFixture); - describe("View Functions (Passthrough)", function () { - it("Should return atom cost from MultiVault", async function () { - const { proxy, mockMultiVault } = await loadFixture(deployFixture); - expect(await proxy.getAtomCost()).to.equal(await mockMultiVault.getAtomCost()); + await expect(proxy.connect(nonAdmin).withdrawFees(nonAdmin.address, 1n)) + .to.be.revertedWithCustomError(proxy, "IntuitionFeeProxy_NotWhitelistedAdmin"); }); - it("Should return triple cost from MultiVault", async function () { - const { proxy, mockMultiVault } = await loadFixture(deployFixture); - expect(await proxy.getTripleCost()).to.equal(await mockMultiVault.getTripleCost()); - }); + it("Should reject invalid fee withdrawals", async function () { + const { proxy, admin1 } = await loadFixture(deployFixture); - it("Should return isTermCreated from MultiVault", async function () { - const { proxy, mockMultiVault } = await loadFixture(deployFixture); - const termId = ethers.zeroPadValue("0x01", 32); - await mockMultiVault.setTermCreated(termId, true); - expect(await proxy.isTermCreated(termId)).to.be.true; - }); + await expect(proxy.connect(admin1).withdrawFees(ethers.ZeroAddress, 1n)) + .to.be.revertedWithCustomError(proxy, "IntuitionFeeProxy_ZeroAddress"); - it("Should return shares from MultiVault", async function () { - const { proxy, mockMultiVault, user } = await loadFixture(deployFixture); - const termId = ethers.zeroPadValue("0x01", 32); - await mockMultiVault.setShares(user.address, termId, 1n, 1000n); - expect(await proxy.getShares(user.address, termId, 1n)).to.equal(1000n); + await expect(proxy.connect(admin1).withdrawFees(FEE_RECIPIENT, 1n)) + .to.be.revertedWithCustomError(proxy, "IntuitionFeeProxy_InsufficientValue"); }); }); -}); + + describe("Proxy Functions - createAtoms", function () { + it("Should collect fees on createAtoms (fees based on deposits)", async function () { + const { proxy, mockMultiVault, user } = await loadFixture(deployFixture); + + const data = [ethers.toUtf8Bytes("ipfs://atom1"), ethers.toUtf8Bytes("ipfs://atom2")]; + const assets = [ethers.parseEther("0.01"), ethers.parseEther("0.01")]; + const curveId = 1n; + + const atomCost = await mockMultiVault.getAtomCost(); + const totalDeposit = ethers.parseEther("0.02"); + const depositCount = 2n; // Both have non-zero deposits + const fee = await proxy.calculateDepositFee(depositCount, totalDeposit); + const multiVaultCost = (atomCost * 2n) + totalDeposit; + const totalRequired = fee + multiVaultCost; + + const initialBalance = await ethers.provider.getBalance(await proxy.getAddress()); + + await expect(proxy.connect(user).createAtoms(user.address, data, assets, curveId, { value: totalRequired })) + .to.emit(proxy, "FeesCollected") + .withArgs(user.address, fee, "createAtoms"); + + const finalBalance = await ethers.provider.getBalance(await proxy.getAddress()); + expect(finalBalance - initialBalance).to.equal(fee); + }); + + it("Should not charge fees for zero deposits in createAtoms", async function () { + const { proxy, mockMultiVault, user } = await loadFixture(deployFixture); + + const data = [ethers.toUtf8Bytes("ipfs://atom1"), ethers.toUtf8Bytes("ipfs://atom2")]; + const assets = [0n, 0n]; // No deposits + const curveId = 1n; + + const atomCost = await mockMultiVault.getAtomCost(); + const fee = 0n; // No deposits = no fee + const multiVaultCost = atomCost * 2n; + const totalRequired = fee + multiVaultCost; + + const initialBalance = await ethers.provider.getBalance(await proxy.getAddress()); + + await proxy.connect(user).createAtoms(user.address, data, assets, curveId, { value: totalRequired }); + + const finalBalance = await ethers.provider.getBalance(await proxy.getAddress()); + expect(finalBalance - initialBalance).to.equal(0n); + }); + + it("Should revert on insufficient value for createAtoms", async function () { + const { proxy, user } = await loadFixture(deployFixture); + + const data = [ethers.toUtf8Bytes("ipfs://atom1")]; + const assets = [ethers.parseEther("0.01")]; + const curveId = 1n; + + await expect( + proxy.connect(user).createAtoms(user.address, data, assets, curveId, { value: ethers.parseEther("0.01") }) + ).to.be.revertedWithCustomError(proxy, "IntuitionFeeProxy_InsufficientValue"); + }); + }); + + describe("Proxy Functions - createTriples", function () { + it("Should collect fees on createTriples (fees based on deposits)", async function () { + const { proxy, mockMultiVault, user } = await loadFixture(deployFixture); + + const subjectIds = [ethers.zeroPadValue("0x01", 32)]; + const predicateIds = [ethers.zeroPadValue("0x02", 32)]; + const objectIds = [ethers.zeroPadValue("0x03", 32)]; + const assets = [ethers.parseEther("0.01")]; + const curveId = 1n; + + const tripleCost = await mockMultiVault.getTripleCost(); + const totalDeposit = ethers.parseEther("0.01"); + const depositCount = 1n; + const fee = await proxy.calculateDepositFee(depositCount, totalDeposit); + const multiVaultCost = tripleCost + totalDeposit; + const totalRequired = fee + multiVaultCost; + + const initialBalance = await ethers.provider.getBalance(await proxy.getAddress()); + + await expect(proxy.connect(user).createTriples(user.address, subjectIds, predicateIds, objectIds, assets, curveId, { value: totalRequired })) + .to.emit(proxy, "FeesCollected") + .withArgs(user.address, fee, "createTriples"); + + const finalBalance = await ethers.provider.getBalance(await proxy.getAddress()); + expect(finalBalance - initialBalance).to.equal(fee); + }); + + it("Should revert on wrong array lengths", async function () { + const { proxy, user } = await loadFixture(deployFixture); + + const subjectIds = [ethers.zeroPadValue("0x01", 32), ethers.zeroPadValue("0x04", 32)]; + const predicateIds = [ethers.zeroPadValue("0x02", 32)]; // Wrong length + const objectIds = [ethers.zeroPadValue("0x03", 32), ethers.zeroPadValue("0x05", 32)]; + const assets = [ethers.parseEther("0.01"), ethers.parseEther("0.01")]; + const curveId = 1n; + + await expect( + proxy.connect(user).createTriples(user.address, subjectIds, predicateIds, objectIds, assets, curveId, { value: ethers.parseEther("10") }) + ).to.be.revertedWithCustomError(proxy, "IntuitionFeeProxy_WrongArrayLengths"); + }); + }); + + describe("Proxy Functions - deposit", function () { + it("Should collect fees on deposit (inverse calculation)", async function () { + const { proxy, user } = await loadFixture(deployFixture); + + const desiredDepositAmount = ethers.parseEther("10"); + const totalToSend = await proxy.getTotalDepositCost(desiredDepositAmount); + + const initialBalance = await ethers.provider.getBalance(await proxy.getAddress()); + + const termId = ethers.zeroPadValue("0x01", 32); + + await expect(proxy.connect(user).deposit(user.address, termId, 1n, 0n, { value: totalToSend })) + .to.emit(proxy, "FeesCollected"); + + const finalBalance = await ethers.provider.getBalance(await proxy.getAddress()); + const collectedFee = finalBalance - initialBalance; + const expectedFee = await proxy.calculateDepositFee(1n, desiredDepositAmount); + expect(collectedFee).to.be.closeTo(expectedFee, 1); + }); + + it("Should calculate multiVaultAmount correctly", async function () { + const { proxy } = await loadFixture(deployFixture); + + const totalSent = ethers.parseEther("10.6"); // 10 + 0.1 fixed + 0.5 (5% of 10) + const multiVaultAmount = await proxy.getMultiVaultAmountFromValue(totalSent); + + expect(multiVaultAmount).to.be.closeTo(ethers.parseEther("10"), ethers.parseEther("0.001")); + }); + + it("Should revert when sending only fixed fee or less", async function () { + const { proxy, user } = await loadFixture(deployFixture); + + const termId = ethers.zeroPadValue("0x01", 32); + + await expect( + proxy.connect(user).deposit(user.address, termId, 1n, 0n, { value: DEPOSIT_FEE }) + ).to.be.revertedWithCustomError(proxy, "IntuitionFeeProxy_InsufficientValue"); + + await expect( + proxy.connect(user).deposit(user.address, termId, 1n, 0n, { value: ethers.parseEther("0.05") }) + ).to.be.revertedWithCustomError(proxy, "IntuitionFeeProxy_InsufficientValue"); + }); + + it("Should return 0 from getMultiVaultAmountFromValue for insufficient value", async function () { + const { proxy } = await loadFixture(deployFixture); + + expect(await proxy.getMultiVaultAmountFromValue(DEPOSIT_FEE)).to.equal(0n); + expect(await proxy.getMultiVaultAmountFromValue(ethers.parseEther("0.05"))).to.equal(0n); + }); + }); + + describe("Proxy Functions - depositBatch", function () { + it("Should collect fees on depositBatch", async function () { + const { proxy, user } = await loadFixture(deployFixture); + + const termIds = [ethers.zeroPadValue("0x01", 32), ethers.zeroPadValue("0x02", 32)]; + const curveIds = [1n, 1n]; + const assets = [ethers.parseEther("5"), ethers.parseEther("5")]; + const minShares = [0n, 0n]; + + const totalDeposit = ethers.parseEther("10"); + const fee = await proxy.calculateDepositFee(2n, totalDeposit); + const totalRequired = totalDeposit + fee; + + const initialBalance = await ethers.provider.getBalance(await proxy.getAddress()); + + await expect(proxy.connect(user).depositBatch(user.address, termIds, curveIds, assets, minShares, { value: totalRequired })) + .to.emit(proxy, "FeesCollected") + .withArgs(user.address, fee, "depositBatch"); + + const finalBalance = await ethers.provider.getBalance(await proxy.getAddress()); + expect(finalBalance - initialBalance).to.equal(fee); + }); + + it("Should revert on wrong array lengths in depositBatch", async function () { + const { proxy, user } = await loadFixture(deployFixture); + + const termIds = [ethers.zeroPadValue("0x01", 32), ethers.zeroPadValue("0x02", 32)]; + const curveIds = [1n]; // Wrong length + const assets = [ethers.parseEther("5"), ethers.parseEther("5")]; + const minShares = [0n, 0n]; + + await expect( + proxy.connect(user).depositBatch(user.address, termIds, curveIds, assets, minShares, { value: ethers.parseEther("20") }) + ).to.be.revertedWithCustomError(proxy, "IntuitionFeeProxy_WrongArrayLengths"); + }); + }); + + describe("View Functions (Passthrough)", function () { + it("Should return atom cost from MultiVault", async function () { + const { proxy, mockMultiVault } = await loadFixture(deployFixture); + expect(await proxy.getAtomCost()).to.equal(await mockMultiVault.getAtomCost()); + }); + + it("Should return triple cost from MultiVault", async function () { + const { proxy, mockMultiVault } = await loadFixture(deployFixture); + expect(await proxy.getTripleCost()).to.equal(await mockMultiVault.getTripleCost()); + }); + + it("Should return isTermCreated from MultiVault", async function () { + const { proxy, mockMultiVault } = await loadFixture(deployFixture); + const termId = ethers.zeroPadValue("0x01", 32); + await mockMultiVault.setTermCreated(termId, true); + expect(await proxy.isTermCreated(termId)).to.be.true; + }); + + it("Should return shares from MultiVault", async function () { + const { proxy, mockMultiVault, user } = await loadFixture(deployFixture); + const termId = ethers.zeroPadValue("0x01", 32); + await mockMultiVault.setShares(user.address, termId, 1n, 1000n); + expect(await proxy.getShares(user.address, termId, 1n)).to.equal(1000n); + }); + }); +});