diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..8d10a6b --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,8 @@ +# Project: Factoring Contracts + +## Build & Test + +- Always use `--via-ir` flag when running forge commands (build, test, etc.) +- Use `make test` / `make build` which set `FOUNDRY_PROFILE=test` automatically (lower optimizer_runs to avoid "Tag too large" errors) +- Default profile uses `optimizer_runs = 200` for production deployments +- Windows environment: do not use `VAR=value command` syntax in shell commands — it's not cross-platform diff --git a/Makefile b/Makefile index d9f5aeb..8983f5f 100644 --- a/Makefile +++ b/Makefile @@ -15,10 +15,10 @@ SEPOLIA_RPC_URL := ${SEPOLIA_RPC_URL} # ex: 0x316..FB5 "Name" 10 constructor-args := -build :; forge build --via-ir +build :; forge build --via-ir --skip test sizes :; forge build --via-ir --sizes -test :; forge test -vv --via-ir --no-match-path "**/Invariant.t.sol" $(ARGS) -test_invariant :; forge test -vvv --via-ir --match-path "**/Invariant.t.sol" $(ARGS) +test :; forge test -vv --via-ir --no-match-path "**/Invariant.t.sol" --optimizer-runs 10 $(ARGS) +test_invariant :; forge test -vvv --via-ir --match-path "**/Invariant.t.sol" --optimizer-runs 10 $(ARGS) test-s :; forge test --match-test "testFuzz_OfferLoanNeverFailsNorGeneratesKickback" -vv --via-ir trace :; forge test -vvvv --via-ir coverage :; forge coverage -vv --ir-minimum --report lcov \ No newline at end of file diff --git a/contracts/BullaFactoring.sol b/contracts/BullaFactoring.sol index e133727..821e062 100644 --- a/contracts/BullaFactoring.sol +++ b/contracts/BullaFactoring.sol @@ -142,6 +142,7 @@ contract BullaFactoringV2_2 is IBullaFactoringV2_2, ERC20, ERC4626, Ownable { error InvoiceNotPaid(); error InvoiceNotImpaired(); error CallerNotInsurer(); + error InvalidReceiverAddressIndex(); error InvoiceAlreadyImpaired(); error ImpairmentPriceTooLow(); error InsufficientInsuranceFunds(uint256 available, uint256 required); @@ -349,18 +350,22 @@ contract BullaFactoringV2_2 is IBullaFactoringV2_2, ERC20, ERC4626, Ownable { emit InvoiceFunded(loanId, pendingLoanOffer.principalAmount, address(this), block.timestamp + pendingLoanOffer.termLength, pendingLoanOffer.feeParams.upfrontBps, 0, address(0)); } - /// @notice Approves an invoice for funding, can only be called by the underwriter - /// @param invoiceId The ID of the invoice to approve - /// @param _targetYieldBps The target yield in basis points - /// @param _spreadBps The spread in basis points to add on top of target yield - /// @param _upfrontBps The maximum upfront percentage the factorer can request - /// @param _initialInvoiceValueOverride The initial invoice value to override the invoice amount. For example in cases of loans or bonds. - function approveInvoice(uint256 invoiceId, uint16 _targetYieldBps, uint16 _spreadBps, uint16 _upfrontBps, uint256 _initialInvoiceValueOverride) external { - if (_upfrontBps <= 0 || _upfrontBps > 10000) revert InvalidPercentage(); + /// @notice Approves multiple invoices for funding in a single transaction, can only be called by the underwriter + /// @param params Array of ApproveInvoiceParams structs + function approveInvoices(ApproveInvoiceParams[] calldata params) external { if (msg.sender != underwriter) revert CallerNotUnderwriter(); + for (uint256 i = 0; i < params.length; i++) { + _approveInvoice(params[i]); + } + } + + /// @notice Internal function to approve a single invoice for funding + /// @param params The approval parameters + function _approveInvoice(ApproveInvoiceParams calldata params) internal { + if (params.upfrontBps <= 0 || params.upfrontBps > 10000) revert InvalidPercentage(); uint256 _validUntil = block.timestamp + approvalDuration; - invoiceProviderAdapter.initializeInvoice(invoiceId); - IInvoiceProviderAdapterV2.Invoice memory invoiceSnapshot = invoiceProviderAdapter.getInvoiceDetails(invoiceId); + invoiceProviderAdapter.initializeInvoice(params.invoiceId); + IInvoiceProviderAdapterV2.Invoice memory invoiceSnapshot = invoiceProviderAdapter.getInvoiceDetails(params.invoiceId); if (invoiceSnapshot.isPaid) revert InvoiceAlreadyPaid(); if (invoiceSnapshot.invoiceAmount - invoiceSnapshot.paidAmount == 0) revert InvoiceCannotBePaid(); // if invoice already got approved and funded (creditor/owner of invoice is this contract), do not override storage @@ -370,16 +375,16 @@ contract BullaFactoringV2_2 is IBullaFactoringV2_2, ERC20, ERC4626, Ownable { if (invoiceSnapshot.tokenAddress != address(assetAddress)) revert InvoiceTokenMismatch(); FeeParams memory feeParams = FeeParams({ - targetYieldBps: _targetYieldBps, - spreadBps: _spreadBps, - upfrontBps: _upfrontBps, + targetYieldBps: params.targetYieldBps, + spreadBps: params.spreadBps, + upfrontBps: params.upfrontBps, protocolFeeBps: protocolFeeBps, adminFeeBps: adminFeeBps }); - uint256 _initialInvoiceValue = _initialInvoiceValueOverride != 0 ? _initialInvoiceValueOverride : invoiceSnapshot.invoiceAmount - invoiceSnapshot.paidAmount; - - approvedInvoices[invoiceId] = InvoiceApproval({ + uint256 _initialInvoiceValue = params.initialInvoiceValueOverride != 0 ? params.initialInvoiceValueOverride : invoiceSnapshot.invoiceAmount - invoiceSnapshot.paidAmount; + + approvedInvoices[params.invoiceId] = InvoiceApproval({ approved: true, validUntil: _validUntil, creditor: invoiceSnapshot.creditor, @@ -393,9 +398,9 @@ contract BullaFactoringV2_2 is IBullaFactoringV2_2, ERC20, ERC4626, Ownable { invoiceDueDate: invoiceSnapshot.dueDate, impairmentDate: invoiceSnapshot.dueDate + invoiceSnapshot.impairmentGracePeriod, protocolFee: 0, - perSecondInterestRateRay: FeeCalculations.calculatePerSecondInterestRateRay(_initialInvoiceValue, _targetYieldBps) + perSecondInterestRateRay: FeeCalculations.calculatePerSecondInterestRateRay(_initialInvoiceValue, params.targetYieldBps) }); - emit InvoiceApproved(invoiceId, _validUntil, feeParams); + emit InvoiceApproved(params.invoiceId, _validUntil, feeParams); } @@ -526,75 +531,114 @@ contract BullaFactoringV2_2 is IBullaFactoringV2_2, ERC20, ERC4626, Ownable { if (!success) revert InvoiceSetPaidCallbackFailed(); } - /// @notice Funds a single invoice, transferring the funded amount from the fund to the caller and transferring the invoice NFT to the fund + /// @notice Funds multiple invoices in a single transaction /// @dev No checks needed for the creditor, as transferFrom will revert unless it gets executed by the nft owner (i.e. claim creditor) - /// @param invoiceId The ID of the invoice to fund - /// @param factorerUpfrontBps factorer specified upfront bps - /// @param receiverAddress Address to receive the funds, if address(0) then funds go to msg.sender - function fundInvoice(uint256 invoiceId, uint16 factorerUpfrontBps, address receiverAddress) external returns(uint256) { + /// @param params Array of FundInvoiceParams structs + /// @param receiverAddresses Array of receiver addresses; each invoice references one by index. address(0) means msg.sender. Reverts if index is out of bounds. + /// @return fundedAmounts Array of net funded amounts for each invoice + function fundInvoices(FundInvoiceParams[] calldata params, address[] calldata receiverAddresses) external returns(uint256[] memory fundedAmounts) { if (!factoringPermissions.isAllowed(msg.sender)) revert UnauthorizedFactoring(msg.sender); if (!redemptionQueue.isQueueEmpty()) revert RedemptionQueueNotEmpty(); - - // Cache approvedInvoices in memory to reduce storage reads - IBullaFactoringV2_2.InvoiceApproval memory approval = approvedInvoices[invoiceId]; - + + _checkpointAccruedProfits(); + + fundedAmounts = new uint256[](params.length); + uint256[] memory receiverAmounts = new uint256[](receiverAddresses.length); + + // Accumulate per-invoice results + uint256[5] memory totals; // [protocolFee, insurancePremium, fundedGross, withheldFees, perSecondRate] + + for (uint256 i = 0; i < params.length; i++) { + uint256 receiverIdx = params[i].receiverAddressIndex; + ( + uint256 fundedAmountGross, + uint256 fundedAmountNet, + uint256 pFee, + uint256 iPremium, + uint256 perSecondRate + ) = _fundInvoice(params[i], receiverAddresses); + + fundedAmounts[i] = fundedAmountNet; + totals[0] += pFee; + totals[1] += iPremium; + totals[2] += fundedAmountGross; + totals[3] += fundedAmountGross - fundedAmountNet; + totals[4] += perSecondRate; + receiverAmounts[receiverIdx] += fundedAmountNet; + } + + // Single liquidity check for the entire batch + { + uint256 _totalAssets = totalAssets(); + if (totals[2] > _totalAssets) revert InsufficientFunds(_totalAssets, totals[2]); + } + + // Batch state updates (single SSTORE per variable) + protocolFeeBalance += totals[0]; + insuranceBalance += totals[1]; + capitalAtRiskPlusWithheldFees += totals[2]; + withheldFees += totals[3]; + totalPerSecondInterestRateRay += totals[4]; + + // Batch transfers by receiver (address(0) slots go to msg.sender) + uint256 msgSenderTotal = 0; + for (uint256 i = 0; i < receiverAddresses.length; i++) { + if (receiverAmounts[i] > 0) { + if (receiverAddresses[i] == address(0)) { + msgSenderTotal += receiverAmounts[i]; + } else { + assetAddress.safeTransfer(receiverAddresses[i], receiverAmounts[i]); + } + } + } + if (msgSenderTotal > 0) { + assetAddress.safeTransfer(msg.sender, msgSenderTotal); + } + return fundedAmounts; + } + + /// @notice Internal function to process a single invoice within a batch — validates, calculates fees, + /// updates per-invoice storage, transfers NFT, but does NOT transfer funds or update aggregate state. + /// @return fundedAmountGross, fundedAmountNet, protocolFee, insurancePremium, perSecondInterestRateRay + function _fundInvoice( + FundInvoiceParams calldata params, + address[] calldata receiverAddresses + ) internal returns (uint256, uint256, uint256, uint256, uint256) { + IBullaFactoringV2_2.InvoiceApproval memory approval = approvedInvoices[params.invoiceId]; + if (!approval.approved) revert InvoiceNotApproved(); - if (factorerUpfrontBps > approval.feeParams.upfrontBps || factorerUpfrontBps == 0) revert InvalidPercentage(); + if (params.factorerUpfrontBps > approval.feeParams.upfrontBps || params.factorerUpfrontBps == 0) revert InvalidPercentage(); if (block.timestamp > approval.validUntil) revert ApprovalExpired(); - IInvoiceProviderAdapterV2.Invoice memory invoicesDetails = invoiceProviderAdapter.getInvoiceDetails(invoiceId); + IInvoiceProviderAdapterV2.Invoice memory invoicesDetails = invoiceProviderAdapter.getInvoiceDetails(params.invoiceId); if (invoicesDetails.isCanceled) revert InvoiceCanceled(); if (invoicesDetails.isPaid) revert InvoiceAlreadyPaid(); if (approval.initialPaidAmount != invoicesDetails.paidAmount) revert InvoicePaidAmountChanged(); if (approval.creditor != invoicesDetails.creditor) revert InvoiceCreditorChanged(); - (uint256 fundedAmountGross, , , , uint256 protocolFee, uint256 insurancePremium, uint256 fundedAmountNet) = FeeCalculations.calculateTargetFees(approval, invoicesDetails, factorerUpfrontBps, protocolFeeBps, insuranceFeeBps); - - // Realize protocol fee immediately at funding time - protocolFeeBalance += protocolFee; + (uint256 fundedAmountGross, , , , uint256 protocolFee, uint256 insurancePremium, uint256 fundedAmountNet) = FeeCalculations.calculateTargetFees(approval, invoicesDetails, params.factorerUpfrontBps, protocolFeeBps, insuranceFeeBps); - // Realize insurance premium at funding time - insuranceBalance += insurancePremium; - - uint256 _totalAssets = totalAssets(); - // needs to be gross amount here, because the fees will be locked, and we need liquidity to lock these - if(fundedAmountGross > _totalAssets) revert InsufficientFunds(_totalAssets, fundedAmountGross); - - // Update memory struct + // Update per-invoice approval struct approval.fundedAmountGross = fundedAmountGross; approval.fundedAmountNet = fundedAmountNet; approval.fundedTimestamp = block.timestamp; - // update upfrontBps with what was passed in the arg by the factorer - approval.feeParams.upfrontBps = factorerUpfrontBps; + approval.feeParams.upfrontBps = params.factorerUpfrontBps; approval.protocolFee = protocolFee; - // Determine the actual receiver address - use msg.sender if receiverAddress is address(0) - address actualReceiver = receiverAddress == address(0) ? msg.sender : receiverAddress; + if (params.receiverAddressIndex >= receiverAddresses.length) revert InvalidReceiverAddressIndex(); + address actualReceiver = receiverAddresses[params.receiverAddressIndex] == address(0) ? msg.sender : receiverAddresses[params.receiverAddressIndex]; - // Store the receiver address for future kickback payments approval.receiverAddress = actualReceiver; + approvedInvoices[params.invoiceId] = approval; - // Write back to storage once - approvedInvoices[invoiceId] = approval; - - // transfer net funded amount to caller to the actual receiver - assetAddress.safeTransfer(actualReceiver, fundedAmountNet); - - IERC721(invoiceProviderAdapter.getInvoiceContractAddress(invoiceId)).transferFrom(msg.sender, address(this), invoiceId); - - originalCreditors[invoiceId] = msg.sender; - _activeInvoices.add(invoiceId); - - // Add invoice to aggregate state tracking (RAY units) - _addInvoiceToAggregate(approval.perSecondInterestRateRay); - - // Add to capital at risk and withheld fees - capitalAtRiskPlusWithheldFees += fundedAmountGross; - withheldFees += fundedAmountGross - fundedAmountNet; + // Per-invoice operations: NFT transfer, tracking, callback + IERC721(invoiceProviderAdapter.getInvoiceContractAddress(params.invoiceId)).transferFrom(msg.sender, address(this), params.invoiceId); + originalCreditors[params.invoiceId] = msg.sender; + _activeInvoices.add(params.invoiceId); + _registerInvoiceCallback(params.invoiceId); - _registerInvoiceCallback(invoiceId); + emit InvoiceFunded(params.invoiceId, fundedAmountNet, msg.sender, approval.invoiceDueDate, params.factorerUpfrontBps, protocolFee, actualReceiver); - emit InvoiceFunded(invoiceId, fundedAmountNet, msg.sender, approval.invoiceDueDate, factorerUpfrontBps, protocolFee, actualReceiver); - return fundedAmountNet; + return (fundedAmountGross, fundedAmountNet, protocolFee, insurancePremium, approval.perSecondInterestRateRay); } function getActiveInvoices() external view returns (uint256[] memory) { diff --git a/contracts/interfaces/IBullaFactoring.sol b/contracts/interfaces/IBullaFactoring.sol index 309e248..671e223 100644 --- a/contracts/interfaces/IBullaFactoring.sol +++ b/contracts/interfaces/IBullaFactoring.sol @@ -65,6 +65,20 @@ interface IBullaFactoringV2_2 { uint256 paidAmountAtImpairment; } + struct ApproveInvoiceParams { + uint256 invoiceId; + uint16 targetYieldBps; + uint16 spreadBps; + uint16 upfrontBps; + uint256 initialInvoiceValueOverride; + } + + struct FundInvoiceParams { + uint256 invoiceId; + uint16 factorerUpfrontBps; + uint8 receiverAddressIndex; + } + // Events event InvoiceApproved(uint256 indexed invoiceId, uint256 validUntil, FeeParams feeParams); event InvoiceFunded(uint256 indexed invoiceId, uint256 fundedAmount, address indexed originalCreditor, uint256 dueDate, uint16 upfrontBps, uint256 protocolFee, address fundsReceiver); @@ -96,9 +110,9 @@ interface IBullaFactoringV2_2 { event ImpairedInvoiceReconciled(uint256 indexed invoiceId, uint256 amountRecovered, uint256 insuranceShare, uint256 investorShare); // Functions - function approveInvoice(uint256 invoiceId, uint16 _interestApr, uint16 _spreadBps, uint16 _upfrontBps, uint256 _principalAmountOverride) external; + function approveInvoices(ApproveInvoiceParams[] calldata params) external; function pricePerShare() external view returns (uint256); - function fundInvoice(uint256 invoiceId, uint16 factorerUpfrontBps, address receiverAddress) external returns (uint256); + function fundInvoices(FundInvoiceParams[] calldata params, address[] calldata receiverAddresses) external returns (uint256[] memory); function viewPoolStatus(uint256 offset, uint256 limit) external view returns (uint256[] memory impairedInvoiceIds, bool hasMore); function reconcileSingleInvoice(uint256 invoiceId) external; function setGracePeriodDays(uint256 _days) external; diff --git a/foundry.toml b/foundry.toml index f2fa55d..8f79eff 100644 --- a/foundry.toml +++ b/foundry.toml @@ -4,7 +4,7 @@ out = 'artifacts/foundry' libs = ['lib', 'node_modules'] solc = "0.8.30" via_ir = true -optimize = true +optimizer = true optimizer_runs = 200 remappings = [ 'contracts/=contracts/', @@ -21,6 +21,9 @@ remappings = [ 'solmate/=lib/solmate/src' ] +[profile.test] +optimizer_runs = 10 + # See more config options https://github.com/foundry-rs/foundry/tree/master/config # Etherscan V2 API configuration diff --git a/test/foundry/CommonSetup.t.sol b/test/foundry/CommonSetup.t.sol index 6c027a8..a9cd3b5 100644 --- a/test/foundry/CommonSetup.t.sol +++ b/test/foundry/CommonSetup.t.sol @@ -15,6 +15,7 @@ import "../../contracts/interfaces/IInvoiceProviderAdapter.sol"; import {Math} from "@openzeppelin/contracts/utils/math/Math.sol"; import "@openzeppelin/contracts/token/ERC721/IERC721.sol"; import "contracts/interfaces/IBullaFactoring.sol"; +import './helpers/TestHelpers.sol'; import {IBullaClaimV2, LockState} from "bulla-contracts-v2/src/interfaces/IBullaClaimV2.sol"; import {IBullaFrendLendV2} from "bulla-contracts-v2/src/interfaces/IBullaFrendLendV2.sol"; import {BullaFrendLendV2} from "bulla-contracts-v2/src/BullaFrendLendV2.sol"; @@ -26,7 +27,7 @@ import {BullaApprovalRegistry} from "bulla-contracts-v2/src/BullaApprovalRegistr import {CreateClaimParams, ClaimBinding} from "bulla-contracts-v2/src/types/Types.sol"; import {CreateInvoiceParams, InterestConfig} from "bulla-contracts-v2/src/interfaces/IBullaInvoice.sol"; -contract CommonSetup is Test { +contract CommonSetup is Test, BatchTestHelpers { BullaFactoringV2_2 public bullaFactoring; BullaClaimV2InvoiceProviderAdapterV2 public invoiceAdapterBulla; MockUSDC public asset; @@ -121,6 +122,10 @@ contract CommonSetup is Test { vm.stopPrank(); } + function _factoringContract() internal view override returns (IBullaFactoringV2_2) { + return bullaFactoring; + } + function permitUser(address user, bool canFactor, uint256 fundingAmount) internal { depositPermissions.allow(user); redeemPermissions.allow(user); @@ -199,4 +204,5 @@ contract CommonSetup is Test { } return (0, 0); } + } diff --git a/test/foundry/TestActivePaidInvoicesCheck.t.sol b/test/foundry/TestActivePaidInvoicesCheck.t.sol index 10e2d98..7dd3c01 100644 --- a/test/foundry/TestActivePaidInvoicesCheck.t.sol +++ b/test/foundry/TestActivePaidInvoicesCheck.t.sol @@ -50,12 +50,12 @@ contract TestActivePaidInvoicesCheck is CommonSetup { // Fund invoice to reduce liquidity vm.startPrank(underwriter); - bullaFactoring.approveInvoice(invoiceId, 730, 1000, 8000, 0); + _approveInvoice(invoiceId, 730, 1000, 8000, 0); vm.stopPrank(); vm.startPrank(bob); bullaClaim.approve(address(bullaFactoring), invoiceId); - bullaFactoring.fundInvoice(invoiceId, upfrontBps, address(0)); + _fundInvoice(invoiceId, upfrontBps, address(0)); vm.stopPrank(); // Queue a large redemption @@ -92,12 +92,12 @@ contract TestActivePaidInvoicesCheck is CommonSetup { vm.stopPrank(); vm.startPrank(underwriter); - bullaFactoring.approveInvoice(invoiceId, 730, 1000, 8000, 0); + _approveInvoice(invoiceId, 730, 1000, 8000, 0); vm.stopPrank(); vm.startPrank(bob); bullaClaim.approve(address(bullaFactoring), invoiceId); - bullaFactoring.fundInvoice(invoiceId, upfrontBps, address(0)); + _fundInvoice(invoiceId, upfrontBps, address(0)); vm.stopPrank(); // Pay the invoice diff --git a/test/foundry/TestAdvancedBullaInvoiceFactoring.t.sol b/test/foundry/TestAdvancedBullaInvoiceFactoring.t.sol index 6b8fb00..35ac9e4 100644 --- a/test/foundry/TestAdvancedBullaInvoiceFactoring.t.sol +++ b/test/foundry/TestAdvancedBullaInvoiceFactoring.t.sol @@ -64,12 +64,12 @@ contract TestAdvancedBullaInvoiceFactoring is CommonSetup { vm.stopPrank(); vm.startPrank(underwriter); - bullaFactoring.approveInvoice(invoiceId, interestApr, spreadBps, upfrontBps, 0); + _approveInvoice(invoiceId, interestApr, spreadBps, upfrontBps, 0); vm.stopPrank(); vm.startPrank(bob); IERC721(address(bullaInvoice)).approve(address(bullaFactoring), invoiceId); - bullaFactoring.fundInvoice(invoiceId, upfrontBps, address(0)); + _fundInvoice(invoiceId, upfrontBps, address(0)); vm.stopPrank(); // Test at different payment times and verify interest calculations @@ -126,12 +126,12 @@ contract TestAdvancedBullaInvoiceFactoring is CommonSetup { vm.stopPrank(); vm.startPrank(underwriter); - bullaFactoring.approveInvoice(invoiceIds[i], interestApr, spreadBps, upfrontBps, 0); + _approveInvoice(invoiceIds[i], interestApr, spreadBps, upfrontBps, 0); vm.stopPrank(); vm.startPrank(bob); IERC721(address(bullaInvoice)).approve(address(bullaFactoring), invoiceIds[i]); - bullaFactoring.fundInvoice(invoiceIds[i], upfrontBps, address(0)); + _fundInvoice(invoiceIds[i], upfrontBps, address(0)); vm.stopPrank(); } @@ -174,7 +174,7 @@ contract TestAdvancedBullaInvoiceFactoring is CommonSetup { vm.stopPrank(); vm.startPrank(underwriter); - bullaFactoring.approveInvoice(invoiceId, interestApr, spreadBps, upfrontBps, 0); + _approveInvoice(invoiceId, interestApr, spreadBps, upfrontBps, 0); vm.stopPrank(); // Try to fund invoice when insufficient capital @@ -187,7 +187,7 @@ contract TestAdvancedBullaInvoiceFactoring is CommonSetup { // fundedAmountGross now includes protocol fee // Should revert due to insufficient funds vm.expectRevert(abi.encodeWithSelector(BullaFactoringV2_2.InsufficientFunds.selector, initialDeposit, fundedAmountGross)); - bullaFactoring.fundInvoice(invoiceId, upfrontBps, address(0)); + _fundInvoiceExpectRevert(invoiceId, upfrontBps, address(0)); vm.stopPrank(); } @@ -208,19 +208,19 @@ contract TestAdvancedBullaInvoiceFactoring is CommonSetup { vm.stopPrank(); vm.startPrank(underwriter); - bullaFactoring.approveInvoice(invoiceId, interestApr, spreadBps, upfrontBps, 0); + _approveInvoice(invoiceId, interestApr, spreadBps, upfrontBps, 0); vm.stopPrank(); // Fund invoice first time vm.startPrank(bob); IERC721(address(bullaInvoice)).approve(address(bullaFactoring), invoiceId); - bullaFactoring.fundInvoice(invoiceId, upfrontBps, address(0)); + _fundInvoice(invoiceId, upfrontBps, address(0)); vm.stopPrank(); // Try to fund again (should fail) vm.startPrank(bob); vm.expectRevert(); // Should revert because invoice already owned by factoring contract - bullaFactoring.fundInvoice(invoiceId, upfrontBps, address(0)); + _fundInvoiceExpectRevert(invoiceId, upfrontBps, address(0)); vm.stopPrank(); } @@ -240,12 +240,12 @@ contract TestAdvancedBullaInvoiceFactoring is CommonSetup { vm.stopPrank(); vm.startPrank(underwriter); - bullaFactoring.approveInvoice(invoiceId, 100_00, spreadBps, upfrontBps, 0); + _approveInvoice(invoiceId, 100_00, spreadBps, upfrontBps, 0); vm.stopPrank(); vm.startPrank(bob); IERC721(address(bullaInvoice)).approve(address(bullaFactoring), invoiceId); - bullaFactoring.fundInvoice(invoiceId, upfrontBps, address(0)); + _fundInvoice(invoiceId, upfrontBps, address(0)); vm.stopPrank(); // Check approval and validate per-second interest rate is not 0 diff --git a/test/foundry/TestAutomaticRedemptionQueueProcessing.t.sol b/test/foundry/TestAutomaticRedemptionQueueProcessing.t.sol index 91e6898..bda1a98 100644 --- a/test/foundry/TestAutomaticRedemptionQueueProcessing.t.sol +++ b/test/foundry/TestAutomaticRedemptionQueueProcessing.t.sol @@ -53,11 +53,11 @@ contract TestAutomaticRedemptionQueueProcessing is CommonSetup { invoiceId = createClaim(bob, alice, 90000, dueBy); vm.prank(underwriter); - bullaFactoring.approveInvoice(invoiceId, interestApr, spreadBps, upfrontBps, 0); + _approveInvoice(invoiceId, interestApr, spreadBps, upfrontBps, 0); vm.startPrank(bob); bullaClaim.approve(address(bullaFactoring), invoiceId); - bullaFactoring.fundInvoice(invoiceId, upfrontBps, address(0)); + _fundInvoice(invoiceId, upfrontBps, address(0)); vm.stopPrank(); // 3. Alice tries to redeem all shares - should queue @@ -111,11 +111,11 @@ contract TestAutomaticRedemptionQueueProcessing is CommonSetup { uint256 invoiceId = createClaim(charlie, alice, 180000, dueBy); vm.prank(underwriter); - bullaFactoring.approveInvoice(invoiceId, interestApr, spreadBps, 10000, 0); + _approveInvoice(invoiceId, interestApr, spreadBps, 10000, 0); vm.startPrank(charlie); bullaClaim.approve(address(bullaFactoring), invoiceId); - bullaFactoring.fundInvoice(invoiceId, upfrontBps, address(0)); + _fundInvoice(invoiceId, upfrontBps, address(0)); vm.stopPrank(); // 3. Both Alice and Bob queue redemptions @@ -202,11 +202,11 @@ contract TestAutomaticRedemptionQueueProcessing is CommonSetup { uint256 invoiceId = createClaim(bob, alice, 90000, dueBy); vm.prank(underwriter); - bullaFactoring.approveInvoice(invoiceId, interestApr, spreadBps, upfrontBps, 0); + _approveInvoice(invoiceId, interestApr, spreadBps, upfrontBps, 0); vm.startPrank(bob); bullaClaim.approve(address(bullaFactoring), invoiceId); - bullaFactoring.fundInvoice(invoiceId, upfrontBps, address(0)); + _fundInvoice(invoiceId, upfrontBps, address(0)); vm.stopPrank(); // Both investors queue redemptions diff --git a/test/foundry/TestBatchOperations.t.sol b/test/foundry/TestBatchOperations.t.sol new file mode 100644 index 0000000..99f2ad8 --- /dev/null +++ b/test/foundry/TestBatchOperations.t.sol @@ -0,0 +1,486 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import "./CommonSetup.t.sol"; +import {IBullaFactoringV2_2} from "../../contracts/interfaces/IBullaFactoring.sol"; + +contract TestBatchOperations is CommonSetup { + + // ============================================ + // Helpers + // ============================================ + + /// @dev Creates 3 invoices from bob (creditor) to alice (debtor) with default params + function _create3Invoices(uint256 invoiceAmount) internal returns (uint256 id1, uint256 id2, uint256 id3) { + vm.startPrank(bob); + id1 = createClaim(bob, alice, invoiceAmount, dueBy); + id2 = createClaim(bob, alice, invoiceAmount, dueBy); + id3 = createClaim(bob, alice, invoiceAmount, dueBy); + vm.stopPrank(); + } + + /// @dev Batch-approves 3 invoices via the batch interface + function _batchApprove3(uint256 id1, uint256 id2, uint256 id3) internal { + IBullaFactoringV2_2.ApproveInvoiceParams[] memory params = new IBullaFactoringV2_2.ApproveInvoiceParams[](3); + params[0] = IBullaFactoringV2_2.ApproveInvoiceParams({ + invoiceId: id1, + targetYieldBps: targetYield, + spreadBps: spreadBps, + upfrontBps: upfrontBps, + initialInvoiceValueOverride: 0 + }); + params[1] = IBullaFactoringV2_2.ApproveInvoiceParams({ + invoiceId: id2, + targetYieldBps: targetYield, + spreadBps: spreadBps, + upfrontBps: upfrontBps, + initialInvoiceValueOverride: 0 + }); + params[2] = IBullaFactoringV2_2.ApproveInvoiceParams({ + invoiceId: id3, + targetYieldBps: targetYield, + spreadBps: spreadBps, + upfrontBps: upfrontBps, + initialInvoiceValueOverride: 0 + }); + + vm.prank(underwriter); + bullaFactoring.approveInvoices(params); + } + + /// @dev Batch-funds 3 invoices via the batch interface, all pointing to receiver index 0 + function _batchFund3(uint256 id1, uint256 id2, uint256 id3, address receiver) internal returns (uint256[] memory) { + IBullaFactoringV2_2.FundInvoiceParams[] memory params = new IBullaFactoringV2_2.FundInvoiceParams[](3); + address[] memory receivers = new address[](1); + receivers[0] = receiver; + + params[0] = IBullaFactoringV2_2.FundInvoiceParams({ + invoiceId: id1, + factorerUpfrontBps: upfrontBps, + receiverAddressIndex: 0 + }); + params[1] = IBullaFactoringV2_2.FundInvoiceParams({ + invoiceId: id2, + factorerUpfrontBps: upfrontBps, + receiverAddressIndex: 0 + }); + params[2] = IBullaFactoringV2_2.FundInvoiceParams({ + invoiceId: id3, + factorerUpfrontBps: upfrontBps, + receiverAddressIndex: 0 + }); + + vm.startPrank(bob); + bullaClaim.approve(address(bullaFactoring), id1); + bullaClaim.approve(address(bullaFactoring), id2); + bullaClaim.approve(address(bullaFactoring), id3); + uint256[] memory amounts = bullaFactoring.fundInvoices(params, receivers); + vm.stopPrank(); + + return amounts; + } + + /// @dev Helper to check if an invoice is approved by trying to fund it and verifying calculateTargetFees succeeds + function _isApproved(uint256 invoiceId) internal view returns (bool) { + (bool approved, , , , , , , , , , , , , ) = bullaFactoring.approvedInvoices(invoiceId); + return approved; + } + + /// @dev Helper to get the initialInvoiceValue from the auto-generated getter + function _getInitialInvoiceValue(uint256 invoiceId) internal view returns (uint256) { + (, , , , , , , , uint256 initialInvoiceValue, , , , , ) = bullaFactoring.approvedInvoices(invoiceId); + return initialInvoiceValue; + } + + /// @dev Helper to get fundedAmountNet from the auto-generated getter + function _getFundedAmountNet(uint256 invoiceId) internal view returns (uint256) { + (, , , , , , , uint256 fundedAmountNet, , , , , , ) = bullaFactoring.approvedInvoices(invoiceId); + return fundedAmountNet; + } + + /// @dev Helper to get receiverAddress from the auto-generated getter + function _getReceiverAddress(uint256 invoiceId) internal view returns (address) { + (, , , , , , , , , , , address receiverAddress, , ) = bullaFactoring.approvedInvoices(invoiceId); + return receiverAddress; + } + + // ============================================ + // A. BATCH APPROVE TESTS + // ============================================ + + function test_approveInvoices_batchMultipleInvoices() public { + uint256 invoiceAmount = 100000; + (uint256 id1, uint256 id2, uint256 id3) = _create3Invoices(invoiceAmount); + + _batchApprove3(id1, id2, id3); + + // Verify all 3 invoices are approved + assertTrue(_isApproved(id1), "Invoice 1 should be approved"); + assertTrue(_isApproved(id2), "Invoice 2 should be approved"); + assertTrue(_isApproved(id3), "Invoice 3 should be approved"); + + // Verify initialInvoiceValue is set (no override, so it should be full invoice amount) + assertEq(_getInitialInvoiceValue(id1), invoiceAmount, "Invoice 1 initial value"); + assertEq(_getInitialInvoiceValue(id2), invoiceAmount, "Invoice 2 initial value"); + assertEq(_getInitialInvoiceValue(id3), invoiceAmount, "Invoice 3 initial value"); + } + + function test_approveInvoices_singleItemBatch() public { + uint256 invoiceAmount = 100000; + vm.prank(bob); + uint256 invoiceId = createClaim(bob, alice, invoiceAmount, dueBy); + + IBullaFactoringV2_2.ApproveInvoiceParams[] memory params = new IBullaFactoringV2_2.ApproveInvoiceParams[](1); + params[0] = IBullaFactoringV2_2.ApproveInvoiceParams({ + invoiceId: invoiceId, + targetYieldBps: targetYield, + spreadBps: spreadBps, + upfrontBps: upfrontBps, + initialInvoiceValueOverride: 0 + }); + + vm.prank(underwriter); + bullaFactoring.approveInvoices(params); + + assertTrue(_isApproved(invoiceId), "Single invoice should be approved"); + assertEq(_getInitialInvoiceValue(invoiceId), invoiceAmount, "Initial value matches invoice amount"); + } + + function test_approveInvoices_onlyUnderwriter() public { + uint256 invoiceAmount = 100000; + vm.prank(bob); + uint256 invoiceId = createClaim(bob, alice, invoiceAmount, dueBy); + + IBullaFactoringV2_2.ApproveInvoiceParams[] memory params = new IBullaFactoringV2_2.ApproveInvoiceParams[](1); + params[0] = IBullaFactoringV2_2.ApproveInvoiceParams({ + invoiceId: invoiceId, + targetYieldBps: targetYield, + spreadBps: spreadBps, + upfrontBps: upfrontBps, + initialInvoiceValueOverride: 0 + }); + + // alice is not the underwriter + vm.prank(alice); + vm.expectRevert(abi.encodeWithSignature("CallerNotUnderwriter()")); + bullaFactoring.approveInvoices(params); + } + + // ============================================ + // B. BATCH FUND TESTS + // ============================================ + + function test_fundInvoices_batchMultipleInvoices() public { + uint256 depositAmount = 500000; + vm.prank(alice); + bullaFactoring.deposit(depositAmount, alice); + + uint256 invoiceAmount = 100000; + (uint256 id1, uint256 id2, uint256 id3) = _create3Invoices(invoiceAmount); + _batchApprove3(id1, id2, id3); + + uint256 bobBalanceBefore = asset.balanceOf(bob); + + uint256[] memory fundedAmounts = _batchFund3(id1, id2, id3, address(0)); + + // All 3 should return non-zero funded amounts + assertGt(fundedAmounts[0], 0, "Invoice 1 funded amount > 0"); + assertGt(fundedAmounts[1], 0, "Invoice 2 funded amount > 0"); + assertGt(fundedAmounts[2], 0, "Invoice 3 funded amount > 0"); + + // All 3 amounts should be the same (same invoice amount and params) + assertEq(fundedAmounts[0], fundedAmounts[1], "Invoice 1 and 2 funded amounts should match"); + assertEq(fundedAmounts[1], fundedAmounts[2], "Invoice 2 and 3 funded amounts should match"); + + // Verify all are now active + uint256[] memory activeInvoices = bullaFactoring.getActiveInvoices(); + assertEq(activeInvoices.length, 3, "Should have 3 active invoices"); + + // Verify bob received the total net funded amount (address(0) maps to msg.sender=bob) + uint256 totalFunded = fundedAmounts[0] + fundedAmounts[1] + fundedAmounts[2]; + assertEq(asset.balanceOf(bob) - bobBalanceBefore, totalFunded, "Bob should receive total net funded amount"); + + // Verify funded amounts are stored in approvals + assertGt(_getFundedAmountNet(id1), 0, "Approval 1 fundedAmountNet stored"); + assertGt(_getFundedAmountNet(id2), 0, "Approval 2 fundedAmountNet stored"); + assertGt(_getFundedAmountNet(id3), 0, "Approval 3 fundedAmountNet stored"); + } + + function test_fundInvoices_differentReceivers() public { + uint256 depositAmount = 500000; + vm.prank(alice); + bullaFactoring.deposit(depositAmount, alice); + + uint256 invoiceAmount = 100000; + (uint256 id1, uint256 id2, uint256 id3) = _create3Invoices(invoiceAmount); + _batchApprove3(id1, id2, id3); + + // Set up 2 receiver addresses: charlie at index 0, alice at index 1 + address[] memory receivers = new address[](2); + receivers[0] = charlie; + receivers[1] = alice; + + IBullaFactoringV2_2.FundInvoiceParams[] memory params = new IBullaFactoringV2_2.FundInvoiceParams[](3); + // Invoice 1 -> receiver index 0 (charlie) + params[0] = IBullaFactoringV2_2.FundInvoiceParams({ + invoiceId: id1, + factorerUpfrontBps: upfrontBps, + receiverAddressIndex: 0 + }); + // Invoice 2 -> receiver index 1 (alice) + params[1] = IBullaFactoringV2_2.FundInvoiceParams({ + invoiceId: id2, + factorerUpfrontBps: upfrontBps, + receiverAddressIndex: 1 + }); + // Invoice 3 -> receiver index 0 (charlie) + params[2] = IBullaFactoringV2_2.FundInvoiceParams({ + invoiceId: id3, + factorerUpfrontBps: upfrontBps, + receiverAddressIndex: 0 + }); + + uint256 charlieBalanceBefore = asset.balanceOf(charlie); + uint256 aliceBalanceBefore = asset.balanceOf(alice); + + vm.startPrank(bob); + bullaClaim.approve(address(bullaFactoring), id1); + bullaClaim.approve(address(bullaFactoring), id2); + bullaClaim.approve(address(bullaFactoring), id3); + uint256[] memory fundedAmounts = bullaFactoring.fundInvoices(params, receivers); + vm.stopPrank(); + + // charlie receives invoice 1 + invoice 3 + uint256 expectedCharlieReceived = fundedAmounts[0] + fundedAmounts[2]; + assertEq(asset.balanceOf(charlie) - charlieBalanceBefore, expectedCharlieReceived, "Charlie should receive funds for invoices 1 and 3"); + + // alice receives invoice 2 + uint256 expectedAliceReceived = fundedAmounts[1]; + assertEq(asset.balanceOf(alice) - aliceBalanceBefore, expectedAliceReceived, "Alice should receive funds for invoice 2"); + + // Verify receiver addresses are recorded in approvals + assertEq(_getReceiverAddress(id1), charlie, "Invoice 1 receiver should be charlie"); + assertEq(_getReceiverAddress(id2), alice, "Invoice 2 receiver should be alice"); + assertEq(_getReceiverAddress(id3), charlie, "Invoice 3 receiver should be charlie"); + } + + function test_fundInvoices_addressZeroReceiver() public { + uint256 depositAmount = 500000; + vm.prank(alice); + bullaFactoring.deposit(depositAmount, alice); + + uint256 invoiceAmount = 100000; + vm.prank(bob); + uint256 invoiceId = createClaim(bob, alice, invoiceAmount, dueBy); + + vm.prank(underwriter); + _approveInvoice(invoiceId, targetYield, spreadBps, upfrontBps, 0); + + // Use address(0) in the receiver addresses array + IBullaFactoringV2_2.FundInvoiceParams[] memory params = new IBullaFactoringV2_2.FundInvoiceParams[](1); + address[] memory receivers = new address[](1); + receivers[0] = address(0); + params[0] = IBullaFactoringV2_2.FundInvoiceParams({ + invoiceId: invoiceId, + factorerUpfrontBps: upfrontBps, + receiverAddressIndex: 0 + }); + + uint256 bobBalanceBefore = asset.balanceOf(bob); + + vm.startPrank(bob); + bullaClaim.approve(address(bullaFactoring), invoiceId); + uint256[] memory amounts = bullaFactoring.fundInvoices(params, receivers); + vm.stopPrank(); + + // address(0) maps to msg.sender (bob), so bob should receive the funds + assertEq(asset.balanceOf(bob) - bobBalanceBefore, amounts[0], "address(0) receiver should route funds to msg.sender (bob)"); + + // Verify the stored receiver is resolved to bob (msg.sender) + assertEq(_getReceiverAddress(invoiceId), bob, "Stored receiver should be bob (resolved from address(0))"); + } + + function test_fundInvoices_singleItemBatch() public { + uint256 depositAmount = 500000; + vm.prank(alice); + bullaFactoring.deposit(depositAmount, alice); + + uint256 invoiceAmount = 100000; + vm.prank(bob); + uint256 invoiceId = createClaim(bob, alice, invoiceAmount, dueBy); + + vm.prank(underwriter); + _approveInvoice(invoiceId, targetYield, spreadBps, upfrontBps, 0); + + IBullaFactoringV2_2.FundInvoiceParams[] memory params = new IBullaFactoringV2_2.FundInvoiceParams[](1); + address[] memory receivers = new address[](1); + receivers[0] = address(0); + params[0] = IBullaFactoringV2_2.FundInvoiceParams({ + invoiceId: invoiceId, + factorerUpfrontBps: upfrontBps, + receiverAddressIndex: 0 + }); + + vm.startPrank(bob); + bullaClaim.approve(address(bullaFactoring), invoiceId); + uint256[] memory amounts = bullaFactoring.fundInvoices(params, receivers); + vm.stopPrank(); + + assertEq(amounts.length, 1, "Should return exactly 1 funded amount"); + assertGt(amounts[0], 0, "Funded amount should be > 0"); + + // Verify the invoice is now active + assertEq(bullaFactoring.getActiveInvoicesCount(), 1, "Should have 1 active invoice"); + } + + function test_fundInvoices_invalidReceiverAddressIndex() public { + uint256 depositAmount = 500000; + vm.prank(alice); + bullaFactoring.deposit(depositAmount, alice); + + uint256 invoiceAmount = 100000; + vm.prank(bob); + uint256 invoiceId = createClaim(bob, alice, invoiceAmount, dueBy); + + vm.prank(underwriter); + _approveInvoice(invoiceId, targetYield, spreadBps, upfrontBps, 0); + + // receiverAddresses has 1 element (index 0), but we reference index 1 + IBullaFactoringV2_2.FundInvoiceParams[] memory params = new IBullaFactoringV2_2.FundInvoiceParams[](1); + address[] memory receivers = new address[](1); + receivers[0] = address(0); + params[0] = IBullaFactoringV2_2.FundInvoiceParams({ + invoiceId: invoiceId, + factorerUpfrontBps: upfrontBps, + receiverAddressIndex: 1 // out of bounds + }); + + vm.startPrank(bob); + bullaClaim.approve(address(bullaFactoring), invoiceId); + vm.expectRevert(abi.encodeWithSignature("InvalidReceiverAddressIndex()")); + bullaFactoring.fundInvoices(params, receivers); + vm.stopPrank(); + } + + // ============================================ + // C. FEE EQUIVALENCE: BATCH vs INDIVIDUAL + // ============================================ + + /// @dev Verifies that protocol fee, insurance premium, and net funded amounts are + /// identical whether two invoices are funded in a single batch call or + /// in two separate individual calls. + function test_fundInvoices_feesMatchIndividualFunding() public { + uint256 depositAmount = 500000; + uint256 invoiceAmount = 100000; + + vm.prank(alice); + bullaFactoring.deposit(depositAmount, alice); + + // Create 4 identical invoices (2 for individual, 2 for batch) + vm.startPrank(bob); + uint256 idA1 = createClaim(bob, alice, invoiceAmount, dueBy); + uint256 idA2 = createClaim(bob, alice, invoiceAmount, dueBy); + uint256 idB1 = createClaim(bob, alice, invoiceAmount, dueBy); + uint256 idB2 = createClaim(bob, alice, invoiceAmount, dueBy); + vm.stopPrank(); + + // Approve all 4 + vm.startPrank(underwriter); + _approveInvoice(idA1, targetYield, spreadBps, upfrontBps, 0); + _approveInvoice(idA2, targetYield, spreadBps, upfrontBps, 0); + _approveInvoice(idB1, targetYield, spreadBps, upfrontBps, 0); + _approveInvoice(idB2, targetYield, spreadBps, upfrontBps, 0); + vm.stopPrank(); + + // --- Scenario A: Fund 2 invoices individually --- + uint256 protocolFeeBefore_A = bullaFactoring.protocolFeeBalance(); + uint256 insuranceBefore_A = bullaFactoring.insuranceBalance(); + + vm.startPrank(bob); + bullaClaim.approve(address(bullaFactoring), idA1); + uint256 netA1 = _fundInvoice(idA1, upfrontBps, bob); + bullaClaim.approve(address(bullaFactoring), idA2); + uint256 netA2 = _fundInvoice(idA2, upfrontBps, bob); + vm.stopPrank(); + + uint256 protocolFeeIndividual = bullaFactoring.protocolFeeBalance() - protocolFeeBefore_A; + uint256 insuranceIndividual = bullaFactoring.insuranceBalance() - insuranceBefore_A; + + // --- Scenario B: Fund 2 invoices in a single batch --- + uint256 protocolFeeBefore_B = bullaFactoring.protocolFeeBalance(); + uint256 insuranceBefore_B = bullaFactoring.insuranceBalance(); + + IBullaFactoringV2_2.FundInvoiceParams[] memory batchParams = new IBullaFactoringV2_2.FundInvoiceParams[](2); + address[] memory receivers = new address[](1); + receivers[0] = address(0); + batchParams[0] = IBullaFactoringV2_2.FundInvoiceParams({ + invoiceId: idB1, + factorerUpfrontBps: upfrontBps, + receiverAddressIndex: 0 + }); + batchParams[1] = IBullaFactoringV2_2.FundInvoiceParams({ + invoiceId: idB2, + factorerUpfrontBps: upfrontBps, + receiverAddressIndex: 0 + }); + + vm.startPrank(bob); + bullaClaim.approve(address(bullaFactoring), idB1); + bullaClaim.approve(address(bullaFactoring), idB2); + uint256[] memory batchAmounts = bullaFactoring.fundInvoices(batchParams, receivers); + vm.stopPrank(); + + uint256 protocolFeeBatch = bullaFactoring.protocolFeeBalance() - protocolFeeBefore_B; + uint256 insuranceBatch = bullaFactoring.insuranceBalance() - insuranceBefore_B; + + // --- Assertions --- + // Net funded amounts must match + assertEq(netA1, batchAmounts[0], "Net funded amount for invoice 1: individual vs batch"); + assertEq(netA2, batchAmounts[1], "Net funded amount for invoice 2: individual vs batch"); + + // Aggregate protocol fee must match + assertEq(protocolFeeIndividual, protocolFeeBatch, "Total protocol fee: individual vs batch"); + + // Aggregate insurance fee must match + assertEq(insuranceIndividual, insuranceBatch, "Total insurance fee: individual vs batch"); + } + + function test_fundInvoices_insufficientLiquidity() public { + // Deposit only 50000 but try to fund 3 invoices of 100000 each (needs ~300000) + uint256 depositAmount = 50000; + vm.prank(alice); + bullaFactoring.deposit(depositAmount, alice); + + uint256 invoiceAmount = 100000; + (uint256 id1, uint256 id2, uint256 id3) = _create3Invoices(invoiceAmount); + _batchApprove3(id1, id2, id3); + + IBullaFactoringV2_2.FundInvoiceParams[] memory params = new IBullaFactoringV2_2.FundInvoiceParams[](3); + address[] memory receivers = new address[](1); + receivers[0] = address(0); + + params[0] = IBullaFactoringV2_2.FundInvoiceParams({ + invoiceId: id1, + factorerUpfrontBps: upfrontBps, + receiverAddressIndex: 0 + }); + params[1] = IBullaFactoringV2_2.FundInvoiceParams({ + invoiceId: id2, + factorerUpfrontBps: upfrontBps, + receiverAddressIndex: 0 + }); + params[2] = IBullaFactoringV2_2.FundInvoiceParams({ + invoiceId: id3, + factorerUpfrontBps: upfrontBps, + receiverAddressIndex: 0 + }); + + vm.startPrank(bob); + bullaClaim.approve(address(bullaFactoring), id1); + bullaClaim.approve(address(bullaFactoring), id2); + bullaClaim.approve(address(bullaFactoring), id3); + vm.expectRevert(); // InsufficientFunds(available, required) + bullaFactoring.fundInvoices(params, receivers); + vm.stopPrank(); + } +} diff --git a/test/foundry/TestBullaInvoiceFactoring.t.sol b/test/foundry/TestBullaInvoiceFactoring.t.sol index f25a36b..fe67872 100644 --- a/test/foundry/TestBullaInvoiceFactoring.t.sol +++ b/test/foundry/TestBullaInvoiceFactoring.t.sol @@ -75,7 +75,7 @@ contract TestBullaInvoiceFactoring is CommonSetup { protocolFeeBps: protocolFeeBps, adminFeeBps: adminFeeBps })); - bullaFactoring.approveInvoice(invoiceId, interestApr, spreadBps, upfrontBps, 0); + _approveInvoice(invoiceId, interestApr, spreadBps, upfrontBps, 0); vm.stopPrank(); // Verify approval @@ -100,7 +100,7 @@ contract TestBullaInvoiceFactoring is CommonSetup { vm.stopPrank(); vm.startPrank(underwriter); - bullaFactoring.approveInvoice(invoiceId, interestApr, spreadBps, upfrontBps, 0); + _approveInvoice(invoiceId, interestApr, spreadBps, upfrontBps, 0); vm.stopPrank(); // Calculate expected fees (now includes insurance premium) @@ -113,7 +113,7 @@ contract TestBullaInvoiceFactoring is CommonSetup { vm.expectEmit(true, false, false, true); emit InvoiceFunded(invoiceId, netFundedAmount, bob, _dueBy, upfrontBps, protocolFee, address(bob)); - uint256 actualFundedAmount = bullaFactoring.fundInvoice(invoiceId, upfrontBps, address(0)); + uint256 actualFundedAmount = _fundInvoice(invoiceId, upfrontBps, address(0)); vm.stopPrank(); assertEq(actualFundedAmount, netFundedAmount); @@ -142,12 +142,12 @@ contract TestBullaInvoiceFactoring is CommonSetup { vm.stopPrank(); vm.startPrank(underwriter); - bullaFactoring.approveInvoice(invoiceId, interestApr, spreadBps, upfrontBps, 0); + _approveInvoice(invoiceId, interestApr, spreadBps, upfrontBps, 0); vm.stopPrank(); vm.startPrank(bob); IERC721(address(bullaInvoice)).approve(address(bullaFactoring), invoiceId); - bullaFactoring.fundInvoice(invoiceId, upfrontBps, address(0)); + _fundInvoice(invoiceId, upfrontBps, address(0)); vm.stopPrank(); // Fast forward 45 days (before due date) @@ -190,12 +190,12 @@ contract TestBullaInvoiceFactoring is CommonSetup { vm.stopPrank(); vm.startPrank(underwriter); - bullaFactoring.approveInvoice(invoiceId, interestApr, spreadBps, upfrontBps, 0); + _approveInvoice(invoiceId, interestApr, spreadBps, upfrontBps, 0); vm.stopPrank(); vm.startPrank(bob); IERC721(address(bullaInvoice)).approve(address(bullaFactoring), invoiceId); - bullaFactoring.fundInvoice(invoiceId, upfrontBps, address(0)); + _fundInvoice(invoiceId, upfrontBps, address(0)); vm.stopPrank(); uint256 bobBalanceBefore = asset.balanceOf(bob); @@ -244,12 +244,12 @@ contract TestBullaInvoiceFactoring is CommonSetup { vm.stopPrank(); vm.startPrank(underwriter); - bullaFactoring.approveInvoice(invoiceId, interestApr, spreadBps, upfrontBps, 0); + _approveInvoice(invoiceId, interestApr, spreadBps, upfrontBps, 0); vm.stopPrank(); vm.startPrank(bob); IERC721(address(bullaInvoice)).approve(address(bullaFactoring), invoiceId); - bullaFactoring.fundInvoice(invoiceId, upfrontBps, address(0)); + _fundInvoice(invoiceId, upfrontBps, address(0)); vm.stopPrank(); // Make partial payment @@ -302,12 +302,12 @@ contract TestBullaInvoiceFactoring is CommonSetup { vm.stopPrank(); vm.startPrank(underwriter); - bullaFactoring.approveInvoice(invoiceId, interestApr, spreadBps, upfrontBps, 0); + _approveInvoice(invoiceId, interestApr, spreadBps, upfrontBps, 0); vm.stopPrank(); vm.startPrank(bob); IERC721(address(bullaInvoice)).approve(address(bullaFactoring), invoiceId); - bullaFactoring.fundInvoice(invoiceId, upfrontBps, address(0)); + _fundInvoice(invoiceId, upfrontBps, address(0)); vm.stopPrank(); uint256 bobBalanceBefore = asset.balanceOf(bob); @@ -349,18 +349,18 @@ contract TestBullaInvoiceFactoring is CommonSetup { vm.stopPrank(); vm.startPrank(underwriter); - bullaFactoring.approveInvoice(invoiceId1, interestApr, spreadBps, approvedUpfrontBps, 0); - bullaFactoring.approveInvoice(invoiceId2, interestApr, spreadBps, approvedUpfrontBps, 0); + _approveInvoice(invoiceId1, interestApr, spreadBps, approvedUpfrontBps, 0); + _approveInvoice(invoiceId2, interestApr, spreadBps, approvedUpfrontBps, 0); vm.stopPrank(); // Fund first invoice with approved upfront bps vm.startPrank(bob); IERC721(address(bullaInvoice)).approve(address(bullaFactoring), invoiceId1); - uint256 fundedAmount1 = bullaFactoring.fundInvoice(invoiceId1, approvedUpfrontBps, address(0)); + uint256 fundedAmount1 = _fundInvoice(invoiceId1, approvedUpfrontBps, address(0)); // Fund second invoice with lower upfront bps IERC721(address(bullaInvoice)).approve(address(bullaFactoring), invoiceId2); - uint256 fundedAmount2 = bullaFactoring.fundInvoice(invoiceId2, factorerChosenUpfrontBps, address(0)); + uint256 fundedAmount2 = _fundInvoice(invoiceId2, factorerChosenUpfrontBps, address(0)); vm.stopPrank(); // Verify different funded amounts @@ -404,7 +404,7 @@ contract TestBullaInvoiceFactoring is CommonSetup { // Try to approve invoice with mismatched token vm.startPrank(underwriter); vm.expectRevert(abi.encodeWithSignature("InvoiceTokenMismatch()")); - bullaFactoring.approveInvoice(invoiceId, interestApr, spreadBps, upfrontBps, 0); + _approveInvoice(invoiceId, interestApr, spreadBps, upfrontBps, 0); vm.stopPrank(); } @@ -427,18 +427,18 @@ contract TestBullaInvoiceFactoring is CommonSetup { vm.stopPrank(); vm.startPrank(underwriter); - bullaFactoring.approveInvoice(invoiceId, interestApr, spreadBps, upfrontBps, 0); + _approveInvoice(invoiceId, interestApr, spreadBps, upfrontBps, 0); vm.stopPrank(); vm.startPrank(bob); IERC721(address(bullaInvoice)).approve(address(bullaFactoring), invoiceId); - bullaFactoring.fundInvoice(invoiceId, upfrontBps, address(0)); + _fundInvoice(invoiceId, upfrontBps, address(0)); vm.stopPrank(); // Try to approve already funded invoice vm.startPrank(underwriter); vm.expectRevert(abi.encodeWithSignature("InvoiceAlreadyFunded()")); - bullaFactoring.approveInvoice(invoiceId, interestApr, spreadBps, upfrontBps, 0); + _approveInvoice(invoiceId, interestApr, spreadBps, upfrontBps, 0); vm.stopPrank(); } @@ -459,12 +459,12 @@ contract TestBullaInvoiceFactoring is CommonSetup { vm.stopPrank(); vm.startPrank(underwriter); - bullaFactoring.approveInvoice(invoiceId, interestApr, spreadBps, upfrontBps, 0); + _approveInvoice(invoiceId, interestApr, spreadBps, upfrontBps, 0); vm.stopPrank(); vm.startPrank(bob); IERC721(address(bullaInvoice)).approve(address(bullaFactoring), invoiceId); - bullaFactoring.fundInvoice(invoiceId, upfrontBps, address(0)); + _fundInvoice(invoiceId, upfrontBps, address(0)); vm.stopPrank(); // Fast forward to different payment times and verify interest calculations @@ -508,17 +508,17 @@ contract TestBullaInvoiceFactoring is CommonSetup { // Both invoices get the same factoring terms from underwriter vm.startPrank(underwriter); - bullaFactoring.approveInvoice(invoiceId1, interestApr, spreadBps, upfrontBps, 0); - bullaFactoring.approveInvoice(invoiceId2, interestApr, spreadBps, upfrontBps, 0); + _approveInvoice(invoiceId1, interestApr, spreadBps, upfrontBps, 0); + _approveInvoice(invoiceId2, interestApr, spreadBps, upfrontBps, 0); vm.stopPrank(); // Fund both invoices with same terms vm.startPrank(bob); IERC721(address(bullaInvoice)).approve(address(bullaFactoring), invoiceId1); - uint256 fundedAmount1 = bullaFactoring.fundInvoice(invoiceId1, upfrontBps, address(0)); + uint256 fundedAmount1 = _fundInvoice(invoiceId1, upfrontBps, address(0)); IERC721(address(bullaInvoice)).approve(address(bullaFactoring), invoiceId2); - uint256 fundedAmount2 = bullaFactoring.fundInvoice(invoiceId2, upfrontBps, address(0)); + uint256 fundedAmount2 = _fundInvoice(invoiceId2, upfrontBps, address(0)); vm.stopPrank(); // Verify both invoices were funded with the same amount (same principal, same terms) @@ -585,12 +585,12 @@ contract TestBullaInvoiceFactoring is CommonSetup { vm.stopPrank(); vm.startPrank(underwriter); - bullaFactoring.approveInvoice(invoiceId, interestApr, spreadBps, upfrontBps, 0); + _approveInvoice(invoiceId, interestApr, spreadBps, upfrontBps, 0); vm.stopPrank(); vm.startPrank(bob); IERC721(address(bullaInvoice)).approve(address(bullaFactoring), invoiceId); - bullaFactoring.fundInvoice(invoiceId, upfrontBps, address(0)); + _fundInvoice(invoiceId, upfrontBps, address(0)); vm.stopPrank(); // Test 1: Interest at funding time (0 seconds) should be 0 @@ -631,7 +631,7 @@ contract TestBullaInvoiceFactoring is CommonSetup { vm.stopPrank(); vm.startPrank(underwriter); - bullaFactoring.approveInvoice(invoiceId, interestApr, spreadBps, upfrontBps, 0); + _approveInvoice(invoiceId, interestApr, spreadBps, upfrontBps, 0); vm.stopPrank(); // Calculate target fees at funding time (t=0) @@ -641,7 +641,7 @@ contract TestBullaInvoiceFactoring is CommonSetup { // Fund the invoice vm.startPrank(bob); IERC721(address(bullaInvoice)).approve(address(bullaFactoring), invoiceId); - bullaFactoring.fundInvoice(invoiceId, upfrontBps, address(0)); + _fundInvoice(invoiceId, upfrontBps, address(0)); vm.stopPrank(); // Fast forward to due date 30 days, which if target fees are floored, it should be == 30 days + 1 @@ -786,12 +786,12 @@ contract TestBullaInvoiceFactoring is CommonSetup { // Fund invoice vm.startPrank(underwriter); - bullaFactoring.approveInvoice(invoiceId, interestApr, spreadBps, upfrontBps, 0); + _approveInvoice(invoiceId, interestApr, spreadBps, upfrontBps, 0); vm.stopPrank(); vm.startPrank(bob); IERC721(address(bullaInvoice)).approve(address(bullaFactoring), invoiceId); - bullaFactoring.fundInvoice(invoiceId, upfrontBps, address(0)); + _fundInvoice(invoiceId, upfrontBps, address(0)); vm.stopPrank(); // Pay invoice to make it eligible for reconciliation diff --git a/test/foundry/TestDepositAndRedemption.t.sol b/test/foundry/TestDepositAndRedemption.t.sol index 88ffd78..87660e6 100644 --- a/test/foundry/TestDepositAndRedemption.t.sol +++ b/test/foundry/TestDepositAndRedemption.t.sol @@ -72,11 +72,11 @@ contract TestDepositAndRedemption is CommonSetup { vm.prank(bob); uint256 invoiceId = createClaim(bob, alice, invoiceIdAmount, dueBy); vm.startPrank(underwriter); - bullaFactoring.approveInvoice(invoiceId, interestApr, spreadBps, upfrontBps, 0); + _approveInvoice(invoiceId, interestApr, spreadBps, upfrontBps, 0); vm.stopPrank(); vm.startPrank(bob); bullaClaim.approve(address(bullaFactoring), invoiceId); - bullaFactoring.fundInvoice(invoiceId, upfrontBps, address(0)); + _fundInvoice(invoiceId, upfrontBps, address(0)); vm.stopPrank(); // Debtor pays the invoice @@ -125,7 +125,7 @@ contract TestDepositAndRedemption is CommonSetup { vm.expectEmit(true, true, true, true); emit InvoiceApproved(invoiceId, block.timestamp + bullaFactoring.approvalDuration(), expectedFeeParams); - bullaFactoring.approveInvoice(invoiceId, interestApr, spreadBps, upfrontBps, 0); + _approveInvoice(invoiceId, interestApr, spreadBps, upfrontBps, 0); vm.stopPrank(); // creditor funds the invoice @@ -137,7 +137,7 @@ contract TestDepositAndRedemption is CommonSetup { vm.expectEmit(true, true, true, true); emit InvoiceFunded(invoiceId, expectedFundedAmount, bob, dueBy, upfrontBps, 250, address(bob)); - bullaFactoring.fundInvoice(invoiceId, upfrontBps, address(0)); + _fundInvoice(invoiceId, upfrontBps, address(0)); vm.stopPrank(); // Simulate debtor paying in 30 days instead of 60 @@ -191,13 +191,13 @@ contract TestDepositAndRedemption is CommonSetup { // Underwriter approves the invoice vm.startPrank(underwriter); - bullaFactoring.approveInvoice(invoiceId, interestApr, spreadBps, upfrontBps, 0); + _approveInvoice(invoiceId, interestApr, spreadBps, upfrontBps, 0); vm.stopPrank(); // creditor funds the invoice vm.startPrank(bob); bullaClaim.approve(address(bullaFactoring), invoiceId); - bullaFactoring.fundInvoice(invoiceId, upfrontBps, address(0)); + _fundInvoice(invoiceId, upfrontBps, address(0)); vm.stopPrank(); // Simulate debtor paying in 30 days instead of 60 @@ -239,13 +239,13 @@ contract TestDepositAndRedemption is CommonSetup { // Underwriter approves the invoice vm.startPrank(underwriter); - bullaFactoring.approveInvoice(invoiceId, interestApr, spreadBps, upfrontBps, 0); + _approveInvoice(invoiceId, interestApr, spreadBps, upfrontBps, 0); vm.stopPrank(); // creditor funds the invoice vm.startPrank(bob); bullaClaim.approve(address(bullaFactoring), invoiceId); - bullaFactoring.fundInvoice(invoiceId, upfrontBps, address(0)); + _fundInvoice(invoiceId, upfrontBps, address(0)); vm.stopPrank(); // Simulate debtor paying in 30 days instead of 60 @@ -304,13 +304,13 @@ contract TestDepositAndRedemption is CommonSetup { // Underwriter approves the invoice vm.startPrank(underwriter); - bullaFactoring.approveInvoice(invoiceId, interestApr, spreadBps, upfrontBps, 0); + _approveInvoice(invoiceId, interestApr, spreadBps, upfrontBps, 0); vm.stopPrank(); // creditor funds the invoice vm.startPrank(bob); bullaClaim.approve(address(bullaFactoring), invoiceId); - bullaFactoring.fundInvoice(invoiceId, upfrontBps, address(0)); + _fundInvoice(invoiceId, upfrontBps, address(0)); vm.stopPrank(); // Simulate 30 days pass, hence some accrued interest in the pool @@ -342,13 +342,13 @@ contract TestDepositAndRedemption is CommonSetup { // Underwriter approves the invoice vm.startPrank(underwriter); - bullaFactoring.approveInvoice(invoiceId, interestApr, spreadBps, upfrontBps, 0); + _approveInvoice(invoiceId, interestApr, spreadBps, upfrontBps, 0); vm.stopPrank(); // creditor funds the invoice vm.startPrank(bob); bullaClaim.approve(address(bullaFactoring), invoiceId); - bullaFactoring.fundInvoice(invoiceId, upfrontBps, address(0)); + _fundInvoice(invoiceId, upfrontBps, address(0)); vm.stopPrank(); // Fast forward time @@ -403,13 +403,13 @@ contract TestDepositAndRedemption is CommonSetup { // Underwriter approves the invoice vm.startPrank(underwriter); - bullaFactoring.approveInvoice(invoiceId, interestApr, spreadBps, upfrontBps, 0); + _approveInvoice(invoiceId, interestApr, spreadBps, upfrontBps, 0); vm.stopPrank(); // creditor funds the invoice vm.startPrank(bob); bullaClaim.approve(address(bullaFactoring), invoiceId); - bullaFactoring.fundInvoice(invoiceId, upfrontBps, address(0)); + _fundInvoice(invoiceId, upfrontBps, address(0)); vm.stopPrank(); // Preview redeem after funding invoice @@ -447,13 +447,13 @@ contract TestDepositAndRedemption is CommonSetup { // Underwriter approves the invoice vm.startPrank(underwriter); - bullaFactoring.approveInvoice(invoiceId, interestApr, spreadBps, upfrontBps, 0); + _approveInvoice(invoiceId, interestApr, spreadBps, upfrontBps, 0); vm.stopPrank(); // creditor funds the invoice vm.startPrank(bob); bullaClaim.approve(address(bullaFactoring), invoiceId); - bullaFactoring.fundInvoice(invoiceId, upfrontBps, address(0)); + _fundInvoice(invoiceId, upfrontBps, address(0)); vm.stopPrank(); // Preview withdraw after funding invoice @@ -502,13 +502,13 @@ contract TestDepositAndRedemption is CommonSetup { // Underwriter approves the invoice vm.startPrank(underwriter); - bullaFactoring.approveInvoice(invoiceId, interestApr, spreadBps, upfrontBps, 0); + _approveInvoice(invoiceId, interestApr, spreadBps, upfrontBps, 0); vm.stopPrank(); // creditor funds the invoice vm.startPrank(bob); bullaClaim.approve(address(bullaFactoring), invoiceId); - bullaFactoring.fundInvoice(invoiceId, upfrontBps, address(0)); + _fundInvoice(invoiceId, upfrontBps, address(0)); vm.stopPrank(); // Preview deposit after funding invoice @@ -543,13 +543,13 @@ contract TestDepositAndRedemption is CommonSetup { // Underwriter approves the invoice vm.startPrank(underwriter); - bullaFactoring.approveInvoice(invoiceId, interestApr, spreadBps, upfrontBps, 0); + _approveInvoice(invoiceId, interestApr, spreadBps, upfrontBps, 0); vm.stopPrank(); // creditor funds the invoice vm.startPrank(bob); bullaClaim.approve(address(bullaFactoring), invoiceId); - bullaFactoring.fundInvoice(invoiceId, upfrontBps, address(0)); + _fundInvoice(invoiceId, upfrontBps, address(0)); vm.stopPrank(); // After days of interest is not 0 diff --git a/test/foundry/TestEmptyRedemptionGas.t.sol b/test/foundry/TestEmptyRedemptionGas.t.sol index 10481c3..a84e611 100644 --- a/test/foundry/TestEmptyRedemptionGas.t.sol +++ b/test/foundry/TestEmptyRedemptionGas.t.sol @@ -111,12 +111,12 @@ contract TestEmptyRedemptionGas is CommonSetup { vm.stopPrank(); vm.startPrank(underwriter); - bullaFactoring.approveInvoice(invoiceId, 1000, 100, 10000, 0); // 10% APR, 1% spread, 100% upfront + _approveInvoice(invoiceId, 1000, 100, 10000, 0); // 10% APR, 1% spread, 100% upfront vm.stopPrank(); vm.startPrank(bob); bullaClaim.approve(address(bullaFactoring), invoiceId); - bullaFactoring.fundInvoice(invoiceId, 10000, address(0)); // 100% upfront + _fundInvoice(invoiceId, 10000, address(0)); // 100% upfront } vm.stopPrank(); @@ -207,12 +207,12 @@ contract TestEmptyRedemptionGas is CommonSetup { vm.stopPrank(); vm.startPrank(underwriter); - bullaFactoring.approveInvoice(invoiceId, 1000, 100, 10000, 0); // 10% APR, 1% spread, 100% upfront + _approveInvoice(invoiceId, 1000, 100, 10000, 0); // 10% APR, 1% spread, 100% upfront vm.stopPrank(); vm.startPrank(bob); bullaClaim.approve(address(bullaFactoring), invoiceId); - bullaFactoring.fundInvoice(invoiceId, 10000, address(0)); + _fundInvoice(invoiceId, 10000, address(0)); } vm.stopPrank(); diff --git a/test/foundry/TestErrorHandlingAndEdgeCases.t.sol b/test/foundry/TestErrorHandlingAndEdgeCases.t.sol index 578e013..7a2205d 100644 --- a/test/foundry/TestErrorHandlingAndEdgeCases.t.sol +++ b/test/foundry/TestErrorHandlingAndEdgeCases.t.sol @@ -33,7 +33,7 @@ contract TestErrorHandlingAndEdgeCases is CommonSetup { uint256 incorrectInvoiceId = uint256(keccak256(abi.encodePacked(block.timestamp, block.prevrandao))) % 10000000000; vm.startPrank(underwriter); vm.expectRevert(abi.encodeWithSignature("NotMinted()")); - bullaFactoring.approveInvoice(incorrectInvoiceId, interestApr, spreadBps, upfrontBps, 0); + _approveInvoice(incorrectInvoiceId, interestApr, spreadBps, upfrontBps, 0); vm.stopPrank(); } @@ -49,7 +49,7 @@ contract TestErrorHandlingAndEdgeCases is CommonSetup { vm.startPrank(bob); bullaClaim.approve(address(bullaFactoring), invoiceId01); vm.expectRevert(abi.encodeWithSignature("InvoiceNotApproved()")); - bullaFactoring.fundInvoice(invoiceId01, upfrontBps, address(0)); + _fundInvoiceExpectRevert(invoiceId01, upfrontBps, address(0)); vm.stopPrank(); } @@ -65,12 +65,12 @@ contract TestErrorHandlingAndEdgeCases is CommonSetup { vm.startPrank(bob); bullaClaim.approve(address(bullaFactoring), invoiceId); vm.startPrank(underwriter); - bullaFactoring.approveInvoice(invoiceId, interestApr, spreadBps, upfrontBps, 0); + _approveInvoice(invoiceId, interestApr, spreadBps, upfrontBps, 0); vm.stopPrank(); vm.warp(block.timestamp + 2 hours); vm.startPrank(bob); vm.expectRevert(abi.encodeWithSignature("ApprovalExpired()")); - bullaFactoring.fundInvoice(invoiceId, upfrontBps, address(0)); + _fundInvoiceExpectRevert(invoiceId, upfrontBps, address(0)); vm.stopPrank(); } @@ -86,12 +86,12 @@ contract TestErrorHandlingAndEdgeCases is CommonSetup { vm.startPrank(bob); bullaClaim.approve(address(bullaFactoring), invoiceId); vm.startPrank(underwriter); - bullaFactoring.approveInvoice(invoiceId, interestApr, spreadBps, upfrontBps, 0); + _approveInvoice(invoiceId, interestApr, spreadBps, upfrontBps, 0); vm.stopPrank(); vm.startPrank(bob); bullaClaim.cancelClaim(invoiceId, ""); vm.expectRevert(abi.encodeWithSignature("InvoiceCanceled()")); - bullaFactoring.fundInvoice(invoiceId, upfrontBps, address(0)); + _fundInvoiceExpectRevert(invoiceId, upfrontBps, address(0)); vm.stopPrank(); vm.startPrank(bob); @@ -99,7 +99,7 @@ contract TestErrorHandlingAndEdgeCases is CommonSetup { vm.startPrank(bob); bullaClaim.approve(address(bullaFactoring), invoiceId02); vm.startPrank(underwriter); - bullaFactoring.approveInvoice(invoiceId02, interestApr, spreadBps, upfrontBps, 0); + _approveInvoice(invoiceId02, interestApr, spreadBps, upfrontBps, 0); vm.stopPrank(); vm.startPrank(alice); @@ -108,7 +108,7 @@ contract TestErrorHandlingAndEdgeCases is CommonSetup { vm.startPrank(bob); vm.expectRevert(abi.encodeWithSignature("InvoiceCanceled()")); - bullaFactoring.fundInvoice(invoiceId02, upfrontBps, address(0)); + _fundInvoiceExpectRevert(invoiceId02, upfrontBps, address(0)); vm.stopPrank(); } @@ -124,7 +124,7 @@ contract TestErrorHandlingAndEdgeCases is CommonSetup { vm.startPrank(bob); bullaClaim.approve(address(bullaFactoring), invoiceId); vm.startPrank(underwriter); - bullaFactoring.approveInvoice(invoiceId, interestApr, spreadBps, upfrontBps, 0); + _approveInvoice(invoiceId, interestApr, spreadBps, upfrontBps, 0); vm.stopPrank(); vm.startPrank(alice); @@ -134,7 +134,7 @@ contract TestErrorHandlingAndEdgeCases is CommonSetup { vm.startPrank(bob); vm.expectRevert(abi.encodeWithSignature("InvoiceAlreadyPaid()")); - bullaFactoring.fundInvoice(invoiceId, upfrontBps, address(0)); + _fundInvoiceExpectRevert(invoiceId, upfrontBps, address(0)); vm.stopPrank(); } @@ -150,7 +150,7 @@ contract TestErrorHandlingAndEdgeCases is CommonSetup { vm.startPrank(bob); bullaClaim.approve(address(bullaFactoring), invoiceId); vm.startPrank(underwriter); - bullaFactoring.approveInvoice(invoiceId, interestApr, spreadBps, upfrontBps, 0); + _approveInvoice(invoiceId, interestApr, spreadBps, upfrontBps, 0); vm.stopPrank(); vm.startPrank(bob); @@ -159,7 +159,7 @@ contract TestErrorHandlingAndEdgeCases is CommonSetup { vm.startPrank(bob); vm.expectRevert(abi.encodeWithSignature("InvoiceCreditorChanged()")); - bullaFactoring.fundInvoice(invoiceId, upfrontBps, address(0)); + _fundInvoiceExpectRevert(invoiceId, upfrontBps, address(0)); vm.stopPrank(); } @@ -176,11 +176,11 @@ contract TestErrorHandlingAndEdgeCases is CommonSetup { uint invoiceId01Amount = 100; uint256 invoiceId01 = createClaim(bob, alice, invoiceId01Amount, dueBy); vm.startPrank(underwriter); - bullaFactoring.approveInvoice(invoiceId01, interestApr, spreadBps, upfrontBps, 0); + _approveInvoice(invoiceId01, interestApr, spreadBps, upfrontBps, 0); vm.stopPrank(); vm.startPrank(bob); bullaClaim.approve(address(bullaFactoring), invoiceId01); - bullaFactoring.fundInvoice(invoiceId01, upfrontBps, address(0)); + _fundInvoice(invoiceId01, upfrontBps, address(0)); vm.stopPrank(); // Simulate debtor paying in 30 days @@ -201,11 +201,11 @@ contract TestErrorHandlingAndEdgeCases is CommonSetup { uint invoiceId03Amount = 100; uint256 invoiceId03 = createClaim(bob, alice, invoiceId03Amount, dueByNew); vm.startPrank(underwriter); - bullaFactoring.approveInvoice(invoiceId03, interestApr, spreadBps, upfrontBps, 0); + _approveInvoice(invoiceId03, interestApr, spreadBps, upfrontBps, 0); vm.stopPrank(); vm.startPrank(bob); bullaClaim.approve(address(bullaFactoring), invoiceId03); - bullaFactoring.fundInvoice(invoiceId03, upfrontBps, address(0)); + _fundInvoice(invoiceId03, upfrontBps, address(0)); vm.stopPrank(); // Fast forward time by 900 days to simulate interest rate cap @@ -241,13 +241,13 @@ contract TestErrorHandlingAndEdgeCases is CommonSetup { vm.stopPrank(); vm.startPrank(underwriter); - bullaFactoring.approveInvoice(invoiceId, interestApr, spreadBps, upfrontBps, 0); + _approveInvoice(invoiceId, interestApr, spreadBps, upfrontBps, 0); vm.stopPrank(); vm.startPrank(bob); bullaClaim.approve(address(bullaFactoring), invoiceId); (, uint256 adminFee, uint256 targetInterest, uint256 targetSpread, uint256 targetProtocolFee, , ) = bullaFactoring.calculateTargetFees(invoiceId, upfrontBps); - bullaFactoring.fundInvoice(invoiceId, upfrontBps, address(0)); + _fundInvoice(invoiceId, upfrontBps, address(0)); vm.stopPrank(); uint capitalAccountBefore = bullaFactoring.calculateCapitalAccount(); @@ -287,11 +287,11 @@ contract TestErrorHandlingAndEdgeCases is CommonSetup { uint invoiceId01Amount = 500000; // 0.5 USDC uint256 invoiceId01 = createClaim(bob, alice, invoiceId01Amount, dueBy); vm.startPrank(underwriter); - bullaFactoring.approveInvoice(invoiceId01, interestApr, spreadBps, upfrontBps, 0); + _approveInvoice(invoiceId01, interestApr, spreadBps, upfrontBps, 0); vm.stopPrank(); vm.startPrank(bob); bullaClaim.approve(address(bullaFactoring), invoiceId01); - bullaFactoring.fundInvoice(invoiceId01, upfrontBps, address(0)); + _fundInvoice(invoiceId01, upfrontBps, address(0)); vm.stopPrank(); // Fast forward time by 30 days @@ -310,11 +310,11 @@ contract TestErrorHandlingAndEdgeCases is CommonSetup { uint invoiceId02Amount = 1000000; // 1 USDC uint256 invoiceId02 = createClaim(bob, alice, invoiceId02Amount, dueBy); vm.startPrank(underwriter); - bullaFactoring.approveInvoice(invoiceId02, interestApr, spreadBps, upfrontBps, 0); + _approveInvoice(invoiceId02, interestApr, spreadBps, upfrontBps, 0); vm.stopPrank(); vm.startPrank(bob); bullaClaim.approve(address(bullaFactoring), invoiceId02); - bullaFactoring.fundInvoice(invoiceId02, upfrontBps, address(0)); + _fundInvoice(invoiceId02, upfrontBps, address(0)); vm.stopPrank(); uint priceBeforeRedeem = bullaFactoring.pricePerShare(); @@ -372,12 +372,12 @@ contract TestErrorHandlingAndEdgeCases is CommonSetup { vm.startPrank(underwriter); vm.expectRevert(abi.encodeWithSignature("InvalidPercentage()")); - bullaFactoring.approveInvoice(invoiceId, interestApr, spreadBps, 0, 0); + _approveInvoice(invoiceId, interestApr, spreadBps, 0, 0); vm.stopPrank(); vm.startPrank(underwriter); vm.expectRevert(abi.encodeWithSignature("InvalidPercentage()")); - bullaFactoring.approveInvoice(invoiceId, interestApr, spreadBps, 10001, 0); + _approveInvoice(invoiceId, interestApr, spreadBps, 10001, 0); vm.stopPrank(); } @@ -393,7 +393,7 @@ contract TestErrorHandlingAndEdgeCases is CommonSetup { vm.startPrank(alice); vm.expectRevert(abi.encodeWithSignature("CallerNotUnderwriter()")); - bullaFactoring.approveInvoice(invoiceId, interestApr, spreadBps, upfrontBps, 0); + _approveInvoice(invoiceId, interestApr, spreadBps, upfrontBps, 0); vm.stopPrank(); vm.startPrank(address(this)); @@ -401,7 +401,7 @@ contract TestErrorHandlingAndEdgeCases is CommonSetup { vm.stopPrank(); vm.startPrank(alice); - bullaFactoring.approveInvoice(invoiceId, interestApr, spreadBps, upfrontBps, 0); + _approveInvoice(invoiceId, interestApr, spreadBps, upfrontBps, 0); vm.stopPrank(); } @@ -447,13 +447,13 @@ contract TestErrorHandlingAndEdgeCases is CommonSetup { // Underwriter approves the invoice vm.startPrank(underwriter); - bullaFactoring.approveInvoice(invoiceId1, interestApr, spreadBps, upfrontBps, 0); + _approveInvoice(invoiceId1, interestApr, spreadBps, upfrontBps, 0); vm.stopPrank(); // creditor funds the invoice vm.startPrank(bob); bullaClaim.approve(address(bullaFactoring), invoiceId1); - bullaFactoring.fundInvoice(invoiceId1, upfrontBps, address(0)); + _fundInvoice(invoiceId1, upfrontBps, address(0)); vm.stopPrank(); vm.startPrank(address(this)); @@ -470,14 +470,14 @@ contract TestErrorHandlingAndEdgeCases is CommonSetup { // Underwriter approves the invoice vm.startPrank(underwriter); - bullaFactoring.approveInvoice(invoiceId2, interestApr, spreadBps, upfrontBps, 0); + _approveInvoice(invoiceId2, interestApr, spreadBps, upfrontBps, 0); vm.stopPrank(); // creditor funds the invoice vm.startPrank(bob); bullaClaim.approve(address(bullaFactoring), invoiceId2); vm.expectRevert(abi.encodeWithSignature("UnauthorizedFactoring(address)", bob)); - bullaFactoring.fundInvoice(invoiceId2, upfrontBps, address(0)); + _fundInvoiceExpectRevert(invoiceId2, upfrontBps, address(0)); vm.stopPrank(); } @@ -493,12 +493,12 @@ contract TestErrorHandlingAndEdgeCases is CommonSetup { vm.stopPrank(); vm.startPrank(underwriter); - bullaFactoring.approveInvoice(invoiceId01, targetYield, spreadBps, upfrontBps, 0); + _approveInvoice(invoiceId01, targetYield, spreadBps, upfrontBps, 0); vm.stopPrank(); vm.startPrank(bob); bullaClaim.approve(address(bullaFactoring), invoiceId01); - bullaFactoring.fundInvoice(invoiceId01, upfrontBps, address(0)); + _fundInvoice(invoiceId01, upfrontBps, address(0)); vm.stopPrank(); // set new fees higher than initial fees @@ -513,12 +513,12 @@ contract TestErrorHandlingAndEdgeCases is CommonSetup { vm.stopPrank(); vm.startPrank(underwriter); - bullaFactoring.approveInvoice(invoiceId02, newTargetYield, spreadBps, upfrontBps, 0); + _approveInvoice(invoiceId02, newTargetYield, spreadBps, upfrontBps, 0); vm.stopPrank(); vm.startPrank(bob); bullaClaim.approve(address(bullaFactoring), invoiceId02); - bullaFactoring.fundInvoice(invoiceId02, upfrontBps, address(0)); + _fundInvoice(invoiceId02, upfrontBps, address(0)); (, uint targetAdminFeeAfterFeeChange, , uint targetSpreadAfterFeeChange, uint targetProtocolFeeAfterFeeChange, , ) = bullaFactoring.calculateTargetFees(invoiceId02, upfrontBps); vm.stopPrank(); diff --git a/test/foundry/TestExternalFrendLendFactoring.t.sol b/test/foundry/TestExternalFrendLendFactoring.t.sol index be4bc4c..4fbd4a8 100644 --- a/test/foundry/TestExternalFrendLendFactoring.t.sol +++ b/test/foundry/TestExternalFrendLendFactoring.t.sol @@ -124,13 +124,13 @@ contract TestExternalFrendLendFactoring is CommonSetup { // Underwriter approves the loan for factoring vm.startPrank(underwriter); - bullaFactoring.approveInvoice(loanId, 730, 200, 8000, 0); // 7.3% yield, 2% spread, 80% upfront, 7 days min + _approveInvoice(loanId, 730, 200, 8000, 0); // 7.3% yield, 2% spread, 80% upfront, 7 days min vm.stopPrank(); // Bob factors the loan vm.startPrank(bob); IERC721(address(bullaFrendLend)).approve(address(bullaFactoring), loanId); - uint256 fundedAmount = bullaFactoring.fundInvoice(loanId, 8000, address(0)); + uint256 fundedAmount = _fundInvoice(loanId, 8000, address(0)); vm.stopPrank(); assertGt(fundedAmount, 0, "Should have funded the loan"); @@ -222,8 +222,8 @@ contract TestExternalFrendLendFactoring is CommonSetup { // Approve both loans for factoring with identical terms vm.startPrank(underwriter); - bullaFactoring.approveInvoice(loanId1, 730, 300, 7500, 0); // 7.3% target yield, 3% spread, 75% upfront - bullaFactoring.approveInvoice(loanId2, 730, 300, 7500, 0); // Identical factoring terms + _approveInvoice(loanId1, 730, 300, 7500, 0); // 7.3% target yield, 3% spread, 75% upfront + _approveInvoice(loanId2, 730, 300, 7500, 0); // Identical factoring terms vm.stopPrank(); // Record protocol fee balance before funding to verify upfront realization @@ -233,11 +233,11 @@ contract TestExternalFrendLendFactoring is CommonSetup { vm.startPrank(bob); IERC721(address(bullaFrendLend)).approve(address(bullaFactoring), loanId1); IERC721(address(bullaFrendLend)).approve(address(bullaFactoring), loanId2); - uint256 fundedAmount1 = bullaFactoring.fundInvoice(loanId1, 7500, address(0)); + uint256 fundedAmount1 = _fundInvoice(loanId1, 7500, address(0)); uint256 protocolFeeAfterLoan1Funding = bullaFactoring.protocolFeeBalance(); - uint256 fundedAmount2 = bullaFactoring.fundInvoice(loanId2, 7500, address(0)); + uint256 fundedAmount2 = _fundInvoice(loanId2, 7500, address(0)); vm.stopPrank(); uint256 protocolFeeAfterBothFunded = bullaFactoring.protocolFeeBalance(); @@ -345,12 +345,12 @@ contract TestExternalFrendLendFactoring is CommonSetup { // Approve and factor the loan vm.startPrank(underwriter); - bullaFactoring.approveInvoice(loanId, 730, 300, 7500, 0); // 10 days minimum + _approveInvoice(loanId, 730, 300, 7500, 0); // 10 days minimum vm.stopPrank(); vm.startPrank(bob); IERC721(address(bullaFrendLend)).approve(address(bullaFactoring), loanId); - bullaFactoring.fundInvoice(loanId, 7500, address(0)); + _fundInvoice(loanId, 7500, address(0)); vm.stopPrank(); // Fast forward 20 days (early payment - less interest accrued) @@ -404,12 +404,12 @@ contract TestExternalFrendLendFactoring is CommonSetup { // Approve and factor the loan vm.startPrank(underwriter); - bullaFactoring.approveInvoice(loanId, 365, 150, 6000, 0); // 3.65% yield, 1.5% spread, 60% upfront, 5 days min + _approveInvoice(loanId, 365, 150, 6000, 0); // 3.65% yield, 1.5% spread, 60% upfront, 5 days min vm.stopPrank(); vm.startPrank(bob); IERC721(address(bullaFrendLend)).approve(address(bullaFactoring), loanId); - bullaFactoring.fundInvoice(loanId, 6000, address(0)); + _fundInvoice(loanId, 6000, address(0)); vm.stopPrank(); // Fast forward 15 days @@ -468,12 +468,12 @@ contract TestExternalFrendLendFactoring is CommonSetup { // Approve and factor the loan vm.startPrank(underwriter); - bullaFactoring.approveInvoice(loanId, 730, 400, 7000, 0); // 14 days minimum + _approveInvoice(loanId, 730, 400, 7000, 0); // 14 days minimum vm.stopPrank(); vm.startPrank(bob); IERC721(address(bullaFrendLend)).approve(address(bullaFactoring), loanId); - bullaFactoring.fundInvoice(loanId, 7000, address(0)); + _fundInvoice(loanId, 7000, address(0)); vm.stopPrank(); // Fast forward 10 days @@ -516,9 +516,9 @@ contract TestExternalFrendLendFactoring is CommonSetup { // Approve all loans for factoring vm.startPrank(underwriter); - bullaFactoring.approveInvoice(loanId1, 730, 200, 8000, 0); - bullaFactoring.approveInvoice(loanId2, 730, 200, 8000, 0); - bullaFactoring.approveInvoice(loanId3, 730, 200, 8000, 0); + _approveInvoice(loanId1, 730, 200, 8000, 0); + _approveInvoice(loanId2, 730, 200, 8000, 0); + _approveInvoice(loanId3, 730, 200, 8000, 0); vm.stopPrank(); // Factor all loans @@ -527,9 +527,9 @@ contract TestExternalFrendLendFactoring is CommonSetup { IERC721(address(bullaFrendLend)).approve(address(bullaFactoring), loanId2); IERC721(address(bullaFrendLend)).approve(address(bullaFactoring), loanId3); - uint256 fundedAmount1 = bullaFactoring.fundInvoice(loanId1, 8000, address(0)); - uint256 fundedAmount2 = bullaFactoring.fundInvoice(loanId2, 8000, address(0)); - uint256 fundedAmount3 = bullaFactoring.fundInvoice(loanId3, 8000, address(0)); + uint256 fundedAmount1 = _fundInvoice(loanId1, 8000, address(0)); + uint256 fundedAmount2 = _fundInvoice(loanId2, 8000, address(0)); + uint256 fundedAmount3 = _fundInvoice(loanId3, 8000, address(0)); vm.stopPrank(); // All loans should be funded with identical amounts @@ -601,29 +601,29 @@ contract TestExternalFrendLendFactoring is CommonSetup { vm.startPrank(bob); IERC721(address(bullaFrendLend)).approve(address(bullaFactoring), loanId); vm.expectRevert(abi.encodeWithSignature("InvoiceNotApproved()")); - bullaFactoring.fundInvoice(loanId, 8000, address(0)); + _fundInvoiceExpectRevert(loanId, 8000, address(0)); vm.stopPrank(); // Approve with invalid upfront percentage vm.startPrank(underwriter); - bullaFactoring.approveInvoice(loanId, 730, 200, 8000, 0); + _approveInvoice(loanId, 730, 200, 8000, 0); vm.stopPrank(); // Try to factor with too high upfront percentage - should fail vm.startPrank(bob); vm.expectRevert(abi.encodeWithSignature("InvalidPercentage()")); - bullaFactoring.fundInvoice(loanId, 9000, address(0)); // 90% > 80% approved + _fundInvoiceExpectRevert(loanId, 9000, address(0)); vm.stopPrank(); // Try to factor with zero upfront percentage - should fail vm.startPrank(bob); vm.expectRevert(abi.encodeWithSignature("InvalidPercentage()")); - bullaFactoring.fundInvoice(loanId, 0, address(0)); + _fundInvoiceExpectRevert(loanId, 0, address(0)); vm.stopPrank(); // Factor successfully vm.startPrank(bob); - uint256 fundedAmount = bullaFactoring.fundInvoice(loanId, 8000, address(0)); + uint256 fundedAmount = _fundInvoice(loanId, 8000, address(0)); vm.stopPrank(); assertGt(fundedAmount, 0, "Should have funded the loan"); @@ -631,7 +631,7 @@ contract TestExternalFrendLendFactoring is CommonSetup { // Try to factor again - should fail since creditor changed vm.startPrank(bob); vm.expectRevert(abi.encodeWithSignature("InvoiceCreditorChanged()")); - bullaFactoring.fundInvoice(loanId, 8000, address(0)); + _fundInvoiceExpectRevert(loanId, 8000, address(0)); vm.stopPrank(); } diff --git a/test/foundry/TestFees.t.sol b/test/foundry/TestFees.t.sol index f8aca95..143c4ae 100644 --- a/test/foundry/TestFees.t.sol +++ b/test/foundry/TestFees.t.sol @@ -36,11 +36,11 @@ contract TestFees is CommonSetup { uint256 invoiceId = createClaim(bob, alice, invoiceAmount, dueBy); vm.stopPrank(); vm.startPrank(underwriter); - bullaFactoring.approveInvoice(invoiceId, interestApr, spreadBps, upfrontBps, 0); + _approveInvoice(invoiceId, interestApr, spreadBps, upfrontBps, 0); vm.stopPrank(); vm.startPrank(bob); bullaClaim.approve(address(bullaFactoring), invoiceId); - bullaFactoring.fundInvoice(invoiceId, upfrontBps, address(0)); + _fundInvoice(invoiceId, upfrontBps, address(0)); vm.stopPrank(); // Check initial balances @@ -87,11 +87,11 @@ contract TestFees is CommonSetup { uint invoiceId01Amount = 100000; uint256 invoiceId01 = createClaim(bob, alice, invoiceId01Amount, dueBy); vm.startPrank(underwriter); - bullaFactoring.approveInvoice(invoiceId01, interestApr, spreadBps, upfrontBps, 0); + _approveInvoice(invoiceId01, interestApr, spreadBps, upfrontBps, 0); vm.stopPrank(); vm.startPrank(bob); bullaClaim.approve(address(bullaFactoring), invoiceId01); - bullaFactoring.fundInvoice(invoiceId01, upfrontBps, address(0)); + _fundInvoice(invoiceId01, upfrontBps, address(0)); vm.stopPrank(); vm.startPrank(bob); @@ -99,10 +99,10 @@ contract TestFees is CommonSetup { uint256 invoiceId02 = createClaim(bob, alice, invoiceId02Amount, dueBy); bullaClaim.approve(address(bullaFactoring), invoiceId02); vm.startPrank(underwriter); - bullaFactoring.approveInvoice(invoiceId02, interestApr, spreadBps, upfrontBps, 0); + _approveInvoice(invoiceId02, interestApr, spreadBps, upfrontBps, 0); vm.stopPrank(); vm.startPrank(bob); - bullaFactoring.fundInvoice(invoiceId02, upfrontBps, address(0)); + _fundInvoice(invoiceId02, upfrontBps, address(0)); vm.stopPrank(); // Simulate debtor paying in 30 days @@ -146,26 +146,26 @@ contract TestFees is CommonSetup { vm.prank(bob); uint256 invoiceId1 = createClaim(bob, alice, invoiceAmount, dueDate); vm.startPrank(underwriter); - bullaFactoring.approveInvoice(invoiceId1, interestApr, spreadBps, 10000, 0); // 100% upfront + _approveInvoice(invoiceId1, interestApr, spreadBps, 10000, 0); // 100% upfront vm.stopPrank(); vm.startPrank(bob); bullaClaim.approve(address(bullaFactoring), invoiceId1); (, uint256 adminFee1, uint256 targetInterest1, uint256 targetSpread1, uint256 targetProtocolFee1, , ) = bullaFactoring.calculateTargetFees(invoiceId1, 10000); - bullaFactoring.fundInvoice(invoiceId1, 10000, address(0)); + _fundInvoice(invoiceId1, 10000, address(0)); vm.stopPrank(); // Create and fund second invoice with 50% upfront vm.prank(bob); uint256 invoiceId2 = createClaim(bob, alice, invoiceAmount, dueDate); vm.startPrank(underwriter); - bullaFactoring.approveInvoice(invoiceId2, interestApr, spreadBps, 10000, 0); // Approve with 100% max but fund with 50% + _approveInvoice(invoiceId2, interestApr, spreadBps, 10000, 0); // Approve with 100% max but fund with 50% vm.stopPrank(); vm.startPrank(bob); bullaClaim.approve(address(bullaFactoring), invoiceId2); (, uint256 adminFee2, uint256 targetInterest2, uint256 targetSpread2, uint256 targetProtocolFee2, , ) = bullaFactoring.calculateTargetFees(invoiceId2, 5000); - bullaFactoring.fundInvoice(invoiceId2, 5000, address(0)); + _fundInvoice(invoiceId2, 5000, address(0)); vm.stopPrank(); uint capitalAccountBefore = bullaFactoring.calculateCapitalAccount(); @@ -213,13 +213,13 @@ contract TestFees is CommonSetup { vm.prank(bob); uint256 invoiceId1 = createClaim(bob, alice, invoiceAmount, dueDate); vm.startPrank(underwriter); - bullaFactoring.approveInvoice(invoiceId1, interestApr, spreadBps, upfrontBps, 0); // 100% upfront + _approveInvoice(invoiceId1, interestApr, spreadBps, upfrontBps, 0); // 100% upfront vm.stopPrank(); vm.startPrank(bob); bullaClaim.approve(address(bullaFactoring), invoiceId1); (, uint256 targetAdminFee1, , , , , ) = bullaFactoring.calculateTargetFees(invoiceId1, upfrontBps); - bullaFactoring.fundInvoice(invoiceId1, upfrontBps, address(0)); + _fundInvoice(invoiceId1, upfrontBps, address(0)); vm.stopPrank(); @@ -236,13 +236,13 @@ contract TestFees is CommonSetup { vm.prank(bob); uint256 invoiceId2 = createClaim(bob, alice, invoiceAmount, dueDate2); vm.startPrank(underwriter); - bullaFactoring.approveInvoice(invoiceId2, interestApr, spreadBps, upfrontBps, 0); + _approveInvoice(invoiceId2, interestApr, spreadBps, upfrontBps, 0); vm.stopPrank(); vm.startPrank(bob); bullaClaim.approve(address(bullaFactoring), invoiceId2); (, uint256 targetAdminFee2, , , , , ) = bullaFactoring.calculateTargetFees(invoiceId2, upfrontBps); - bullaFactoring.fundInvoice(invoiceId2, upfrontBps, address(0)); + _fundInvoice(invoiceId2, upfrontBps, address(0)); vm.stopPrank(); assertEq(targetAdminFee2, targetAdminFee1, "Admin fee should be the same"); @@ -266,11 +266,11 @@ contract TestFees is CommonSetup { uint256 invoiceId = createClaim(bob, alice, invoiceAmount, dueBy); vm.stopPrank(); vm.startPrank(underwriter); - bullaFactoring.approveInvoice(invoiceId, interestApr, spreadBps, upfrontBps, 0); + _approveInvoice(invoiceId, interestApr, spreadBps, upfrontBps, 0); vm.stopPrank(); vm.startPrank(bob); bullaClaim.approve(address(bullaFactoring), invoiceId); - bullaFactoring.fundInvoice(invoiceId, upfrontBps, address(0)); + _fundInvoice(invoiceId, upfrontBps, address(0)); vm.stopPrank(); // alice pays invoice @@ -322,11 +322,11 @@ contract TestFees is CommonSetup { uint256 invoiceId = createClaim(bob, alice, invoiceAmount, dueBy); vm.stopPrank(); vm.startPrank(underwriter); - bullaFactoring.approveInvoice(invoiceId, interestApr, spreadBps, upfrontBps, 0); + _approveInvoice(invoiceId, interestApr, spreadBps, upfrontBps, 0); vm.stopPrank(); vm.startPrank(bob); bullaClaim.approve(address(bullaFactoring), invoiceId); - bullaFactoring.fundInvoice(invoiceId, upfrontBps, address(0)); + _fundInvoice(invoiceId, upfrontBps, address(0)); vm.stopPrank(); // alice pays invoice @@ -366,11 +366,11 @@ contract TestFees is CommonSetup { vm.stopPrank(); vm.startPrank(underwriter); uint16 zeroSpreadBps = 0; - bullaFactoring.approveInvoice(invoiceId, interestApr, zeroSpreadBps, upfrontBps, 0); + _approveInvoice(invoiceId, interestApr, zeroSpreadBps, upfrontBps, 0); vm.stopPrank(); vm.startPrank(bob); bullaClaim.approve(address(bullaFactoring), invoiceId); - bullaFactoring.fundInvoice(invoiceId, upfrontBps, address(0)); + _fundInvoice(invoiceId, upfrontBps, address(0)); vm.stopPrank(); // alice pays invoice @@ -409,11 +409,11 @@ contract TestFees is CommonSetup { uint256 invoiceId = createClaim(bob, alice, invoiceAmount, dueBy); vm.stopPrank(); vm.startPrank(underwriter); - bullaFactoring.approveInvoice(invoiceId, bullaFactoring.targetYieldBps(), spreadBps, upfrontBps, 0); + _approveInvoice(invoiceId, bullaFactoring.targetYieldBps(), spreadBps, upfrontBps, 0); vm.stopPrank(); vm.startPrank(bob); bullaClaim.approve(address(bullaFactoring), invoiceId); - bullaFactoring.fundInvoice(invoiceId, upfrontBps, address(0)); + _fundInvoice(invoiceId, upfrontBps, address(0)); vm.stopPrank(); // alice pays invoice diff --git a/test/foundry/TestFundManagerFactoringIntegration.t.sol b/test/foundry/TestFundManagerFactoringIntegration.t.sol index bf16481..fcfdc42 100644 --- a/test/foundry/TestFundManagerFactoringIntegration.t.sol +++ b/test/foundry/TestFundManagerFactoringIntegration.t.sol @@ -104,12 +104,12 @@ contract TestFundManagerFactoringIntegration is CommonSetup { uint256 invoiceId = createClaim(bob, alice, invoiceAmount, dueBy); vm.prank(underwriter); - bullaFactoring.approveInvoice(invoiceId, 1000, 100, 8000, 0); + _approveInvoice(invoiceId, 1000, 100, 8000, 0); vm.prank(bob); bullaClaim.approve(address(bullaFactoring), invoiceId); vm.prank(bob); - uint256 fundedAmount = bullaFactoring.fundInvoice(invoiceId, 8000, address(0)); + uint256 fundedAmount = _fundInvoice(invoiceId, 8000, address(0)); // Verify factoring operation succeeded assertGt(fundedAmount, 0); @@ -135,7 +135,7 @@ contract TestFundManagerFactoringIntegration is CommonSetup { uint256 invoiceId = createClaim(bob, alice, invoiceAmount, dueBy); vm.prank(underwriter); - bullaFactoring.approveInvoice(invoiceId, 1000, 100, 8000, 0); + _approveInvoice(invoiceId, 1000, 100, 8000, 0); vm.prank(bob); bullaClaim.approve(address(bullaFactoring), invoiceId); @@ -143,7 +143,7 @@ contract TestFundManagerFactoringIntegration is CommonSetup { vm.prank(bob); // fundedAmountGross now includes protocol fee: 80000 * 1e6 * 8000 / 10000 = 64000 * 1e6 vm.expectRevert(abi.encodeWithSelector(BullaFactoringV2_2.InsufficientFunds.selector, 0, 64000000000)); - bullaFactoring.fundInvoice(invoiceId, 8000, address(0)); + _fundInvoiceExpectRevert(invoiceId, 8000, address(0)); // Capital call to fund the pool vm.prank(capitalCaller); @@ -151,7 +151,7 @@ contract TestFundManagerFactoringIntegration is CommonSetup { // Now factoring should work vm.prank(bob); - uint256 fundedAmount = bullaFactoring.fundInvoice(invoiceId, 8000, address(0)); + uint256 fundedAmount = _fundInvoice(invoiceId, 8000, address(0)); assertGt(fundedAmount, 0); } @@ -210,12 +210,12 @@ contract TestFundManagerFactoringIntegration is CommonSetup { uint256 invoiceId = createClaim(bob, alice, invoiceAmount, dueBy); vm.prank(underwriter); - bullaFactoring.approveInvoice(invoiceId, 1000, 100, 8000, 0); + _approveInvoice(invoiceId, 1000, 100, 8000, 0); vm.prank(bob); bullaClaim.approve(address(bullaFactoring), invoiceId); vm.prank(bob); - uint256 fundedAmount = bullaFactoring.fundInvoice(invoiceId, 8000, address(0)); + uint256 fundedAmount = _fundInvoice(invoiceId, 8000, address(0)); // Fast forward and pay invoice vm.warp(block.timestamp + 30 days); @@ -257,12 +257,12 @@ contract TestFundManagerFactoringIntegration is CommonSetup { invoiceIds[i] = createClaim(bob, alice, invoiceAmounts[i], dueBy + (i * 10 days)); vm.prank(underwriter); - bullaFactoring.approveInvoice(invoiceIds[i], 1000, 100, 8000, 0); + _approveInvoice(invoiceIds[i], 1000, 100, 8000, 0); vm.prank(bob); bullaClaim.approve(address(bullaFactoring), invoiceIds[i]); vm.prank(bob); - bullaFactoring.fundInvoice(invoiceIds[i], 8000, address(0)); + _fundInvoice(invoiceIds[i], 8000, address(0)); } // Pay invoices at different times to simulate real portfolio vm.warp(block.timestamp + 25 days); diff --git a/test/foundry/TestGasProfiler.t.sol b/test/foundry/TestGasProfiler.t.sol index 1ad3a03..5b583b8 100644 --- a/test/foundry/TestGasProfiler.t.sol +++ b/test/foundry/TestGasProfiler.t.sol @@ -69,12 +69,12 @@ contract TestGasProfiler is CommonSetup { vm.stopPrank(); vm.startPrank(underwriter); - bullaFactoring.approveInvoice(invoiceIds[i], interestApr, spreadBps, upfrontBps, 0); + _approveInvoice(invoiceIds[i], interestApr, spreadBps, upfrontBps, 0); vm.stopPrank(); vm.startPrank(bob); bullaClaim.approve(address(bullaFactoring), invoiceIds[i]); - bullaFactoring.fundInvoice(invoiceIds[i], upfrontBps, address(0)); + _fundInvoice(invoiceIds[i], upfrontBps, address(0)); } vm.stopPrank(); @@ -277,12 +277,12 @@ contract TestGasProfiler is CommonSetup { vm.stopPrank(); vm.startPrank(underwriter); - bullaFactoring.approveInvoice(invoiceIds[i], interestApr, spreadBps, upfrontBps, 0); + _approveInvoice(invoiceIds[i], interestApr, spreadBps, upfrontBps, 0); vm.stopPrank(); vm.startPrank(bob); bullaClaim.approve(address(bullaFactoring), invoiceIds[i]); - bullaFactoring.fundInvoice(invoiceIds[i], upfrontBps, address(0)); + _fundInvoice(invoiceIds[i], upfrontBps, address(0)); } vm.stopPrank(); diff --git a/test/foundry/TestGetInvoiceDetailsGasCost.t.sol b/test/foundry/TestGetInvoiceDetailsGasCost.t.sol index 5664f5a..287591d 100644 --- a/test/foundry/TestGetInvoiceDetailsGasCost.t.sol +++ b/test/foundry/TestGetInvoiceDetailsGasCost.t.sol @@ -53,12 +53,12 @@ contract TestGetInvoiceDetailsGasCost is CommonSetup { vm.stopPrank(); vm.startPrank(underwriter); - bullaFactoring.approveInvoice(invoiceIds[i], interestApr, spreadBps, upfrontBps, 0); + _approveInvoice(invoiceIds[i], interestApr, spreadBps, upfrontBps, 0); vm.stopPrank(); vm.startPrank(bob); IERC721(address(bullaInvoice)).approve(address(bullaFactoring), invoiceIds[i]); // Approve BullaInvoice NFT - bullaFactoring.fundInvoice(invoiceIds[i], upfrontBps, address(0)); + _fundInvoice(invoiceIds[i], upfrontBps, address(0)); } vm.stopPrank(); @@ -103,12 +103,12 @@ contract TestGetInvoiceDetailsGasCost is CommonSetup { vm.stopPrank(); vm.startPrank(underwriter); - bullaFactoring.approveInvoice(invoiceIds[i], interestApr, spreadBps, upfrontBps, 0); + _approveInvoice(invoiceIds[i], interestApr, spreadBps, upfrontBps, 0); vm.stopPrank(); vm.startPrank(bob); bullaClaim.approve(address(bullaFactoring), invoiceIds[i]); - bullaFactoring.fundInvoice(invoiceIds[i], upfrontBps, address(0)); + _fundInvoice(invoiceIds[i], upfrontBps, address(0)); } vm.stopPrank(); @@ -192,12 +192,12 @@ contract TestGetInvoiceDetailsGasCost is CommonSetup { vm.stopPrank(); vm.startPrank(underwriter); - bullaFactoring.approveInvoice(invoiceId, interestApr, spreadBps, upfrontBps, 0); + _approveInvoice(invoiceId, interestApr, spreadBps, upfrontBps, 0); vm.stopPrank(); vm.startPrank(bob); bullaClaim.approve(address(bullaFactoring), invoiceId); - bullaFactoring.fundInvoice(invoiceId, upfrontBps, address(0)); + _fundInvoice(invoiceId, upfrontBps, address(0)); } vm.stopPrank(); } @@ -353,12 +353,12 @@ contract TestGetInvoiceDetailsGasCost is CommonSetup { vm.stopPrank(); vm.startPrank(underwriter); - bullaFactoring.approveInvoice(invoiceId, interestApr, spreadBps, upfrontBps, 0); + _approveInvoice(invoiceId, interestApr, spreadBps, upfrontBps, 0); vm.stopPrank(); vm.startPrank(bob); bullaClaim.approve(address(bullaFactoring), invoiceId); - bullaFactoring.fundInvoice(invoiceId, upfrontBps, address(0)); + _fundInvoice(invoiceId, upfrontBps, address(0)); } vm.stopPrank(); @@ -878,12 +878,12 @@ contract TestGetInvoiceDetailsGasCost is CommonSetup { vm.stopPrank(); vm.startPrank(underwriter); - bullaFactoring.approveInvoice(invoiceId, interestApr, spreadBps, upfrontBps, 0); + _approveInvoice(invoiceId, interestApr, spreadBps, upfrontBps, 0); vm.stopPrank(); vm.startPrank(bob); IERC721(address(bullaInvoice)).approve(address(bullaFactoring), invoiceId); - bullaFactoring.fundInvoice(invoiceId, upfrontBps, address(0)); + _fundInvoice(invoiceId, upfrontBps, address(0)); } vm.stopPrank(); diff --git a/test/foundry/TestInsufficientFundsWithDeployedCapital.t.sol b/test/foundry/TestInsufficientFundsWithDeployedCapital.t.sol index c7c9117..e1edd3e 100644 --- a/test/foundry/TestInsufficientFundsWithDeployedCapital.t.sol +++ b/test/foundry/TestInsufficientFundsWithDeployedCapital.t.sol @@ -100,11 +100,11 @@ contract TestInsufficientFundsWithDeployedCapital is CommonSetup { vm.stopPrank(); vm.startPrank(underwriter); - bullaFactoring.approveInvoice(invoiceId1, interestApr, spreadBps, upfrontBps, 0); + _approveInvoice(invoiceId1, interestApr, spreadBps, upfrontBps, 0); vm.stopPrank(); vm.startPrank(bob); - bullaFactoring.fundInvoice(invoiceId1, upfrontBps, address(0)); + _fundInvoice(invoiceId1, upfrontBps, address(0)); vm.stopPrank(); // Second invoice with higher fees to consume remaining assets @@ -118,11 +118,11 @@ contract TestInsufficientFundsWithDeployedCapital is CommonSetup { uint16 highSpread = 1500; // 15% vm.startPrank(underwriter); - bullaFactoring.approveInvoice(invoiceId2, interestApr, highSpread, upfrontBps, 0); + _approveInvoice(invoiceId2, interestApr, highSpread, upfrontBps, 0); vm.stopPrank(); vm.startPrank(charlie); - bullaFactoring.fundInvoice(invoiceId2, upfrontBps, address(0)); + _fundInvoice(invoiceId2, upfrontBps, address(0)); vm.stopPrank(); // Check if we achieved totalAssets <= 0 @@ -136,12 +136,12 @@ contract TestInsufficientFundsWithDeployedCapital is CommonSetup { vm.stopPrank(); vm.startPrank(underwriter); - bullaFactoring.approveInvoice(invoiceId3, interestApr, 0, 10000, 0); // High fees + _approveInvoice(invoiceId3, interestApr, 0, 10000, 0); // High fees vm.stopPrank(); vm.startPrank(bob); vm.expectRevert(abi.encodeWithSelector(BullaFactoringV2_2.InsufficientFunds.selector, totalAssetsAfterFactoring, remainingAmount)); - bullaFactoring.fundInvoice(invoiceId3, 10000, address(0)); + _fundInvoiceExpectRevert(invoiceId3, 10000, address(0)); vm.stopPrank(); // Step 2: Create a loan offer for the remaining amount diff --git a/test/foundry/TestInsurance.t.sol b/test/foundry/TestInsurance.t.sol index 8a4664d..39bd5d2 100644 --- a/test/foundry/TestInsurance.t.sol +++ b/test/foundry/TestInsurance.t.sol @@ -106,13 +106,13 @@ contract TestInsurance is CommonSetup { uint256 invoiceId = createClaim(bob, alice, invoiceAmount, dueBy); vm.prank(underwriter); - bullaFactoring.approveInvoice(invoiceId, interestApr, spreadBps, upfrontBps, 0); + _approveInvoice(invoiceId, interestApr, spreadBps, upfrontBps, 0); uint256 insuranceBalanceBefore = bullaFactoring.insuranceBalance(); vm.startPrank(bob); bullaClaim.approve(address(bullaFactoring), invoiceId); - bullaFactoring.fundInvoice(invoiceId, upfrontBps, address(0)); + _fundInvoice(invoiceId, upfrontBps, address(0)); vm.stopPrank(); assertEq(bullaFactoring.insuranceBalance() - insuranceBalanceBefore, 1000, "Premium should be 1% of 100000 = 1000"); @@ -139,8 +139,8 @@ contract TestInsurance is CommonSetup { vm.stopPrank(); vm.startPrank(underwriter); - bullaFactoring.approveInvoice(invoiceId1, interestApr, spreadBps, upfrontBps, 0); - bullaFactoring.approveInvoice(invoiceId2, interestApr, spreadBps, upfrontBps, 0); + _approveInvoice(invoiceId1, interestApr, spreadBps, upfrontBps, 0); + _approveInvoice(invoiceId2, interestApr, spreadBps, upfrontBps, 0); vm.stopPrank(); (, , , , , uint256 insurancePremium1, ) = bullaFactoring.calculateTargetFees(invoiceId1, upfrontBps); @@ -164,10 +164,10 @@ contract TestInsurance is CommonSetup { vm.prank(bob); uint256 invoiceId = createClaim(bob, alice, invoiceAmount, dueBy); vm.prank(underwriter); - bullaFactoring.approveInvoice(invoiceId, interestApr, spreadBps, upfrontBps, 0); + _approveInvoice(invoiceId, interestApr, spreadBps, upfrontBps, 0); vm.startPrank(bob); bullaClaim.approve(address(bullaFactoring), invoiceId); - bullaFactoring.fundInvoice(invoiceId, upfrontBps, address(0)); + _fundInvoice(invoiceId, upfrontBps, address(0)); vm.stopPrank(); } @@ -294,10 +294,10 @@ contract TestInsurance is CommonSetup { vm.prank(bob); uint256 invoiceId = createClaim(bob, alice, invoiceAmount, dueBy); vm.prank(underwriter); - bullaFactoring.approveInvoice(invoiceId, interestApr, spreadBps, upfrontBps, 0); + _approveInvoice(invoiceId, interestApr, spreadBps, upfrontBps, 0); vm.startPrank(bob); bullaClaim.approve(address(bullaFactoring), invoiceId); - bullaFactoring.fundInvoice(invoiceId, upfrontBps, address(0)); + _fundInvoice(invoiceId, upfrontBps, address(0)); vm.stopPrank(); assertEq(bullaFactoring.insuranceBalance(), 2000, "Premium = 200000 * 1% = 2000"); @@ -394,10 +394,10 @@ contract TestInsurance is CommonSetup { vm.prank(bob); uint256 invoiceId = createClaim(bob, alice, invoiceAmount, dueBy); vm.prank(underwriter); - bullaFactoring.approveInvoice(invoiceId, interestApr, spreadBps, upfrontBps, 0); + _approveInvoice(invoiceId, interestApr, spreadBps, upfrontBps, 0); vm.startPrank(bob); bullaClaim.approve(address(bullaFactoring), invoiceId); - bullaFactoring.fundInvoice(invoiceId, upfrontBps, address(0)); + _fundInvoice(invoiceId, upfrontBps, address(0)); vm.stopPrank(); vm.startPrank(alice); @@ -545,10 +545,10 @@ contract TestInsurance is CommonSetup { vm.prank(bob); invoiceIds[i] = createClaim(bob, alice, invoiceAmount, dueBy); vm.prank(underwriter); - bullaFactoring.approveInvoice(invoiceIds[i], interestApr, spreadBps, upfrontBps, 0); + _approveInvoice(invoiceIds[i], interestApr, spreadBps, upfrontBps, 0); vm.startPrank(bob); bullaClaim.approve(address(bullaFactoring), invoiceIds[i]); - bullaFactoring.fundInvoice(invoiceIds[i], upfrontBps, address(0)); + _fundInvoice(invoiceIds[i], upfrontBps, address(0)); vm.stopPrank(); } @@ -583,7 +583,7 @@ contract TestInsurance is CommonSetup { vm.prank(bob); uint256 invoiceId = createClaim(bob, alice, invoiceAmount, dueBy); vm.prank(underwriter); - bullaFactoring.approveInvoice(invoiceId, interestApr, spreadBps, upfrontBps, 0); + _approveInvoice(invoiceId, interestApr, spreadBps, upfrontBps, 0); (, , , , , uint256 insurancePremium, uint256 netFundedAmount) = bullaFactoring.calculateTargetFees(invoiceId, upfrontBps); @@ -604,7 +604,17 @@ contract TestInsurance is CommonSetup { vm.prank(bob); uint256 invoiceId2 = createClaim(bob, alice, invoiceAmount, dueBy); vm.prank(underwriter); - noInsurancePool.approveInvoice(invoiceId2, interestApr, spreadBps, upfrontBps, 0); + { + IBullaFactoringV2_2.ApproveInvoiceParams[] memory approveParams = new IBullaFactoringV2_2.ApproveInvoiceParams[](1); + approveParams[0] = IBullaFactoringV2_2.ApproveInvoiceParams({ + invoiceId: invoiceId2, + targetYieldBps: interestApr, + spreadBps: spreadBps, + upfrontBps: upfrontBps, + initialInvoiceValueOverride: 0 + }); + noInsurancePool.approveInvoices(approveParams); + } (, , , , , uint256 noInsurancePremium, uint256 noInsuranceNetFunded) = noInsurancePool.calculateTargetFees(invoiceId2, upfrontBps); @@ -626,11 +636,11 @@ contract TestInsurance is CommonSetup { invoiceId = createClaim(bob, alice, invoiceAmount, dueBy); vm.prank(underwriter); - bullaFactoring.approveInvoice(invoiceId, interestApr, spreadBps, upfrontBps, 0); + _approveInvoice(invoiceId, interestApr, spreadBps, upfrontBps, 0); vm.startPrank(bob); bullaClaim.approve(address(bullaFactoring), invoiceId); - bullaFactoring.fundInvoice(invoiceId, upfrontBps, address(0)); + _fundInvoice(invoiceId, upfrontBps, address(0)); vm.stopPrank(); } @@ -647,20 +657,20 @@ contract TestInsurance is CommonSetup { vm.prank(bob); uint256 extraId = createClaim(bob, alice, invoiceAmount, dueBy); vm.prank(underwriter); - bullaFactoring.approveInvoice(extraId, interestApr, spreadBps, upfrontBps, 0); + _approveInvoice(extraId, interestApr, spreadBps, upfrontBps, 0); vm.startPrank(bob); bullaClaim.approve(address(bullaFactoring), extraId); - bullaFactoring.fundInvoice(extraId, upfrontBps, address(0)); + _fundInvoice(extraId, upfrontBps, address(0)); vm.stopPrank(); } vm.prank(bob); targetInvoiceId = createClaim(bob, alice, invoiceAmount, dueBy); vm.prank(underwriter); - bullaFactoring.approveInvoice(targetInvoiceId, interestApr, spreadBps, upfrontBps, 0); + _approveInvoice(targetInvoiceId, interestApr, spreadBps, upfrontBps, 0); vm.startPrank(bob); bullaClaim.approve(address(bullaFactoring), targetInvoiceId); - bullaFactoring.fundInvoice(targetInvoiceId, upfrontBps, address(0)); + _fundInvoice(targetInvoiceId, upfrontBps, address(0)); vm.stopPrank(); } } diff --git a/test/foundry/TestInvoiceFundingAndPayment.t.sol b/test/foundry/TestInvoiceFundingAndPayment.t.sol index 96a0545..2e188d7 100644 --- a/test/foundry/TestInvoiceFundingAndPayment.t.sol +++ b/test/foundry/TestInvoiceFundingAndPayment.t.sol @@ -38,13 +38,13 @@ contract TestInvoiceFundingAndPayment is CommonSetup { // Underwriter approves the invoice vm.startPrank(underwriter); - bullaFactoring.approveInvoice(invoiceId, interestApr, spreadBps, upfrontBps, 0); + _approveInvoice(invoiceId, interestApr, spreadBps, upfrontBps, 0); vm.stopPrank(); // creditor funds the invoice vm.startPrank(bob); bullaClaim.approve(address(bullaFactoring), invoiceId); - bullaFactoring.fundInvoice(invoiceId, upfrontBps, address(0)); + _fundInvoice(invoiceId, upfrontBps, address(0)); vm.stopPrank(); @@ -85,16 +85,16 @@ contract TestInvoiceFundingAndPayment is CommonSetup { // Underwriter approves the invoice with approvedUpfrontBps vm.startPrank(underwriter); - bullaFactoring.approveInvoice(invoiceId, interestApr, spreadBps, approvedUpfrontBps, 0); - bullaFactoring.approveInvoice(invoiceId2, interestApr, spreadBps, approvedUpfrontBps, 0); + _approveInvoice(invoiceId, interestApr, spreadBps, approvedUpfrontBps, 0); + _approveInvoice(invoiceId2, interestApr, spreadBps, approvedUpfrontBps, 0); vm.stopPrank(); // Factorer funds one invoice at a lower UpfrontBps vm.startPrank(bob); bullaClaim.approve(address(bullaFactoring), invoiceId); - bullaFactoring.fundInvoice(invoiceId, approvedUpfrontBps, address(0)); + _fundInvoice(invoiceId, approvedUpfrontBps, address(0)); bullaClaim.approve(address(bullaFactoring), invoiceId2); - bullaFactoring.fundInvoice(invoiceId2, factorerUpfrontBps, address(0)); + _fundInvoice(invoiceId2, factorerUpfrontBps, address(0)); vm.stopPrank(); (, , , , , , uint256 actualFundedAmount, , , , , , , ) = bullaFactoring.approvedInvoices(invoiceId); @@ -117,11 +117,11 @@ contract TestInvoiceFundingAndPayment is CommonSetup { uint256 invoiceId01 = createClaim(bob, alice, invoiceId01Amount, dueBy); vm.startPrank(underwriter); - bullaFactoring.approveInvoice(invoiceId01, interestApr, spreadBps, upfrontBps, 0); + _approveInvoice(invoiceId01, interestApr, spreadBps, upfrontBps, 0); vm.stopPrank(); vm.startPrank(bob); bullaClaim.approve(address(bullaFactoring), invoiceId01); - bullaFactoring.fundInvoice(invoiceId01, upfrontBps, address(0)); + _fundInvoice(invoiceId01, upfrontBps, address(0)); vm.stopPrank(); // Simulate debtor paying in 30 days @@ -166,11 +166,11 @@ contract TestInvoiceFundingAndPayment is CommonSetup { uint256 invoiceId01 = createClaim(bob, alice, invoiceId01Amount, dueBy); vm.startPrank(underwriter); - bullaFactoring.approveInvoice(invoiceId01, targetYield, spreadBps, upfrontBps, 0); + _approveInvoice(invoiceId01, targetYield, spreadBps, upfrontBps, 0); vm.stopPrank(); vm.startPrank(bob); bullaClaim.approve(address(bullaFactoring), invoiceId01); - bullaFactoring.fundInvoice(invoiceId01, upfrontBps, address(0)); + _fundInvoice(invoiceId01, upfrontBps, address(0)); vm.stopPrank(); // Simulate debtor paying in 30 days @@ -220,16 +220,16 @@ contract TestInvoiceFundingAndPayment is CommonSetup { // Underwriter approves both invoices vm.startPrank(underwriter); - bullaFactoring.approveInvoice(partiallyPaidInvoiceId, interestApr, spreadBps, upfrontBps, 0); - bullaFactoring.approveInvoice(fullyUnpaidInvoiceId, interestApr, spreadBps, upfrontBps, 0); + _approveInvoice(partiallyPaidInvoiceId, interestApr, spreadBps, upfrontBps, 0); + _approveInvoice(fullyUnpaidInvoiceId, interestApr, spreadBps, upfrontBps, 0); vm.stopPrank(); // Factorer funds both invoices vm.startPrank(bob); bullaClaim.approve(address(bullaFactoring), partiallyPaidInvoiceId); - uint256 partiallyPaidFundedAmount = bullaFactoring.fundInvoice(partiallyPaidInvoiceId, upfrontBps, address(0)); + uint256 partiallyPaidFundedAmount = _fundInvoice(partiallyPaidInvoiceId, upfrontBps, address(0)); bullaClaim.approve(address(bullaFactoring), fullyUnpaidInvoiceId); - uint256 fullyUnpaidFundedAmount =bullaFactoring.fundInvoice(fullyUnpaidInvoiceId, upfrontBps, address(0)); + uint256 fullyUnpaidFundedAmount =_fundInvoice(fullyUnpaidInvoiceId, upfrontBps, address(0)); vm.stopPrank(); assertTrue(fullyUnpaidFundedAmount > partiallyPaidFundedAmount, "Funded amount for partially paid invoice should be less than fully unpaid invoice"); @@ -258,15 +258,15 @@ contract TestInvoiceFundingAndPayment is CommonSetup { vm.startPrank(underwriter); - bullaFactoring.approveInvoice(invoiceId01, targetYield, spreadBps, upfrontBps, 0); - bullaFactoring.approveInvoice(invoiceId02, targetYield, spreadBps, upfrontBps, 0); + _approveInvoice(invoiceId01, targetYield, spreadBps, upfrontBps, 0); + _approveInvoice(invoiceId02, targetYield, spreadBps, upfrontBps, 0); vm.stopPrank(); vm.startPrank(bob); bullaClaim.approve(address(bullaFactoring), invoiceId01); - uint fundedAmount01 = bullaFactoring.fundInvoice(invoiceId01, upfrontBps, address(0)); + uint fundedAmount01 = _fundInvoice(invoiceId01, upfrontBps, address(0)); bullaClaim.approve(address(bullaFactoring), invoiceId02); - uint fundedAmount02 = bullaFactoring.fundInvoice(invoiceId02, upfrontBps, address(0)); + uint fundedAmount02 = _fundInvoice(invoiceId02, upfrontBps, address(0)); vm.stopPrank(); assertLt(fundedAmount01, fundedAmount02, "Funded amount for partially paid invoice should be less than fully unpaid invoice"); @@ -300,7 +300,7 @@ contract TestInvoiceFundingAndPayment is CommonSetup { vm.startPrank(underwriter); vm.expectRevert(abi.encodeWithSignature("InvoiceAlreadyPaid()")); - bullaFactoring.approveInvoice(invoiceId01, targetYield, spreadBps, upfrontBps, 0); + _approveInvoice(invoiceId01, targetYield, spreadBps, upfrontBps, 0); vm.stopPrank(); } @@ -322,14 +322,14 @@ contract TestInvoiceFundingAndPayment is CommonSetup { vm.startPrank(underwriter); - bullaFactoring.approveInvoice(invoiceId01, targetYield, spreadBps, upfrontBps, 0); + _approveInvoice(invoiceId01, targetYield, spreadBps, upfrontBps, 0); vm.stopPrank(); vm.warp(block.timestamp + 10 minutes); vm.startPrank(bob); vm.expectRevert(abi.encodeWithSignature("ApprovalExpired()")); - bullaFactoring.fundInvoice(invoiceId01, upfrontBps, address(0)); + _fundInvoiceExpectRevert(invoiceId01, upfrontBps, address(0)); vm.stopPrank(); } @@ -349,7 +349,7 @@ contract TestInvoiceFundingAndPayment is CommonSetup { uint256 invoiceId01 = createClaim(bob, alice, invoiceId01Amount, dueBy); vm.startPrank(underwriter); - bullaFactoring.approveInvoice(invoiceId01, targetYield, spreadBps, upfrontBps, 0); + _approveInvoice(invoiceId01, targetYield, spreadBps, upfrontBps, 0); vm.stopPrank(); vm.startPrank(bob); bullaClaim.approve(address(bullaFactoring), invoiceId01); @@ -357,7 +357,7 @@ contract TestInvoiceFundingAndPayment is CommonSetup { vm.startPrank(alice); vm.expectRevert(); - bullaFactoring.fundInvoice(invoiceId01, upfrontBps, address(0)); + _fundInvoiceExpectRevert(invoiceId01, upfrontBps, address(0)); vm.stopPrank(); } @@ -391,7 +391,7 @@ contract TestInvoiceFundingAndPayment is CommonSetup { vm.startPrank(underwriter); vm.expectRevert(abi.encodeWithSignature("InvoiceTokenMismatch()")); - bullaFactoring.approveInvoice(invoiceId01, targetYield, spreadBps, upfrontBps, 0); + _approveInvoice(invoiceId01, targetYield, spreadBps, upfrontBps, 0); vm.stopPrank(); } @@ -414,14 +414,14 @@ contract TestInvoiceFundingAndPayment is CommonSetup { uint16 highInterestApr = 1000; // 10% APR uint16 highSpreadBps = 1000; // 10% spread vm.startPrank(underwriter); - bullaFactoring.approveInvoice(invoiceId1, highInterestApr, highSpreadBps, upfrontBps, 0); + _approveInvoice(invoiceId1, highInterestApr, highSpreadBps, upfrontBps, 0); vm.stopPrank(); // Approve second invoice with 0% APR and 0% spread uint16 zeroInterestApr = 0; // 0% APR uint16 zeroSpreadBps = 0; // 0% spread vm.startPrank(underwriter); - bullaFactoring.approveInvoice(invoiceId2, zeroInterestApr, zeroSpreadBps, upfrontBps, 0); + _approveInvoice(invoiceId2, zeroInterestApr, zeroSpreadBps, upfrontBps, 0); vm.stopPrank(); // Calculate target fees for both invoices @@ -445,9 +445,9 @@ contract TestInvoiceFundingAndPayment is CommonSetup { // Fund both invoices vm.startPrank(bob); bullaClaim.approve(address(bullaFactoring), invoiceId1); - bullaFactoring.fundInvoice(invoiceId1, upfrontBps, address(0)); + _fundInvoice(invoiceId1, upfrontBps, address(0)); bullaClaim.approve(address(bullaFactoring), invoiceId2); - bullaFactoring.fundInvoice(invoiceId2, upfrontBps, address(0)); + _fundInvoice(invoiceId2, upfrontBps, address(0)); vm.stopPrank(); // Simulate payment after some time @@ -681,7 +681,7 @@ contract TestInvoiceFundingAndPayment is CommonSetup { // Approve invoice vm.startPrank(underwriter); - bullaFactoring.approveInvoice(invoiceId, interestApr, spreadBps, upfrontBps, 0); + _approveInvoice(invoiceId, interestApr, spreadBps, upfrontBps, 0); vm.stopPrank(); // Check initial balances @@ -691,7 +691,7 @@ contract TestInvoiceFundingAndPayment is CommonSetup { // Fund invoice with charlie as receiver vm.startPrank(bob); bullaClaim.approve(address(bullaFactoring), invoiceId); - uint256 fundedAmount = bullaFactoring.fundInvoice(invoiceId, upfrontBps, charlie); + uint256 fundedAmount = _fundInvoice(invoiceId, upfrontBps, charlie); vm.stopPrank(); // Verify charlie received the funds, not bob @@ -715,7 +715,7 @@ contract TestInvoiceFundingAndPayment is CommonSetup { // Approve invoice vm.startPrank(underwriter); - bullaFactoring.approveInvoice(invoiceId, interestApr, spreadBps, upfrontBps, 0); + _approveInvoice(invoiceId, interestApr, spreadBps, upfrontBps, 0); vm.stopPrank(); // Check initial balance @@ -724,7 +724,7 @@ contract TestInvoiceFundingAndPayment is CommonSetup { // Fund invoice with address(0) as receiver (should default to msg.sender) vm.startPrank(bob); bullaClaim.approve(address(bullaFactoring), invoiceId); - uint256 fundedAmount = bullaFactoring.fundInvoice(invoiceId, upfrontBps, address(0)); + uint256 fundedAmount = _fundInvoice(invoiceId, upfrontBps, address(0)); vm.stopPrank(); // Verify bob received the funds (since address(0) defaults to msg.sender) @@ -748,8 +748,8 @@ contract TestInvoiceFundingAndPayment is CommonSetup { // Approve both invoices vm.startPrank(underwriter); - bullaFactoring.approveInvoice(invoiceId1, interestApr, spreadBps, upfrontBps, 0); - bullaFactoring.approveInvoice(invoiceId2, interestApr, spreadBps, upfrontBps, 0); + _approveInvoice(invoiceId1, interestApr, spreadBps, upfrontBps, 0); + _approveInvoice(invoiceId2, interestApr, spreadBps, upfrontBps, 0); vm.stopPrank(); // Check initial balances @@ -759,11 +759,11 @@ contract TestInvoiceFundingAndPayment is CommonSetup { // Fund first invoice with address(0) (should go to bob) vm.startPrank(bob); bullaClaim.approve(address(bullaFactoring), invoiceId1); - uint256 fundedAmount1 = bullaFactoring.fundInvoice(invoiceId1, upfrontBps, address(0)); + uint256 fundedAmount1 = _fundInvoice(invoiceId1, upfrontBps, address(0)); // Fund second invoice with charlie as receiver bullaClaim.approve(address(bullaFactoring), invoiceId2); - uint256 fundedAmount2 = bullaFactoring.fundInvoice(invoiceId2, upfrontBps, charlie); + uint256 fundedAmount2 = _fundInvoice(invoiceId2, upfrontBps, charlie); vm.stopPrank(); // Verify the funded amounts are the same (since invoices are identical) @@ -790,7 +790,7 @@ contract TestInvoiceFundingAndPayment is CommonSetup { // Approve invoice vm.startPrank(underwriter); - bullaFactoring.approveInvoice(invoiceId, interestApr, spreadBps, upfrontBps, 0); + _approveInvoice(invoiceId, interestApr, spreadBps, upfrontBps, 0); vm.stopPrank(); // Check initial balances @@ -799,7 +799,7 @@ contract TestInvoiceFundingAndPayment is CommonSetup { // Fund invoice with charlie as receiver vm.startPrank(bob); bullaClaim.approve(address(bullaFactoring), invoiceId); - uint256 fundedAmount = bullaFactoring.fundInvoice(invoiceId, upfrontBps, charlie); + uint256 fundedAmount = _fundInvoice(invoiceId, upfrontBps, charlie); vm.stopPrank(); // Verify charlie received the initial funding @@ -841,7 +841,7 @@ contract TestInvoiceFundingAndPayment is CommonSetup { // Approve invoice vm.startPrank(underwriter); - bullaFactoring.approveInvoice(invoiceId, interestApr, spreadBps, upfrontBps, 0); + _approveInvoice(invoiceId, interestApr, spreadBps, upfrontBps, 0); vm.stopPrank(); // Record initial balance @@ -850,7 +850,7 @@ contract TestInvoiceFundingAndPayment is CommonSetup { // Fund invoice with address(0) as receiver (should default to msg.sender = bob) vm.startPrank(bob); bullaClaim.approve(address(bullaFactoring), invoiceId); - uint256 fundedAmount = bullaFactoring.fundInvoice(invoiceId, upfrontBps, address(0)); + uint256 fundedAmount = _fundInvoice(invoiceId, upfrontBps, address(0)); vm.stopPrank(); // Verify bob received the initial funding diff --git a/test/foundry/TestInvoiceUnfactoring.t.sol b/test/foundry/TestInvoiceUnfactoring.t.sol index a5b8796..90acebc 100644 --- a/test/foundry/TestInvoiceUnfactoring.t.sol +++ b/test/foundry/TestInvoiceUnfactoring.t.sol @@ -37,11 +37,11 @@ contract TestInvoiceUnfactoring is CommonSetup { vm.prank(bob); uint256 invoiceId = createClaim(bob, alice, invoiceIdAmount, dueBy); vm.startPrank(underwriter); - bullaFactoring.approveInvoice(invoiceId, interestApr, spreadBps, upfrontBps, 0); + _approveInvoice(invoiceId, interestApr, spreadBps, upfrontBps, 0); vm.stopPrank(); vm.startPrank(bob); bullaClaim.approve(address(bullaFactoring), invoiceId); - bullaFactoring.fundInvoice(invoiceId, upfrontBps, address(0)); + _fundInvoice(invoiceId, upfrontBps, address(0)); vm.stopPrank(); vm.expectEmit(true, false, false, false); @@ -70,22 +70,22 @@ contract TestInvoiceUnfactoring is CommonSetup { uint invoiceId01Amount = 100; uint256 invoiceId01 = createClaim(bob, alice, invoiceId01Amount, dueBy); vm.startPrank(underwriter); - bullaFactoring.approveInvoice(invoiceId01, interestApr, spreadBps, upfrontBps, 0); + _approveInvoice(invoiceId01, interestApr, spreadBps, upfrontBps, 0); vm.stopPrank(); vm.startPrank(bob); bullaClaim.approve(address(bullaFactoring), invoiceId01); - bullaFactoring.fundInvoice(invoiceId01, upfrontBps, address(0)); + _fundInvoice(invoiceId01, upfrontBps, address(0)); vm.stopPrank(); vm.startPrank(bob); uint invoiceId02Amount = 900; uint256 invoiceId02 = createClaim(bob, alice, invoiceId02Amount, dueBy); vm.startPrank(underwriter); - bullaFactoring.approveInvoice(invoiceId02, interestApr, spreadBps, upfrontBps, 0); + _approveInvoice(invoiceId02, interestApr, spreadBps, upfrontBps, 0); vm.stopPrank(); vm.startPrank(bob); bullaClaim.approve(address(bullaFactoring), invoiceId02); - bullaFactoring.fundInvoice(invoiceId02, upfrontBps, address(0)); + _fundInvoice(invoiceId02, upfrontBps, address(0)); vm.stopPrank(); // alice pays both invoices @@ -99,11 +99,11 @@ contract TestInvoiceUnfactoring is CommonSetup { uint invoiceId03Amount = 50; uint256 invoiceId03 = createClaim(bob, alice, invoiceId03Amount, dueBy); vm.startPrank(underwriter); - bullaFactoring.approveInvoice(invoiceId03, interestApr, spreadBps, upfrontBps, 0); + _approveInvoice(invoiceId03, interestApr, spreadBps, upfrontBps, 0); vm.stopPrank(); vm.startPrank(bob); bullaClaim.approve(address(bullaFactoring), invoiceId03); - bullaFactoring.fundInvoice(invoiceId03, upfrontBps, address(0)); + _fundInvoice(invoiceId03, upfrontBps, address(0)); vm.stopPrank(); // Fast forward time by 100 days to simulate the invoice becoming impaired @@ -143,11 +143,11 @@ contract TestInvoiceUnfactoring is CommonSetup { uint invoiceId01Amount = 50000; uint256 invoiceId01 = createClaim(bob, alice, invoiceId01Amount, dueBy); vm.startPrank(underwriter); - bullaFactoring.approveInvoice(invoiceId01, interestApr, spreadBps, upfrontBps, 0); + _approveInvoice(invoiceId01, interestApr, spreadBps, upfrontBps, 0); vm.stopPrank(); vm.startPrank(bob); bullaClaim.approve(address(bullaFactoring), invoiceId01); - bullaFactoring.fundInvoice(invoiceId01, upfrontBps, address(0)); + _fundInvoice(invoiceId01, upfrontBps, address(0)); vm.stopPrank(); vm.warp(block.timestamp + 1 days); @@ -186,11 +186,11 @@ contract TestInvoiceUnfactoring is CommonSetup { vm.startPrank(bob); uint256 invoiceId01 = createClaim(bob, alice, invoiceAmount, dueBy); vm.startPrank(underwriter); - bullaFactoring.approveInvoice(invoiceId01, interestApr, spreadBps, upfrontBps, 0); + _approveInvoice(invoiceId01, interestApr, spreadBps, upfrontBps, 0); vm.stopPrank(); vm.startPrank(bob); bullaClaim.approve(address(bullaFactoring), invoiceId01); - bullaFactoring.fundInvoice(invoiceId01, upfrontBps, address(0)); + _fundInvoice(invoiceId01, upfrontBps, address(0)); vm.stopPrank(); uint balanceBeforeUnfactoring = asset.balanceOf(bob); @@ -206,11 +206,11 @@ contract TestInvoiceUnfactoring is CommonSetup { vm.startPrank(bob); uint256 invoiceId03 = createClaim(bob, alice, invoiceAmount, dueBy); vm.startPrank(underwriter); - bullaFactoring.approveInvoice(invoiceId03, interestApr, spreadBps, upfrontBps, 0); + _approveInvoice(invoiceId03, interestApr, spreadBps, upfrontBps, 0); vm.stopPrank(); vm.startPrank(bob); bullaClaim.approve(address(bullaFactoring), invoiceId03); - bullaFactoring.fundInvoice(invoiceId03, upfrontBps, address(0)); + _fundInvoice(invoiceId03, upfrontBps, address(0)); vm.stopPrank(); // Fast forward time by 90 days @@ -243,11 +243,11 @@ contract TestInvoiceUnfactoring is CommonSetup { vm.prank(bob); uint256 invoiceId = createClaim(bob, alice, invoiceAmount, dueBy); vm.startPrank(underwriter); - bullaFactoring.approveInvoice(invoiceId, interestApr, spreadBps, upfrontBps, 0); + _approveInvoice(invoiceId, interestApr, spreadBps, upfrontBps, 0); vm.stopPrank(); vm.startPrank(bob); bullaClaim.approve(address(bullaFactoring), invoiceId); - bullaFactoring.fundInvoice(invoiceId, upfrontBps, address(0)); + _fundInvoice(invoiceId, upfrontBps, address(0)); vm.stopPrank(); uint256 assetBalanceBefore = asset.balanceOf(bob); @@ -263,11 +263,11 @@ contract TestInvoiceUnfactoring is CommonSetup { vm.prank(bob); uint256 invoiceId2 = createClaim(bob, alice, invoiceAmount, dueBy); vm.startPrank(underwriter); - bullaFactoring.approveInvoice(invoiceId2, interestApr, spreadBps, upfrontBps, 0); + _approveInvoice(invoiceId2, interestApr, spreadBps, upfrontBps, 0); vm.stopPrank(); vm.startPrank(bob); bullaClaim.approve(address(bullaFactoring), invoiceId2); - bullaFactoring.fundInvoice(invoiceId2, upfrontBps, address(0)); + _fundInvoice(invoiceId2, upfrontBps, address(0)); vm.stopPrank(); // alice makes a small payment @@ -290,11 +290,11 @@ contract TestInvoiceUnfactoring is CommonSetup { vm.prank(bob); uint256 invoiceId3 = createClaim(bob, alice, invoiceAmount, dueBy); vm.startPrank(underwriter); - bullaFactoring.approveInvoice(invoiceId3, interestApr, spreadBps, upfrontBps, 0); + _approveInvoice(invoiceId3, interestApr, spreadBps, upfrontBps, 0); vm.stopPrank(); vm.startPrank(bob); bullaClaim.approve(address(bullaFactoring), invoiceId3); - bullaFactoring.fundInvoice(invoiceId3, upfrontBps, address(0)); + _fundInvoice(invoiceId3, upfrontBps, address(0)); vm.stopPrank(); // alice pays almost all of it @@ -330,11 +330,11 @@ contract TestInvoiceUnfactoring is CommonSetup { vm.prank(bob); uint256 invoiceId = createClaim(bob, alice, invoiceAmount, dueBy); vm.startPrank(underwriter); - bullaFactoring.approveInvoice(invoiceId, interestApr, spreadBps, upfrontBps, 0); + _approveInvoice(invoiceId, interestApr, spreadBps, upfrontBps, 0); vm.stopPrank(); vm.startPrank(bob); bullaClaim.approve(address(bullaFactoring), invoiceId); - bullaFactoring.fundInvoice(invoiceId, upfrontBps, address(0)); + _fundInvoice(invoiceId, upfrontBps, address(0)); vm.stopPrank(); // Fast forward past impairment date (30 days due + 60 days grace period) @@ -367,11 +367,11 @@ contract TestInvoiceUnfactoring is CommonSetup { vm.prank(bob); uint256 invoiceId = createClaim(bob, alice, invoiceAmount, dueBy); vm.startPrank(underwriter); - bullaFactoring.approveInvoice(invoiceId, interestApr, spreadBps, upfrontBps, 0); + _approveInvoice(invoiceId, interestApr, spreadBps, upfrontBps, 0); vm.stopPrank(); vm.startPrank(bob); bullaClaim.approve(address(bullaFactoring), invoiceId); - bullaFactoring.fundInvoice(invoiceId, upfrontBps, address(0)); + _fundInvoice(invoiceId, upfrontBps, address(0)); vm.stopPrank(); // Try to unfactor before impairment - should fail @@ -394,11 +394,11 @@ contract TestInvoiceUnfactoring is CommonSetup { vm.prank(bob); uint256 invoiceId = createClaim(bob, alice, invoiceAmount, dueBy); vm.startPrank(underwriter); - bullaFactoring.approveInvoice(invoiceId, interestApr, spreadBps, upfrontBps, 0); + _approveInvoice(invoiceId, interestApr, spreadBps, upfrontBps, 0); vm.stopPrank(); vm.startPrank(bob); bullaClaim.approve(address(bullaFactoring), invoiceId); - bullaFactoring.fundInvoice(invoiceId, upfrontBps, address(0)); + _fundInvoice(invoiceId, upfrontBps, address(0)); vm.stopPrank(); // Alice makes a partial payment @@ -449,11 +449,11 @@ contract TestInvoiceUnfactoring is CommonSetup { vm.prank(bob); uint256 invoiceId = createClaim(bob, alice, invoiceAmount, dueBy); vm.startPrank(underwriter); - bullaFactoring.approveInvoice(invoiceId, interestApr, spreadBps, upfrontBps, 0); + _approveInvoice(invoiceId, interestApr, spreadBps, upfrontBps, 0); vm.stopPrank(); vm.startPrank(bob); bullaClaim.approve(address(bullaFactoring), invoiceId); - bullaFactoring.fundInvoice(invoiceId, upfrontBps, address(0)); + _fundInvoice(invoiceId, upfrontBps, address(0)); vm.stopPrank(); // Fast forward past impairment date @@ -478,14 +478,14 @@ contract TestInvoiceUnfactoring is CommonSetup { vm.prank(bob); uint256 invoiceId = createClaim(bob, alice, invoiceAmount, dueBy); vm.startPrank(underwriter); - bullaFactoring.approveInvoice(invoiceId, interestApr, spreadBps, upfrontBps, 0); + _approveInvoice(invoiceId, interestApr, spreadBps, upfrontBps, 0); vm.stopPrank(); uint256 bobBalanceAfterFunding = asset.balanceOf(bob); vm.startPrank(bob); bullaClaim.approve(address(bullaFactoring), invoiceId); - bullaFactoring.fundInvoice(invoiceId, upfrontBps, address(0)); + _fundInvoice(invoiceId, upfrontBps, address(0)); vm.stopPrank(); bobBalanceAfterFunding = asset.balanceOf(bob); @@ -538,11 +538,11 @@ contract TestInvoiceUnfactoring is CommonSetup { vm.prank(bob); uint256 invoiceId = createClaim(bob, alice, invoiceAmount, dueBy); vm.startPrank(underwriter); - bullaFactoring.approveInvoice(invoiceId, interestApr, spreadBps, upfrontBps, 0); + _approveInvoice(invoiceId, interestApr, spreadBps, upfrontBps, 0); vm.stopPrank(); vm.startPrank(bob); bullaClaim.approve(address(bullaFactoring), invoiceId); - bullaFactoring.fundInvoice(invoiceId, upfrontBps, address(0)); + _fundInvoice(invoiceId, upfrontBps, address(0)); vm.stopPrank(); // Fast forward some time to accrue interest @@ -587,22 +587,22 @@ contract TestInvoiceUnfactoring is CommonSetup { vm.prank(bob); uint256 invoiceId1 = createClaim(bob, alice, invoiceAmount, dueBy); vm.startPrank(underwriter); - bullaFactoring.approveInvoice(invoiceId1, interestApr, spreadBps, upfrontBps, 0); + _approveInvoice(invoiceId1, interestApr, spreadBps, upfrontBps, 0); vm.stopPrank(); vm.startPrank(bob); bullaClaim.approve(address(bullaFactoring), invoiceId1); - bullaFactoring.fundInvoice(invoiceId1, upfrontBps, address(0)); + _fundInvoice(invoiceId1, upfrontBps, address(0)); vm.stopPrank(); // Second invoice - pool owner unfactors vm.prank(bob); uint256 invoiceId2 = createClaim(bob, alice, invoiceAmount, dueBy); vm.startPrank(underwriter); - bullaFactoring.approveInvoice(invoiceId2, interestApr, spreadBps, upfrontBps, 0); + _approveInvoice(invoiceId2, interestApr, spreadBps, upfrontBps, 0); vm.stopPrank(); vm.startPrank(bob); bullaClaim.approve(address(bullaFactoring), invoiceId2); - bullaFactoring.fundInvoice(invoiceId2, upfrontBps, address(0)); + _fundInvoice(invoiceId2, upfrontBps, address(0)); vm.stopPrank(); // Bob unfactors first invoice - flag should be false @@ -641,11 +641,11 @@ contract TestInvoiceUnfactoring is CommonSetup { vm.prank(bob); uint256 invoiceId = createClaim(bob, alice, invoiceAmount, dueBy); vm.startPrank(underwriter); - bullaFactoring.approveInvoice(invoiceId, interestApr, spreadBps, upfrontBps, 0); + _approveInvoice(invoiceId, interestApr, spreadBps, upfrontBps, 0); vm.stopPrank(); vm.startPrank(bob); bullaClaim.approve(address(bullaFactoring), invoiceId); - bullaFactoring.fundInvoice(invoiceId, upfrontBps, address(0)); + _fundInvoice(invoiceId, upfrontBps, address(0)); vm.stopPrank(); // Warp forward a bit, but NOT past impairment date @@ -660,11 +660,11 @@ contract TestInvoiceUnfactoring is CommonSetup { vm.prank(bob); uint256 invoiceId2 = createClaim(bob, alice, invoiceAmount, dueBy + 10 days); vm.startPrank(underwriter); - bullaFactoring.approveInvoice(invoiceId2, interestApr, spreadBps, upfrontBps, 0); + _approveInvoice(invoiceId2, interestApr, spreadBps, upfrontBps, 0); vm.stopPrank(); vm.startPrank(bob); bullaClaim.approve(address(bullaFactoring), invoiceId2); - bullaFactoring.fundInvoice(invoiceId2, upfrontBps, address(0)); + _fundInvoice(invoiceId2, upfrontBps, address(0)); vm.stopPrank(); address poolOwner = bullaFactoring.owner(); @@ -697,11 +697,11 @@ contract TestInvoiceUnfactoring is CommonSetup { vm.prank(bob); uint256 invoiceId = createClaim(bob, alice, invoiceAmount, dueBy); vm.startPrank(underwriter); - bullaFactoring.approveInvoice(invoiceId, interestApr, spreadBps, upfrontBps, 0); + _approveInvoice(invoiceId, interestApr, spreadBps, upfrontBps, 0); vm.stopPrank(); vm.startPrank(bob); bullaClaim.approve(address(bullaFactoring), invoiceId); - bullaFactoring.fundInvoice(invoiceId, upfrontBps, address(0)); + _fundInvoice(invoiceId, upfrontBps, address(0)); vm.stopPrank(); // Debtor cancels the invoice diff --git a/test/foundry/TestMarkClaimAsPaid.t.sol b/test/foundry/TestMarkClaimAsPaid.t.sol index 703d028..7804d8d 100644 --- a/test/foundry/TestMarkClaimAsPaid.t.sol +++ b/test/foundry/TestMarkClaimAsPaid.t.sol @@ -50,7 +50,7 @@ contract TestMarkClaimAsPaid is CommonSetup { // Step 4: Underwriter approves vm.prank(underwriter); vm.expectRevert(abi.encodeWithSignature("InvoiceAlreadyPaid()")); - bullaFactoring.approveInvoice(claimId, interestApr, spreadBps, upfrontBps, 0); + _approveInvoice(claimId, interestApr, spreadBps, upfrontBps, 0); uint256 attackerBalanceAfter = asset.balanceOf(attacker); uint256 poolBalanceAfter = asset.balanceOf(address(bullaFactoring)); @@ -81,7 +81,7 @@ contract TestMarkClaimAsPaid is CommonSetup { // Step 3: Underwriter approves vm.prank(underwriter); - bullaFactoring.approveInvoice(claimId, interestApr, spreadBps, upfrontBps, 0); + _approveInvoice(claimId, interestApr, spreadBps, upfrontBps, 0); // Step 4: Attacker marks claim as paid (frontrunning the approval) vm.prank(attacker); @@ -95,7 +95,7 @@ contract TestMarkClaimAsPaid is CommonSetup { vm.startPrank(attacker); bullaClaim.approve(address(bullaFactoring), claimId); vm.expectRevert(abi.encodeWithSignature("InvoiceAlreadyPaid()")); - bullaFactoring.fundInvoice(claimId, upfrontBps, attacker); + _fundInvoiceExpectRevert(claimId, upfrontBps, attacker); uint256 attackerBalanceAfter = asset.balanceOf(attacker); uint256 poolBalanceAfter = asset.balanceOf(address(bullaFactoring)); diff --git a/test/foundry/TestMissingCoverage.t.sol b/test/foundry/TestMissingCoverage.t.sol index a1f0bb4..b7c01d6 100644 --- a/test/foundry/TestMissingCoverage.t.sol +++ b/test/foundry/TestMissingCoverage.t.sol @@ -109,11 +109,11 @@ contract TestMissingCoverage is CommonSetup { uint256 invoiceId = createClaim(bob, alice, 50000, dueBy); vm.prank(underwriter); - bullaFactoring.approveInvoice(invoiceId, interestApr, spreadBps, upfrontBps, 0); + _approveInvoice(invoiceId, interestApr, spreadBps, upfrontBps, 0); vm.startPrank(bob); bullaClaim.approve(address(bullaFactoring), invoiceId); - uint256 fundedAmountNet = bullaFactoring.fundInvoice(invoiceId, upfrontBps, address(0)); + uint256 fundedAmountNet = _fundInvoice(invoiceId, upfrontBps, address(0)); vm.stopPrank(); IBullaFactoringV2_2.FundInfo memory info = bullaFactoring.getFundInfo(); @@ -134,11 +134,11 @@ contract TestMissingCoverage is CommonSetup { uint256 invoiceId = createClaim(bob, alice, 50000, dueBy); vm.prank(underwriter); - bullaFactoring.approveInvoice(invoiceId, interestApr, spreadBps, upfrontBps, 0); + _approveInvoice(invoiceId, interestApr, spreadBps, upfrontBps, 0); vm.startPrank(bob); bullaClaim.approve(address(bullaFactoring), invoiceId); - bullaFactoring.fundInvoice(invoiceId, upfrontBps, address(0)); + _fundInvoice(invoiceId, upfrontBps, address(0)); vm.stopPrank(); // Pay the invoice - Alice is the debtor and needs funds to pay @@ -186,11 +186,11 @@ contract TestMissingCoverage is CommonSetup { uint256 invoiceId = createClaim(bob, alice, 30000 + (i * 10000), dueBy); vm.prank(underwriter); - bullaFactoring.approveInvoice(invoiceId, interestApr, spreadBps, upfrontBps, 0); + _approveInvoice(invoiceId, interestApr, spreadBps, upfrontBps, 0); vm.startPrank(bob); bullaClaim.approve(address(bullaFactoring), invoiceId); - uint256 fundedAmount = bullaFactoring.fundInvoice(invoiceId, upfrontBps, address(0)); + uint256 fundedAmount = _fundInvoice(invoiceId, upfrontBps, address(0)); totalFunded += fundedAmount; vm.stopPrank(); } @@ -247,11 +247,11 @@ contract TestMissingCoverage is CommonSetup { uint256 invoiceId = createClaim(bob, alice, 50000, dueBy); vm.prank(underwriter); - bullaFactoring.approveInvoice(invoiceId, interestApr, spreadBps, upfrontBps, 0); + _approveInvoice(invoiceId, interestApr, spreadBps, upfrontBps, 0); vm.startPrank(bob); bullaClaim.approve(address(bullaFactoring), invoiceId); - bullaFactoring.fundInvoice(invoiceId, upfrontBps, address(0)); + _fundInvoice(invoiceId, upfrontBps, address(0)); vm.stopPrank(); IBullaFactoringV2_2.FundInfo memory info = bullaFactoring.getFundInfo(); diff --git a/test/foundry/TestPermissionsAndAccessControl.t.sol b/test/foundry/TestPermissionsAndAccessControl.t.sol index c5d5c4f..2a2a4b6 100644 --- a/test/foundry/TestPermissionsAndAccessControl.t.sol +++ b/test/foundry/TestPermissionsAndAccessControl.t.sol @@ -26,12 +26,12 @@ contract TestPermissionsAndAccessControl is CommonSetup { uint256 InvoiceId = createClaim(userWithoutPermissions, alice, invoiceId01Amount, dueBy); vm.stopPrank(); vm.startPrank(underwriter); - bullaFactoring.approveInvoice(InvoiceId, interestApr, spreadBps, upfrontBps, 0); + _approveInvoice(InvoiceId, interestApr, spreadBps, upfrontBps, 0); vm.stopPrank(); vm.startPrank(userWithoutPermissions); bullaClaim.approve(address(bullaFactoring), InvoiceId); vm.expectRevert(abi.encodeWithSignature("UnauthorizedFactoring(address)", userWithoutPermissions)); - bullaFactoring.fundInvoice(InvoiceId, upfrontBps, address(0)); + _fundInvoiceExpectRevert(InvoiceId, upfrontBps, address(0)); vm.stopPrank(); } @@ -103,19 +103,19 @@ contract TestPermissionsAndAccessControl is CommonSetup { // Underwriter approves the invoice vm.startPrank(underwriter); - bullaFactoring.approveInvoice(invoiceId, interestApr, spreadBps, upfrontBps, 0); + _approveInvoice(invoiceId, interestApr, spreadBps, upfrontBps, 0); vm.stopPrank(); // creditor funds the invoice vm.startPrank(bob); bullaClaim.approve(address(bullaFactoring), invoiceId); - bullaFactoring.fundInvoice(invoiceId, upfrontBps, address(0)); + _fundInvoice(invoiceId, upfrontBps, address(0)); vm.stopPrank(); // Underwriter approves the invoice again vm.startPrank(underwriter); vm.expectRevert(abi.encodeWithSignature("InvoiceAlreadyFunded()")); - bullaFactoring.approveInvoice(invoiceId, interestApr, spreadBps, upfrontBps, 0); + _approveInvoice(invoiceId, interestApr, spreadBps, upfrontBps, 0); vm.stopPrank(); } } diff --git a/test/foundry/TestPreviewUnfactor.t.sol b/test/foundry/TestPreviewUnfactor.t.sol index 75cf4ed..be04dda 100644 --- a/test/foundry/TestPreviewUnfactor.t.sol +++ b/test/foundry/TestPreviewUnfactor.t.sol @@ -20,12 +20,12 @@ contract TestPreviewUnfactor is CommonSetup { vm.prank(bob); uint256 invoiceId = createClaim(bob, alice, invoiceAmount, dueBy); vm.startPrank(underwriter); - bullaFactoring.approveInvoice(invoiceId, interestApr, spreadBps, upfrontBps, 0); + _approveInvoice(invoiceId, interestApr, spreadBps, upfrontBps, 0); vm.stopPrank(); vm.startPrank(bob); bullaClaim.approve(address(bullaFactoring), invoiceId); - bullaFactoring.fundInvoice(invoiceId, upfrontBps, address(0)); + _fundInvoice(invoiceId, upfrontBps, address(0)); vm.stopPrank(); // Alice makes a large partial payment (95 out of 100) to create a refund scenario @@ -63,12 +63,12 @@ contract TestPreviewUnfactor is CommonSetup { vm.prank(bob); uint256 invoiceId = createClaim(bob, alice, invoiceAmount, dueBy); vm.startPrank(underwriter); - bullaFactoring.approveInvoice(invoiceId, interestApr, spreadBps, upfrontBps, 0); + _approveInvoice(invoiceId, interestApr, spreadBps, upfrontBps, 0); vm.stopPrank(); vm.startPrank(bob); bullaClaim.approve(address(bullaFactoring), invoiceId); - bullaFactoring.fundInvoice(invoiceId, upfrontBps, address(0)); + _fundInvoice(invoiceId, upfrontBps, address(0)); vm.stopPrank(); // Fast forward to accrue some interest @@ -117,12 +117,12 @@ contract TestPreviewUnfactor is CommonSetup { vm.prank(bob); uint256 invoiceId = createClaim(bob, alice, invoiceAmount, dueBy); vm.startPrank(underwriter); - bullaFactoring.approveInvoice(invoiceId, interestApr, spreadBps, upfrontBps, 0); + _approveInvoice(invoiceId, interestApr, spreadBps, upfrontBps, 0); vm.stopPrank(); vm.startPrank(bob); bullaClaim.approve(address(bullaFactoring), invoiceId); - bullaFactoring.fundInvoice(invoiceId, upfrontBps, address(0)); + _fundInvoice(invoiceId, upfrontBps, address(0)); vm.stopPrank(); // Alice pays the invoice in full @@ -149,12 +149,12 @@ contract TestPreviewUnfactor is CommonSetup { vm.prank(bob); uint256 invoiceId = createClaim(bob, alice, invoiceAmount, dueBy); vm.startPrank(underwriter); - bullaFactoring.approveInvoice(invoiceId, interestApr, spreadBps, upfrontBps, 0); + _approveInvoice(invoiceId, interestApr, spreadBps, upfrontBps, 0); vm.stopPrank(); vm.startPrank(bob); bullaClaim.approve(address(bullaFactoring), invoiceId); - bullaFactoring.fundInvoice(invoiceId, upfrontBps, address(0)); + _fundInvoice(invoiceId, upfrontBps, address(0)); vm.stopPrank(); // Fast forward only 5 days - NOT past impairment (30 days due + 60 days grace = 90 days) diff --git a/test/foundry/TestPricePerShareCalculations.t.sol b/test/foundry/TestPricePerShareCalculations.t.sol index 0056550..7c6c645 100644 --- a/test/foundry/TestPricePerShareCalculations.t.sol +++ b/test/foundry/TestPricePerShareCalculations.t.sol @@ -34,11 +34,11 @@ contract TestPricePerShareCalculations is CommonSetup { uint invoiceId01Amount = 100000; uint256 invoiceId01 = createClaim(bob, alice, invoiceId01Amount, dueBy); vm.startPrank(underwriter); - bullaFactoring.approveInvoice(invoiceId01, interestApr, spreadBps, upfrontBps, 0); + _approveInvoice(invoiceId01, interestApr, spreadBps, upfrontBps, 0); vm.stopPrank(); vm.startPrank(bob); bullaClaim.approve(address(bullaFactoring), invoiceId01); - bullaFactoring.fundInvoice(invoiceId01, upfrontBps, address(0)); + _fundInvoice(invoiceId01, upfrontBps, address(0)); vm.stopPrank(); vm.startPrank(bob); @@ -46,10 +46,10 @@ contract TestPricePerShareCalculations is CommonSetup { uint256 invoiceId02 = createClaim(bob, alice, invoiceId02Amount, dueBy); bullaClaim.approve(address(bullaFactoring), invoiceId02); vm.startPrank(underwriter); - bullaFactoring.approveInvoice(invoiceId02, interestApr, spreadBps, upfrontBps, 0); + _approveInvoice(invoiceId02, interestApr, spreadBps, upfrontBps, 0); vm.stopPrank(); vm.startPrank(bob); - bullaFactoring.fundInvoice(invoiceId02, upfrontBps, address(0)); + _fundInvoice(invoiceId02, upfrontBps, address(0)); vm.stopPrank(); uint factorerBalanceAfterFactoring = asset.balanceOf(bob); @@ -86,11 +86,11 @@ contract TestPricePerShareCalculations is CommonSetup { uint invoiceId01Amount = 1000000; uint256 invoiceId01 = createClaim(bob, alice, invoiceId01Amount, dueBy); vm.startPrank(underwriter); - bullaFactoring.approveInvoice(invoiceId01, interestApr, spreadBps, upfrontBps, 0); + _approveInvoice(invoiceId01, interestApr, spreadBps, upfrontBps, 0); vm.stopPrank(); vm.startPrank(bob); bullaClaim.approve(address(bullaFactoring), invoiceId01); - bullaFactoring.fundInvoice(invoiceId01, upfrontBps, address(0)); + _fundInvoice(invoiceId01, upfrontBps, address(0)); vm.stopPrank(); // Simulate debtor paying in 30 days @@ -110,11 +110,11 @@ contract TestPricePerShareCalculations is CommonSetup { uint invoiceId02Amount = 2000000; uint256 invoiceId02 = createClaim(bob, alice, invoiceId02Amount, dueBy); vm.startPrank(underwriter); - bullaFactoring.approveInvoice(invoiceId02, interestApr, spreadBps, upfrontBps, 0); + _approveInvoice(invoiceId02, interestApr, spreadBps, upfrontBps, 0); vm.stopPrank(); vm.startPrank(bob); bullaClaim.approve(address(bullaFactoring), invoiceId02); - bullaFactoring.fundInvoice(invoiceId02, upfrontBps, address(0)); + _fundInvoice(invoiceId02, upfrontBps, address(0)); vm.stopPrank(); vm.startPrank(alice); @@ -142,11 +142,11 @@ contract TestPricePerShareCalculations is CommonSetup { uint invoiceId01Amount = 500000; // 0.5 USDC uint256 invoiceId01 = createClaim(bob, alice, invoiceId01Amount, dueBy); vm.startPrank(underwriter); - bullaFactoring.approveInvoice(invoiceId01, interestApr, spreadBps, upfrontBps, 0); + _approveInvoice(invoiceId01, interestApr, spreadBps, upfrontBps, 0); vm.stopPrank(); vm.startPrank(bob); bullaClaim.approve(address(bullaFactoring), invoiceId01); - bullaFactoring.fundInvoice(invoiceId01, upfrontBps, address(0)); + _fundInvoice(invoiceId01, upfrontBps, address(0)); vm.stopPrank(); // Fast forward time by 30 days @@ -175,11 +175,11 @@ contract TestPricePerShareCalculations is CommonSetup { uint invoiceId02Amount = 1000000; // 1 USDC uint256 invoiceId02 = createClaim(bob, alice, invoiceId02Amount, dueBy); vm.startPrank(underwriter); - bullaFactoring.approveInvoice(invoiceId02, interestApr, spreadBps, upfrontBps, 0); + _approveInvoice(invoiceId02, interestApr, spreadBps, upfrontBps, 0); vm.stopPrank(); vm.startPrank(bob); bullaClaim.approve(address(bullaFactoring), invoiceId02); - bullaFactoring.fundInvoice(invoiceId02, upfrontBps, address(0)); + _fundInvoice(invoiceId02, upfrontBps, address(0)); vm.stopPrank(); // Fast forward time by 30 days diff --git a/test/foundry/TestPrincipalAmountOverride.t.sol b/test/foundry/TestPrincipalAmountOverride.t.sol index cd76fa1..2a77ad9 100644 --- a/test/foundry/TestPrincipalAmountOverride.t.sol +++ b/test/foundry/TestPrincipalAmountOverride.t.sol @@ -57,7 +57,7 @@ contract TestPrincipalAmountOverride is CommonSetup { // Approve with zero overrideAmount vm.startPrank(underwriter); - bullaFactoring.approveInvoice(invoiceId, interestApr, spreadBps, upfrontBps, 0); + _approveInvoice(invoiceId, interestApr, spreadBps, upfrontBps, 0); vm.stopPrank(); // Should use original calculation (invoice amount - paid amount) @@ -77,7 +77,7 @@ contract TestPrincipalAmountOverride is CommonSetup { // Should allow overrideAmount higher than invoice amount vm.startPrank(underwriter); - bullaFactoring.approveInvoice(invoiceId, interestApr, spreadBps, upfrontBps, overrideAmount); + _approveInvoice(invoiceId, interestApr, spreadBps, upfrontBps, overrideAmount); vm.stopPrank(); (,,,,,,,,uint256 initialInvoiceValue,,,,,) = bullaFactoring.approvedInvoices(invoiceId); @@ -95,7 +95,7 @@ contract TestPrincipalAmountOverride is CommonSetup { vm.stopPrank(); vm.startPrank(underwriter); - bullaFactoring.approveInvoice(invoiceId, interestApr, spreadBps, upfrontBps, overrideAmount); + _approveInvoice(invoiceId, interestApr, spreadBps, upfrontBps, overrideAmount); vm.stopPrank(); (,,,,,,,,uint256 initialInvoiceValue,,,,,) = bullaFactoring.approvedInvoices(invoiceId); @@ -116,7 +116,7 @@ contract TestPrincipalAmountOverride is CommonSetup { vm.stopPrank(); vm.startPrank(underwriter); - bullaFactoring.approveInvoice(invoiceId, interestApr, spreadBps, upfrontBps, overrideAmount); + _approveInvoice(invoiceId, interestApr, spreadBps, upfrontBps, overrideAmount); vm.stopPrank(); (uint256 fundedAmountGross, , , , , , ) = bullaFactoring.calculateTargetFees(invoiceId, upfrontPercentage); @@ -136,13 +136,13 @@ contract TestPrincipalAmountOverride is CommonSetup { vm.stopPrank(); vm.startPrank(underwriter); - bullaFactoring.approveInvoice(invoiceId, interestApr, spreadBps, upfrontBps, overrideAmount); + _approveInvoice(invoiceId, interestApr, spreadBps, upfrontBps, overrideAmount); vm.stopPrank(); // Fund the invoice vm.startPrank(bob); bullaClaim.approve(address(bullaFactoring), invoiceId); - uint256 fundedAmount = bullaFactoring.fundInvoice(invoiceId, upfrontBps, address(0)); + uint256 fundedAmount = _fundInvoice(invoiceId, upfrontBps, address(0)); vm.stopPrank(); // Verify funding is based on overrideAmount amount @@ -162,7 +162,7 @@ contract TestPrincipalAmountOverride is CommonSetup { vm.stopPrank(); vm.startPrank(underwriter); - bullaFactoring.approveInvoice(invoiceId, interestApr, spreadBps, upfrontBps, overrideAmount); + _approveInvoice(invoiceId, interestApr, spreadBps, upfrontBps, overrideAmount); vm.stopPrank(); // Should revert due to insufficient funds @@ -170,7 +170,7 @@ contract TestPrincipalAmountOverride is CommonSetup { vm.startPrank(bob); bullaClaim.approve(address(bullaFactoring), invoiceId); vm.expectRevert(abi.encodeWithSelector(BullaFactoringV2_2.InsufficientFunds.selector, 500000, 560000)); - bullaFactoring.fundInvoice(invoiceId, upfrontBps, address(0)); + _fundInvoiceExpectRevert(invoiceId, upfrontBps, address(0)); vm.stopPrank(); } @@ -186,13 +186,13 @@ contract TestPrincipalAmountOverride is CommonSetup { vm.stopPrank(); vm.startPrank(underwriter); - bullaFactoring.approveInvoice(invoiceId, interestApr, spreadBps, upfrontBps, overrideAmount); + _approveInvoice(invoiceId, interestApr, spreadBps, upfrontBps, overrideAmount); vm.stopPrank(); // Fund the invoice vm.startPrank(bob); bullaClaim.approve(address(bullaFactoring), invoiceId); - bullaFactoring.fundInvoice(invoiceId, upfrontBps, address(0)); + _fundInvoice(invoiceId, upfrontBps, address(0)); vm.stopPrank(); // Fast forward and pay invoice @@ -215,13 +215,13 @@ contract TestPrincipalAmountOverride is CommonSetup { vm.stopPrank(); vm.startPrank(underwriter); - bullaFactoring.approveInvoice(invoiceId, interestApr, spreadBps, upfrontBps, overrideAmount); + _approveInvoice(invoiceId, interestApr, spreadBps, upfrontBps, overrideAmount); vm.stopPrank(); // Fund the invoice vm.startPrank(bob); bullaClaim.approve(address(bullaFactoring), invoiceId); - bullaFactoring.fundInvoice(invoiceId, upfrontBps, address(0)); + _fundInvoice(invoiceId, upfrontBps, address(0)); vm.stopPrank(); // Fast forward and pay full invoice @@ -248,12 +248,12 @@ contract TestPrincipalAmountOverride is CommonSetup { vm.stopPrank(); vm.startPrank(underwriter); - bullaFactoring.approveInvoice(invoiceId, interestApr, spreadBps, upfrontBps, overrideAmount); + _approveInvoice(invoiceId, interestApr, spreadBps, upfrontBps, overrideAmount); vm.stopPrank(); vm.startPrank(bob); bullaClaim.approve(address(bullaFactoring), invoiceId); - bullaFactoring.fundInvoice(invoiceId, upfrontBps, address(0)); + _fundInvoice(invoiceId, upfrontBps, address(0)); vm.stopPrank(); // Fast forward and pay @@ -283,12 +283,12 @@ contract TestPrincipalAmountOverride is CommonSetup { vm.stopPrank(); vm.startPrank(underwriter); - bullaFactoring.approveInvoice(invoiceIds[i], interestApr, spreadBps, upfrontBps, overrides[i]); + _approveInvoice(invoiceIds[i], interestApr, spreadBps, upfrontBps, overrides[i]); vm.stopPrank(); vm.startPrank(bob); bullaClaim.approve(address(bullaFactoring), invoiceIds[i]); - bullaFactoring.fundInvoice(invoiceIds[i], upfrontBps, address(0)); + _fundInvoice(invoiceIds[i], upfrontBps, address(0)); vm.stopPrank(); } @@ -320,7 +320,7 @@ contract TestPrincipalAmountOverride is CommonSetup { // Then approve with overrideAmount vm.startPrank(underwriter); - bullaFactoring.approveInvoice(invoiceId, interestApr, spreadBps, upfrontBps, overrideAmount); + _approveInvoice(invoiceId, interestApr, spreadBps, upfrontBps, overrideAmount); vm.stopPrank(); (,,,,,,,,uint256 initialInvoiceValue, uint256 initialPaidAmount,,,,) = bullaFactoring.approvedInvoices(invoiceId); @@ -342,7 +342,7 @@ contract TestPrincipalAmountOverride is CommonSetup { // Should revert when trying to approve fully paid invoice vm.startPrank(underwriter); vm.expectRevert(); - bullaFactoring.approveInvoice(invoiceId, interestApr, spreadBps, upfrontBps, 50000); + _approveInvoice(invoiceId, interestApr, spreadBps, upfrontBps, 50000); vm.stopPrank(); } @@ -359,13 +359,13 @@ contract TestPrincipalAmountOverride is CommonSetup { // 2. Approve with overrideAmount vm.startPrank(underwriter); - bullaFactoring.approveInvoice(invoiceId, interestApr, spreadBps, upfrontBps, overrideAmount); + _approveInvoice(invoiceId, interestApr, spreadBps, upfrontBps, overrideAmount); vm.stopPrank(); // 3. Fund vm.startPrank(bob); bullaClaim.approve(address(bullaFactoring), invoiceId); - uint256 fundedAmount = bullaFactoring.fundInvoice(invoiceId, upfrontBps, address(0)); + uint256 fundedAmount = _fundInvoice(invoiceId, upfrontBps, address(0)); vm.stopPrank(); assertTrue(fundedAmount > 0); @@ -390,12 +390,12 @@ contract TestPrincipalAmountOverride is CommonSetup { vm.stopPrank(); vm.startPrank(underwriter); - bullaFactoring.approveInvoice(invoiceId, interestApr, spreadBps, upfrontBps, overrideAmount); + _approveInvoice(invoiceId, interestApr, spreadBps, upfrontBps, overrideAmount); vm.stopPrank(); vm.startPrank(bob); bullaClaim.approve(address(bullaFactoring), invoiceId); - bullaFactoring.fundInvoice(invoiceId, upfrontBps, address(0)); + _fundInvoice(invoiceId, upfrontBps, address(0)); vm.stopPrank(); // Fast forward time @@ -429,7 +429,7 @@ contract TestPrincipalAmountOverride is CommonSetup { // Approve with conservative overrideAmount for risk management vm.startPrank(underwriter); - bullaFactoring.approveInvoice(invoiceId, higherInterestApr, higherSpreadBps, upfrontBps, conservativeOverrideAmount); + _approveInvoice(invoiceId, higherInterestApr, higherSpreadBps, upfrontBps, conservativeOverrideAmount); vm.stopPrank(); // Should allow partial exposure to high-risk invoice @@ -454,12 +454,12 @@ contract TestPrincipalAmountOverride is CommonSetup { vm.stopPrank(); vm.startPrank(underwriter); - bullaFactoring.approveInvoice(invoiceId, interestApr, spreadBps, upfrontBps, overrideAmount); + _approveInvoice(invoiceId, interestApr, spreadBps, upfrontBps, overrideAmount); vm.stopPrank(); vm.startPrank(bob); bullaClaim.approve(address(bullaFactoring), invoiceId); - uint256 funded = bullaFactoring.fundInvoice(invoiceId, upfrontBps, address(0)); + uint256 funded = _fundInvoice(invoiceId, upfrontBps, address(0)); vm.stopPrank(); // Should have remaining capacity for other invoices @@ -482,16 +482,16 @@ contract TestPrincipalAmountOverride is CommonSetup { // Approve both with different overrides vm.startPrank(underwriter); - bullaFactoring.approveInvoice(invoiceId1, interestApr, spreadBps, upfrontBps, overrideAmountoverride1); - bullaFactoring.approveInvoice(invoiceId2, interestApr, spreadBps, upfrontBps, overrideAmountoverride2); + _approveInvoice(invoiceId1, interestApr, spreadBps, upfrontBps, overrideAmountoverride1); + _approveInvoice(invoiceId2, interestApr, spreadBps, upfrontBps, overrideAmountoverride2); vm.stopPrank(); // Fund both vm.startPrank(bob); bullaClaim.approve(address(bullaFactoring), invoiceId1); bullaClaim.approve(address(bullaFactoring), invoiceId2); - uint256 funded1 = bullaFactoring.fundInvoice(invoiceId1, upfrontBps, address(0)); - uint256 funded2 = bullaFactoring.fundInvoice(invoiceId2, upfrontBps, address(0)); + uint256 funded1 = _fundInvoice(invoiceId1, upfrontBps, address(0)); + uint256 funded2 = _fundInvoice(invoiceId2, upfrontBps, address(0)); vm.stopPrank(); // Verify funding amounts are proportional to overrides diff --git a/test/foundry/TestProtocolFeeMissingCases.t.sol b/test/foundry/TestProtocolFeeMissingCases.t.sol index 17e8cdd..e8227e7 100644 --- a/test/foundry/TestProtocolFeeMissingCases.t.sol +++ b/test/foundry/TestProtocolFeeMissingCases.t.sol @@ -135,7 +135,7 @@ contract TestProtocolFeeMissingCases is CommonSetup { vm.stopPrank(); vm.startPrank(underwriter); - bullaFactoring.approveInvoice(invoiceId, interestApr, spreadBps, upfrontBps, 0); + _approveInvoice(invoiceId, interestApr, spreadBps, upfrontBps, 0); vm.stopPrank(); uint256 protocolFeeBalanceBefore = bullaFactoring.protocolFeeBalance(); @@ -143,7 +143,7 @@ contract TestProtocolFeeMissingCases is CommonSetup { vm.startPrank(bob); bullaClaim.approve(address(bullaFactoring), invoiceId); (, , , , uint256 expectedProtocolFee, , ) = bullaFactoring.calculateTargetFees(invoiceId, upfrontBps); - bullaFactoring.fundInvoice(invoiceId, upfrontBps, address(0)); + _fundInvoice(invoiceId, upfrontBps, address(0)); vm.stopPrank(); uint256 protocolFeeBalanceAfterFunding = bullaFactoring.protocolFeeBalance(); @@ -183,7 +183,7 @@ contract TestProtocolFeeMissingCases is CommonSetup { vm.stopPrank(); vm.startPrank(underwriter); - bullaFactoring.approveInvoice(invoiceId, interestApr, spreadBps, upfrontBps, 0); + _approveInvoice(invoiceId, interestApr, spreadBps, upfrontBps, 0); vm.stopPrank(); vm.startPrank(bob); @@ -196,7 +196,7 @@ contract TestProtocolFeeMissingCases is CommonSetup { uint256 availableFunds = bullaFactoring.totalAssets(); vm.expectRevert(abi.encodeWithSelector(BullaFactoringV2_2.InsufficientFunds.selector, availableFunds, totalRequired)); - bullaFactoring.fundInvoice(invoiceId, upfrontBps, address(0)); + _fundInvoiceExpectRevert(invoiceId, upfrontBps, address(0)); vm.stopPrank(); } @@ -214,7 +214,7 @@ contract TestProtocolFeeMissingCases is CommonSetup { vm.stopPrank(); vm.startPrank(underwriter); - bullaFactoring.approveInvoice(invoiceId, interestApr, spreadBps, upfrontBps, 0); + _approveInvoice(invoiceId, interestApr, spreadBps, upfrontBps, 0); vm.stopPrank(); vm.startPrank(bob); @@ -226,7 +226,7 @@ contract TestProtocolFeeMissingCases is CommonSetup { uint256 availableFunds = bullaFactoring.totalAssets(); uint256 protocolFeeBalanceBefore = bullaFactoring.protocolFeeBalance(); - bullaFactoring.fundInvoice(invoiceId, upfrontBps, address(0)); + _fundInvoice(invoiceId, upfrontBps, address(0)); uint256 protocolFeeBalanceAfter = bullaFactoring.protocolFeeBalance(); // Protocol fee is realized at funding time (upfront realization) @@ -244,7 +244,7 @@ contract TestProtocolFeeMissingCases is CommonSetup { vm.stopPrank(); vm.startPrank(underwriter); - bullaFactoring.approveInvoice(invoiceId, interestApr, spreadBps, upfrontBps, 0); + _approveInvoice(invoiceId, interestApr, spreadBps, upfrontBps, 0); vm.stopPrank(); (, , , , uint256 protocolFee, , ) = bullaFactoring.calculateTargetFees(invoiceId, upfrontBps); @@ -256,7 +256,7 @@ contract TestProtocolFeeMissingCases is CommonSetup { bullaClaim.approve(address(bullaFactoring), invoiceId); uint256 protocolFeeBalanceBefore = bullaFactoring.protocolFeeBalance(); - bullaFactoring.fundInvoice(invoiceId, upfrontBps, address(0)); + _fundInvoice(invoiceId, upfrontBps, address(0)); uint256 protocolFeeBalanceAfter = bullaFactoring.protocolFeeBalance(); vm.stopPrank(); @@ -278,7 +278,7 @@ contract TestProtocolFeeMissingCases is CommonSetup { vm.stopPrank(); vm.startPrank(underwriter); - bullaFactoring.approveInvoice(invoiceId, interestApr, spreadBps, upfrontBps, 0); + _approveInvoice(invoiceId, interestApr, spreadBps, upfrontBps, 0); vm.stopPrank(); (, , , , uint256 protocolFee, , ) = bullaFactoring.calculateTargetFees(invoiceId, upfrontBps); @@ -311,7 +311,7 @@ contract TestProtocolFeeMissingCases is CommonSetup { vm.stopPrank(); vm.startPrank(underwriter); - bullaFactoring.approveInvoice(invoiceId, interestApr, spreadBps, upfrontBps, 0); + _approveInvoice(invoiceId, interestApr, spreadBps, upfrontBps, 0); vm.stopPrank(); (, , , , uint256 protocolFee, , ) = bullaFactoring.calculateTargetFees(invoiceId, upfrontBps); @@ -347,12 +347,12 @@ contract TestProtocolFeeMissingCases is CommonSetup { vm.stopPrank(); vm.startPrank(underwriter); - bullaFactoring.approveInvoice(invoiceId, interestApr, spreadBps, upfrontBps, 0); + _approveInvoice(invoiceId, interestApr, spreadBps, upfrontBps, 0); vm.stopPrank(); vm.startPrank(bob); bullaClaim.approve(address(bullaFactoring), invoiceId); - bullaFactoring.fundInvoice(invoiceId, upfrontBps, address(0)); + _fundInvoice(invoiceId, upfrontBps, address(0)); vm.stopPrank(); // Pay invoice to realize protocol fees (reconciliation happens automatically) @@ -394,12 +394,12 @@ contract TestProtocolFeeMissingCases is CommonSetup { vm.stopPrank(); vm.startPrank(underwriter); - bullaFactoring.approveInvoice(invoiceId, interestApr, spreadBps, upfrontBps, 0); + _approveInvoice(invoiceId, interestApr, spreadBps, upfrontBps, 0); vm.stopPrank(); vm.startPrank(bob); bullaClaim.approve(address(bullaFactoring), invoiceId); - bullaFactoring.fundInvoice(invoiceId, upfrontBps, address(0)); + _fundInvoice(invoiceId, upfrontBps, address(0)); vm.stopPrank(); // Pay invoice to realize protocol fees (reconciliation happens automatically) @@ -451,12 +451,12 @@ contract TestProtocolFeeMissingCases is CommonSetup { vm.stopPrank(); vm.startPrank(underwriter); - bullaFactoring.approveInvoice(invoiceId, interestApr, spreadBps, upfrontBps, 0); + _approveInvoice(invoiceId, interestApr, spreadBps, upfrontBps, 0); vm.stopPrank(); vm.startPrank(bob); bullaClaim.approve(address(bullaFactoring), invoiceId); - bullaFactoring.fundInvoice(invoiceId, upfrontBps, address(0)); + _fundInvoice(invoiceId, upfrontBps, address(0)); vm.stopPrank(); // Pay invoice to realize protocol fees (reconciliation happens automatically) @@ -498,7 +498,7 @@ contract TestProtocolFeeMissingCases is CommonSetup { vm.stopPrank(); vm.startPrank(underwriter); - bullaFactoring.approveInvoice(invoiceId, interestApr, spreadBps, upfrontBps, 0); + _approveInvoice(invoiceId, interestApr, spreadBps, upfrontBps, 0); vm.stopPrank(); (, , , , uint256 protocolFee, , ) = bullaFactoring.calculateTargetFees(invoiceId, upfrontBps); @@ -522,7 +522,7 @@ contract TestProtocolFeeMissingCases is CommonSetup { vm.stopPrank(); vm.startPrank(underwriter); - bullaFactoring.approveInvoice(invoiceId, interestApr, spreadBps, upfrontBps, 0); + _approveInvoice(invoiceId, interestApr, spreadBps, upfrontBps, 0); vm.stopPrank(); (, , , , uint256 protocolFee, , ) = bullaFactoring.calculateTargetFees(invoiceId, upfrontBps); @@ -530,7 +530,7 @@ contract TestProtocolFeeMissingCases is CommonSetup { vm.startPrank(bob); bullaClaim.approve(address(bullaFactoring), invoiceId); - bullaFactoring.fundInvoice(invoiceId, upfrontBps, address(0)); + _fundInvoice(invoiceId, upfrontBps, address(0)); vm.stopPrank(); // Pay invoice to realize protocol fees (reconciliation happens automatically) @@ -571,7 +571,7 @@ contract TestProtocolFeeMissingCases is CommonSetup { vm.stopPrank(); vm.startPrank(underwriter); - bullaFactoring.approveInvoice(invoiceId, interestApr, spreadBps, upfrontBps, 0); + _approveInvoice(invoiceId, interestApr, spreadBps, upfrontBps, 0); vm.stopPrank(); // Calculate expected protocol fee @@ -583,7 +583,7 @@ contract TestProtocolFeeMissingCases is CommonSetup { vm.startPrank(bob); bullaClaim.approve(address(bullaFactoring), invoiceId); - bullaFactoring.fundInvoice(invoiceId, upfrontBps, address(0)); + _fundInvoice(invoiceId, upfrontBps, address(0)); vm.stopPrank(); uint256 bobBalanceAfterFunding = asset.balanceOf(bob); @@ -678,14 +678,14 @@ contract TestProtocolFeeMissingCases is CommonSetup { vm.stopPrank(); vm.startPrank(underwriter); - bullaFactoring.approveInvoice(invoiceId, interestApr, spreadBps, upfrontBps, 0); + _approveInvoice(invoiceId, interestApr, spreadBps, upfrontBps, 0); vm.stopPrank(); (, , , , uint256 expectedProtocolFee, , ) = bullaFactoring.calculateTargetFees(invoiceId, upfrontBps); vm.startPrank(bob); bullaClaim.approve(address(bullaFactoring), invoiceId); - bullaFactoring.fundInvoice(invoiceId, upfrontBps, address(0)); + _fundInvoice(invoiceId, upfrontBps, address(0)); vm.stopPrank(); uint256 bobBalanceAfterFunding = asset.balanceOf(bob); @@ -829,7 +829,7 @@ contract TestProtocolFeeMissingCases is CommonSetup { // Approve invoice vm.startPrank(underwriter); - bullaFactoring.approveInvoice(invoiceId, interestApr, spreadBps, upfrontBps, 0); + _approveInvoice(invoiceId, interestApr, spreadBps, upfrontBps, 0); vm.stopPrank(); // Calculate expected fees @@ -841,7 +841,7 @@ contract TestProtocolFeeMissingCases is CommonSetup { // Fund invoice vm.startPrank(bob); bullaClaim.approve(address(bullaFactoring), invoiceId); - bullaFactoring.fundInvoice(invoiceId, upfrontBps, address(0)); + _fundInvoice(invoiceId, upfrontBps, address(0)); vm.stopPrank(); uint256 creditor_afterFunding = asset.balanceOf(bob); @@ -1065,7 +1065,7 @@ contract TestProtocolFeeMissingCases is CommonSetup { vm.stopPrank(); vm.startPrank(underwriter); - bullaFactoring.approveInvoice(invoiceIds[i], interestApr, spreadBps, upfrontBps, 0); + _approveInvoice(invoiceIds[i], interestApr, spreadBps, upfrontBps, 0); vm.stopPrank(); // Calculate expected protocol fee @@ -1074,7 +1074,7 @@ contract TestProtocolFeeMissingCases is CommonSetup { vm.startPrank(bob); bullaClaim.approve(address(bullaFactoring), invoiceIds[i]); - bullaFactoring.fundInvoice(invoiceIds[i], upfrontBps, address(0)); + _fundInvoice(invoiceIds[i], upfrontBps, address(0)); vm.stopPrank(); } @@ -1111,12 +1111,12 @@ contract TestProtocolFeeMissingCases is CommonSetup { vm.stopPrank(); vm.startPrank(underwriter); - bullaFactoring.approveInvoice(invoiceId, interestApr, spreadBps, upfrontBps, 0); + _approveInvoice(invoiceId, interestApr, spreadBps, upfrontBps, 0); vm.stopPrank(); vm.startPrank(bob); bullaClaim.approve(address(bullaFactoring), invoiceId); - bullaFactoring.fundInvoice(invoiceId, upfrontBps, address(0)); + _fundInvoice(invoiceId, upfrontBps, address(0)); vm.stopPrank(); // Pay invoice to trigger reconciliation and protocol fee realization diff --git a/test/foundry/TestQueueIndexBug.t.sol b/test/foundry/TestQueueIndexBug.t.sol index a205e11..c61d6b6 100644 --- a/test/foundry/TestQueueIndexBug.t.sol +++ b/test/foundry/TestQueueIndexBug.t.sol @@ -71,11 +71,11 @@ contract TestQueueIndexBug is CommonSetup { vm.prank(alice_investor); uint256 invoiceId = createClaim(alice_investor, bob_investor, 1000000, dueBy); vm.prank(underwriter); - bullaFactoring.approveInvoice(invoiceId, 1000, 100, 10000, 0); + _approveInvoice(invoiceId, 1000, 100, 10000, 0); vm.prank(alice_investor); bullaClaim.approve(address(bullaFactoring), invoiceId); vm.prank(alice_investor); - bullaFactoring.fundInvoice(invoiceId, 10000, address(0)); + _fundInvoice(invoiceId, 10000, address(0)); console.log("Available liquidity after deployment:", bullaFactoring.maxRedeem()); diff --git a/test/foundry/TestReconciliationOfPaidInvoices.t.sol b/test/foundry/TestReconciliationOfPaidInvoices.t.sol index d1b710a..fffaf0c 100644 --- a/test/foundry/TestReconciliationOfPaidInvoices.t.sol +++ b/test/foundry/TestReconciliationOfPaidInvoices.t.sol @@ -38,12 +38,12 @@ contract TestReconciliationOfPaidInvoices is CommonSetup { vm.stopPrank(); vm.startPrank(underwriter); - bullaFactoring.approveInvoice(invoiceId, interestApr, spreadBps, upfrontBps, 0); + _approveInvoice(invoiceId, interestApr, spreadBps, upfrontBps, 0); vm.stopPrank(); vm.startPrank(bob); bullaClaim.approve(address(bullaFactoring), invoiceId); - bullaFactoring.fundInvoice(invoiceId, upfrontBps, address(0)); + _fundInvoice(invoiceId, upfrontBps, address(0)); vm.stopPrank(); // Pay invoice @@ -91,12 +91,12 @@ contract TestReconciliationOfPaidInvoices is CommonSetup { vm.stopPrank(); vm.startPrank(underwriter); - bullaFactoring.approveInvoice(invoiceId, interestApr, spreadBps, upfrontBps, 0); + _approveInvoice(invoiceId, interestApr, spreadBps, upfrontBps, 0); vm.stopPrank(); vm.startPrank(bob); bullaClaim.approve(address(bullaFactoring), invoiceId); - bullaFactoring.fundInvoice(invoiceId, upfrontBps, address(0)); + _fundInvoice(invoiceId, upfrontBps, address(0)); vm.stopPrank(); // Pay invoice @@ -144,12 +144,12 @@ contract TestReconciliationOfPaidInvoices is CommonSetup { vm.stopPrank(); vm.startPrank(underwriter); - bullaFactoring.approveInvoice(invoiceId, interestApr, spreadBps, upfrontBps, 0); + _approveInvoice(invoiceId, interestApr, spreadBps, upfrontBps, 0); vm.stopPrank(); vm.startPrank(bob); bullaClaim.approve(address(bullaFactoring), invoiceId); - bullaFactoring.fundInvoice(invoiceId, upfrontBps, address(0)); + _fundInvoice(invoiceId, upfrontBps, address(0)); vm.stopPrank(); // Pay invoice diff --git a/test/foundry/TestRedemptionQueueInsufficientFunds.t.sol b/test/foundry/TestRedemptionQueueInsufficientFunds.t.sol index 94b40f9..b23fbbf 100644 --- a/test/foundry/TestRedemptionQueueInsufficientFunds.t.sol +++ b/test/foundry/TestRedemptionQueueInsufficientFunds.t.sol @@ -99,7 +99,7 @@ contract TestRedemptionQueueInsufficientFunds is CommonSetup { // Underwriter approves vm.prank(underwriter); - bullaFactoring.approveInvoice(invoiceId, 1000, 100, 10000, 0); + _approveInvoice(invoiceId, 1000, 100, 10000, 0); // Bob approves NFT transfer vm.prank(bob); @@ -107,7 +107,7 @@ contract TestRedemptionQueueInsufficientFunds is CommonSetup { // Bob funds invoice vm.prank(bob); - bullaFactoring.fundInvoice(invoiceId, 10000, address(0)); + _fundInvoice(invoiceId, 10000, address(0)); return invoiceId; } diff --git a/test/foundry/TestRedemptionQueueIntegration.t.sol b/test/foundry/TestRedemptionQueueIntegration.t.sol index 6473f33..7c2821a 100644 --- a/test/foundry/TestRedemptionQueueIntegration.t.sol +++ b/test/foundry/TestRedemptionQueueIntegration.t.sol @@ -120,12 +120,12 @@ contract TestRedemptionQueueIntegration is CommonSetup { vm.prank(alice); uint256 invoiceId = createClaim(alice, bob, invoiceAmount, dueBy); vm.prank(underwriter); - bullaFactoring.approveInvoice(invoiceId, 1000, 100, 8000, 0); + _approveInvoice(invoiceId, 1000, 100, 8000, 0); vm.prank(alice); bullaClaim.approve(address(bullaFactoring), invoiceId); vm.prank(alice); - bullaFactoring.fundInvoice(invoiceId, 8000, address(0)); + _fundInvoice(invoiceId, 8000, address(0)); // Alice attempts to redeem more than available - should partially redeem and queue vm.recordLogs(); @@ -153,12 +153,12 @@ contract TestRedemptionQueueIntegration is CommonSetup { vm.prank(bob); uint256 invoiceId = createClaim(bob, alice, depositAmount, dueBy); vm.prank(underwriter); - bullaFactoring.approveInvoice(invoiceId, 1000, 100, 10000, 0); + _approveInvoice(invoiceId, 1000, 100, 10000, 0); vm.prank(bob); bullaClaim.approve(address(bullaFactoring), invoiceId); vm.prank(bob); - bullaFactoring.fundInvoice(invoiceId, 10000, address(0)); + _fundInvoice(invoiceId, 10000, address(0)); // Alice attempts to redeem - should queue all shares vm.expectEmit(true, true, false, true); @@ -203,12 +203,12 @@ contract TestRedemptionQueueIntegration is CommonSetup { vm.prank(bob); uint256 invoiceId = createClaim(bob, alice, invoiceAmount, dueBy); vm.prank(underwriter); - bullaFactoring.approveInvoice(invoiceId, 1000, 100, 8000, 0); + _approveInvoice(invoiceId, 1000, 100, 8000, 0); vm.prank(bob); bullaClaim.approve(address(bullaFactoring), invoiceId); vm.prank(bob); - bullaFactoring.fundInvoice(invoiceId, 8000, address(0)); + _fundInvoice(invoiceId, 8000, address(0)); // Alice attempts to withdraw more than available - should partially withdraw and queue vm.recordLogs(); @@ -235,12 +235,12 @@ contract TestRedemptionQueueIntegration is CommonSetup { vm.prank(bob); uint256 invoiceId = createClaim(bob, alice, depositAmount, dueBy); vm.prank(underwriter); - bullaFactoring.approveInvoice(invoiceId, 1000, 100, 10000, 0); + _approveInvoice(invoiceId, 1000, 100, 10000, 0); vm.prank(bob); bullaClaim.approve(address(bullaFactoring), invoiceId); vm.prank(bob); - bullaFactoring.fundInvoice(invoiceId, 10000, address(0)); + _fundInvoice(invoiceId, 10000, address(0)); vm.recordLogs(); vm.expectEmit(true, true, false, true); @@ -275,11 +275,11 @@ contract TestRedemptionQueueIntegration is CommonSetup { vm.prank(david); uint256 invoiceId = createClaim(david, eve, 2500000, dueBy); vm.prank(underwriter); - bullaFactoring.approveInvoice(invoiceId, 1000, 100, 10000, 0); + _approveInvoice(invoiceId, 1000, 100, 10000, 0); vm.prank(david); bullaClaim.approve(address(bullaFactoring), invoiceId); vm.prank(david); - bullaFactoring.fundInvoice(invoiceId, 10000, address(0)); + _fundInvoice(invoiceId, 10000, address(0)); // Users queue redemptions in order: Alice, Bob, Charlie vm.prank(alice); @@ -381,11 +381,11 @@ contract TestRedemptionQueueIntegration is CommonSetup { vm.prank(bob); uint256 invoiceId = createClaim(bob, alice, 800000, dueBy); vm.prank(underwriter); - bullaFactoring.approveInvoice(invoiceId, 1000, 100, 9000, 0); + _approveInvoice(invoiceId, 1000, 100, 9000, 0); vm.prank(bob); bullaClaim.approve(address(bullaFactoring), invoiceId); vm.prank(bob); - bullaFactoring.fundInvoice(invoiceId, 9000, address(0)); + _fundInvoice(invoiceId, 9000, address(0)); vm.prank(alice); bullaFactoring.redeem(queueAmount, alice, alice); @@ -425,11 +425,11 @@ contract TestRedemptionQueueIntegration is CommonSetup { vm.prank(bob); uint256 invoiceId = createClaim(bob, alice, 800000, dueBy); vm.prank(underwriter); - bullaFactoring.approveInvoice(invoiceId, 1000, 100, 9000, 0); + _approveInvoice(invoiceId, 1000, 100, 9000, 0); vm.prank(bob); bullaClaim.approve(address(bullaFactoring), invoiceId); vm.prank(bob); - bullaFactoring.fundInvoice(invoiceId, 9000, address(0)); + _fundInvoice(invoiceId, 9000, address(0)); vm.recordLogs(); vm.prank(alice); @@ -464,11 +464,11 @@ contract TestRedemptionQueueIntegration is CommonSetup { vm.prank(david); uint256 invoiceId = createClaim(david, eve, 2500000, dueBy); vm.prank(underwriter); - bullaFactoring.approveInvoice(invoiceId, 1000, 100, 10000, 0); + _approveInvoice(invoiceId, 1000, 100, 10000, 0); vm.prank(david); bullaClaim.approve(address(bullaFactoring), invoiceId); vm.prank(david); - bullaFactoring.fundInvoice(invoiceId, 10000, address(0)); + _fundInvoice(invoiceId, 10000, address(0)); // Mixed redemption types vm.prank(alice); diff --git a/test/foundry/TestRedemptionQueueLimit.t.sol b/test/foundry/TestRedemptionQueueLimit.t.sol index 576b1bb..99df2c7 100644 --- a/test/foundry/TestRedemptionQueueLimit.t.sol +++ b/test/foundry/TestRedemptionQueueLimit.t.sol @@ -51,11 +51,11 @@ contract TestRedemptionQueueLimit is CommonSetup { vm.prank(bob); uint256 invoiceId = createClaim(bob, alice, 95000, dueBy); vm.startPrank(underwriter); - bullaFactoring.approveInvoice(invoiceId, interestApr, spreadBps, upfrontBps, 0); + _approveInvoice(invoiceId, interestApr, spreadBps, upfrontBps, 0); vm.stopPrank(); vm.startPrank(bob); bullaClaim.approve(address(bullaFactoring), invoiceId); - bullaFactoring.fundInvoice(invoiceId, upfrontBps, address(0)); + _fundInvoice(invoiceId, upfrontBps, address(0)); vm.stopPrank(); // Queue redemption @@ -80,11 +80,11 @@ contract TestRedemptionQueueLimit is CommonSetup { vm.prank(bob); uint256 invoiceId = createClaim(bob, alice, 95000, dueBy); vm.startPrank(underwriter); - bullaFactoring.approveInvoice(invoiceId, interestApr, spreadBps, upfrontBps, 0); + _approveInvoice(invoiceId, interestApr, spreadBps, upfrontBps, 0); vm.stopPrank(); vm.startPrank(bob); bullaClaim.approve(address(bullaFactoring), invoiceId); - bullaFactoring.fundInvoice(invoiceId, upfrontBps, address(0)); + _fundInvoice(invoiceId, upfrontBps, address(0)); vm.stopPrank(); // Queue redemption @@ -117,11 +117,11 @@ contract TestRedemptionQueueLimit is CommonSetup { vm.prank(bob); uint256 invoiceId = createClaim(bob, alice, 95000, dueBy); vm.startPrank(underwriter); - bullaFactoring.approveInvoice(invoiceId, interestApr, spreadBps, upfrontBps, 0); + _approveInvoice(invoiceId, interestApr, spreadBps, upfrontBps, 0); vm.stopPrank(); vm.startPrank(bob); bullaClaim.approve(address(bullaFactoring), invoiceId); - bullaFactoring.fundInvoice(invoiceId, upfrontBps, address(0)); + _fundInvoice(invoiceId, upfrontBps, address(0)); vm.stopPrank(); // Queue redemption @@ -161,11 +161,11 @@ contract TestRedemptionQueueLimit is CommonSetup { vm.prank(bob); uint256 invoiceId = createClaim(bob, alice, 98000, dueBy); vm.startPrank(underwriter); - bullaFactoring.approveInvoice(invoiceId, interestApr, spreadBps, upfrontBps, 0); + _approveInvoice(invoiceId, interestApr, spreadBps, upfrontBps, 0); vm.stopPrank(); vm.startPrank(bob); bullaClaim.approve(address(bullaFactoring), invoiceId); - bullaFactoring.fundInvoice(invoiceId, upfrontBps, address(0)); + _fundInvoice(invoiceId, upfrontBps, address(0)); vm.stopPrank(); // Queue 2 redemptions from existing balance @@ -214,11 +214,11 @@ contract TestRedemptionQueueLimit is CommonSetup { vm.prank(bob); uint256 invoiceId = createClaim(bob, alice, 95000, dueBy); vm.startPrank(underwriter); - bullaFactoring.approveInvoice(invoiceId, interestApr, spreadBps, upfrontBps, 0); + _approveInvoice(invoiceId, interestApr, spreadBps, upfrontBps, 0); vm.stopPrank(); vm.startPrank(bob); bullaClaim.approve(address(bullaFactoring), invoiceId); - bullaFactoring.fundInvoice(invoiceId, upfrontBps, address(0)); + _fundInvoice(invoiceId, upfrontBps, address(0)); vm.stopPrank(); // Queue one redemption diff --git a/test/foundry/TestRedemptionQueueOversizedRequest.t.sol b/test/foundry/TestRedemptionQueueOversizedRequest.t.sol index e54ede3..a36fa33 100644 --- a/test/foundry/TestRedemptionQueueOversizedRequest.t.sol +++ b/test/foundry/TestRedemptionQueueOversizedRequest.t.sol @@ -29,11 +29,11 @@ contract TestRedemptionQueueOversizedRequest is CommonSetup { uint256 invoiceId = createClaim(bob, alice, invoiceAmount, dueBy); vm.prank(underwriter); - bullaFactoring.approveInvoice(invoiceId, interestApr, spreadBps, upfrontBps, 0); + _approveInvoice(invoiceId, interestApr, spreadBps, upfrontBps, 0); vm.startPrank(bob); bullaClaim.approve(address(bullaFactoring), invoiceId); - bullaFactoring.fundInvoice(invoiceId, upfrontBps, address(0)); + _fundInvoice(invoiceId, upfrontBps, address(0)); vm.stopPrank(); // Get remaining liquidity after invoice funding @@ -69,11 +69,11 @@ contract TestRedemptionQueueOversizedRequest is CommonSetup { uint256 invoiceId = createClaim(bob, alice, invoiceAmount, dueBy); vm.prank(underwriter); - bullaFactoring.approveInvoice(invoiceId, interestApr, spreadBps, upfrontBps, 0); + _approveInvoice(invoiceId, interestApr, spreadBps, upfrontBps, 0); vm.startPrank(bob); bullaClaim.approve(address(bullaFactoring), invoiceId); - bullaFactoring.fundInvoice(invoiceId, upfrontBps, address(0)); + _fundInvoice(invoiceId, upfrontBps, address(0)); vm.stopPrank(); // Get Alice's max assets (what her shares are worth) diff --git a/test/foundry/TestRedemptionQueueStealing.t.sol b/test/foundry/TestRedemptionQueueStealing.t.sol index 5a4a70c..98df636 100644 --- a/test/foundry/TestRedemptionQueueStealing.t.sol +++ b/test/foundry/TestRedemptionQueueStealing.t.sol @@ -47,11 +47,11 @@ contract TestRedemptionQueueStealing is CommonSetup { uint256 invoiceId = createClaim(victim, attacker, invoiceAmount, dueBy); vm.prank(underwriter); - bullaFactoring.approveInvoice(invoiceId, interestApr, spreadBps, upfrontBps, 0); + _approveInvoice(invoiceId, interestApr, spreadBps, upfrontBps, 0); vm.startPrank(victim); bullaClaim.approve(address(bullaFactoring), invoiceId); - bullaFactoring.fundInvoice(invoiceId, upfrontBps, address(0)); + _fundInvoice(invoiceId, upfrontBps, address(0)); vm.stopPrank(); // Create initial queue entry (attacker queues their own shares first) @@ -149,11 +149,11 @@ contract TestRedemptionQueueStealing is CommonSetup { uint256 invoiceId = createClaim(victim, attacker, invoiceAmount, dueBy); vm.prank(underwriter); - bullaFactoring.approveInvoice(invoiceId, interestApr, spreadBps, upfrontBps, 0); + _approveInvoice(invoiceId, interestApr, spreadBps, upfrontBps, 0); vm.startPrank(victim); bullaClaim.approve(address(bullaFactoring), invoiceId); - bullaFactoring.fundInvoice(invoiceId, upfrontBps, address(0)); + _fundInvoice(invoiceId, upfrontBps, address(0)); vm.stopPrank(); // Create initial queue entry diff --git a/test/foundry/TestScalingCurves.t.sol b/test/foundry/TestScalingCurves.t.sol index bb93485..5607593 100644 --- a/test/foundry/TestScalingCurves.t.sol +++ b/test/foundry/TestScalingCurves.t.sol @@ -38,12 +38,12 @@ contract TestScalingCurves is CommonSetup { vm.prank(bob); uint256 id = createClaim(bob, alice, invoiceAmount, dueBy); vm.prank(underwriter); - bullaFactoring.approveInvoice(id, 500, 300, 10000, 0); + _approveInvoice(id, 500, 300, 10000, 0); vm.prank(bob); IERC721(address(bullaClaim)).approve(address(bullaFactoring), id); uint256 g = gasleft(); vm.prank(bob); - bullaFactoring.fundInvoice(id, 10000, address(0)); + _fundInvoice(id, 10000, address(0)); dataPoints[0] = g - gasleft(); // Get to 10 @@ -53,7 +53,7 @@ contract TestScalingCurves is CommonSetup { id = _approveOneInvoice(invoiceAmount); g = gasleft(); vm.prank(bob); - bullaFactoring.fundInvoice(id, 10000, address(0)); + _fundInvoice(id, 10000, address(0)); dataPoints[1] = g - gasleft(); // Get to 50 @@ -63,7 +63,7 @@ contract TestScalingCurves is CommonSetup { id = _approveOneInvoice(invoiceAmount); g = gasleft(); vm.prank(bob); - bullaFactoring.fundInvoice(id, 10000, address(0)); + _fundInvoice(id, 10000, address(0)); dataPoints[2] = g - gasleft(); // Get to 100 @@ -73,7 +73,7 @@ contract TestScalingCurves is CommonSetup { id = _approveOneInvoice(invoiceAmount); g = gasleft(); vm.prank(bob); - bullaFactoring.fundInvoice(id, 10000, address(0)); + _fundInvoice(id, 10000, address(0)); dataPoints[3] = g - gasleft(); // Get to 200 @@ -83,7 +83,7 @@ contract TestScalingCurves is CommonSetup { id = _approveOneInvoice(invoiceAmount); g = gasleft(); vm.prank(bob); - bullaFactoring.fundInvoice(id, 10000, address(0)); + _fundInvoice(id, 10000, address(0)); dataPoints[4] = g - gasleft(); console.log("\nData Points:"); @@ -378,18 +378,18 @@ contract TestScalingCurves is CommonSetup { vm.prank(bob); uint256 id = createClaim(bob, alice, amount, dueBy); vm.prank(underwriter); - bullaFactoring.approveInvoice(id, 500, 300, 10000, 0); + _approveInvoice(id, 500, 300, 10000, 0); vm.prank(bob); IERC721(address(bullaClaim)).approve(address(bullaFactoring), id); vm.prank(bob); - bullaFactoring.fundInvoice(id, 10000, address(0)); + _fundInvoice(id, 10000, address(0)); } function _approveOneInvoice(uint256 amount) internal returns (uint256) { vm.prank(bob); uint256 id = createClaim(bob, alice, amount, dueBy); vm.prank(underwriter); - bullaFactoring.approveInvoice(id, 500, 300, 10000, 0); + _approveInvoice(id, 500, 300, 10000, 0); vm.prank(bob); IERC721(address(bullaClaim)).approve(address(bullaFactoring), id); return id; diff --git a/test/foundry/TestUnfactoringTriggersReconciliation.t.sol b/test/foundry/TestUnfactoringTriggersReconciliation.t.sol index 6fd438b..2be7b3f 100644 --- a/test/foundry/TestUnfactoringTriggersReconciliation.t.sol +++ b/test/foundry/TestUnfactoringTriggersReconciliation.t.sol @@ -52,16 +52,16 @@ contract TestUnfactoringTriggersReconciliation is CommonSetup { // Underwriter approves both invoices vm.startPrank(underwriter); - bullaFactoring.approveInvoice(invoiceId1, interestApr, spreadBps, upfrontBps, 0); - bullaFactoring.approveInvoice(invoiceId2, interestApr, spreadBps, upfrontBps, 0); + _approveInvoice(invoiceId1, interestApr, spreadBps, upfrontBps, 0); + _approveInvoice(invoiceId2, interestApr, spreadBps, upfrontBps, 0); vm.stopPrank(); // Bob funds both invoices vm.startPrank(bob); bullaClaim.approve(address(bullaFactoring), invoiceId1); bullaClaim.approve(address(bullaFactoring), invoiceId2); - bullaFactoring.fundInvoice(invoiceId1, upfrontBps, address(0)); - bullaFactoring.fundInvoice(invoiceId2, upfrontBps, address(0)); + _fundInvoice(invoiceId1, upfrontBps, address(0)); + _fundInvoice(invoiceId2, upfrontBps, address(0)); vm.stopPrank(); // Wait 30 days diff --git a/test/foundry/TestUpfrontProtocolFee.t.sol b/test/foundry/TestUpfrontProtocolFee.t.sol index 68a1049..466ba4d 100644 --- a/test/foundry/TestUpfrontProtocolFee.t.sol +++ b/test/foundry/TestUpfrontProtocolFee.t.sol @@ -28,7 +28,7 @@ contract TestUpfrontProtocolFee is CommonSetup { vm.stopPrank(); vm.startPrank(underwriter); - bullaFactoring.approveInvoice(invoiceId, interestApr, spreadBps, upfrontBps, 0); + _approveInvoice(invoiceId, interestApr, spreadBps, upfrontBps, 0); vm.stopPrank(); uint256 protocolFeeBalanceBefore = bullaFactoring.protocolFeeBalance(); @@ -37,7 +37,7 @@ contract TestUpfrontProtocolFee is CommonSetup { // Fund invoice vm.startPrank(bob); bullaClaim.approve(address(bullaFactoring), invoiceId); - bullaFactoring.fundInvoice(invoiceId, upfrontBps, address(0)); + _fundInvoice(invoiceId, upfrontBps, address(0)); vm.stopPrank(); uint256 protocolFeeBalanceAfter = bullaFactoring.protocolFeeBalance(); @@ -62,12 +62,12 @@ contract TestUpfrontProtocolFee is CommonSetup { vm.stopPrank(); vm.startPrank(underwriter); - bullaFactoring.approveInvoice(invoiceId, interestApr, spreadBps, upfrontBps, 0); + _approveInvoice(invoiceId, interestApr, spreadBps, upfrontBps, 0); vm.stopPrank(); vm.startPrank(bob); bullaClaim.approve(address(bullaFactoring), invoiceId); - bullaFactoring.fundInvoice(invoiceId, upfrontBps, address(0)); + _fundInvoice(invoiceId, upfrontBps, address(0)); vm.stopPrank(); uint256 protocolFeeBalance = bullaFactoring.protocolFeeBalance(); @@ -99,12 +99,12 @@ contract TestUpfrontProtocolFee is CommonSetup { vm.stopPrank(); vm.startPrank(underwriter); - bullaFactoring.approveInvoice(invoiceId, interestApr, spreadBps, upfrontBps, 0); + _approveInvoice(invoiceId, interestApr, spreadBps, upfrontBps, 0); vm.stopPrank(); vm.startPrank(bob); bullaClaim.approve(address(bullaFactoring), invoiceId); - bullaFactoring.fundInvoice(invoiceId, upfrontBps, address(0)); + _fundInvoice(invoiceId, upfrontBps, address(0)); vm.stopPrank(); uint256 protocolFeeAfterFunding = bullaFactoring.protocolFeeBalance(); @@ -136,12 +136,12 @@ contract TestUpfrontProtocolFee is CommonSetup { vm.stopPrank(); vm.startPrank(underwriter); - bullaFactoring.approveInvoice(invoiceId, interestApr, spreadBps, upfrontBps, 0); + _approveInvoice(invoiceId, interestApr, spreadBps, upfrontBps, 0); vm.stopPrank(); vm.startPrank(bob); bullaClaim.approve(address(bullaFactoring), invoiceId); - bullaFactoring.fundInvoice(invoiceId, upfrontBps, address(0)); + _fundInvoice(invoiceId, upfrontBps, address(0)); vm.stopPrank(); uint256 protocolFeeAfterFunding = bullaFactoring.protocolFeeBalance(); @@ -174,12 +174,12 @@ contract TestUpfrontProtocolFee is CommonSetup { vm.stopPrank(); vm.startPrank(underwriter); - bullaFactoring.approveInvoice(invoiceId, interestApr, spreadBps, upfrontBps, 0); + _approveInvoice(invoiceId, interestApr, spreadBps, upfrontBps, 0); vm.stopPrank(); vm.startPrank(bob); bullaClaim.approve(address(bullaFactoring), invoiceId); - bullaFactoring.fundInvoice(invoiceId, upfrontBps, address(0)); + _fundInvoice(invoiceId, upfrontBps, address(0)); vm.stopPrank(); uint256 capitalAccountAfter = bullaFactoring.calculateCapitalAccount(); @@ -200,12 +200,12 @@ contract TestUpfrontProtocolFee is CommonSetup { vm.stopPrank(); vm.startPrank(underwriter); - bullaFactoring.approveInvoice(invoiceId, interestApr, spreadBps, upfrontBps, 0); + _approveInvoice(invoiceId, interestApr, spreadBps, upfrontBps, 0); vm.stopPrank(); vm.startPrank(bob); bullaClaim.approve(address(bullaFactoring), invoiceId); - bullaFactoring.fundInvoice(invoiceId, upfrontBps, address(0)); + _fundInvoice(invoiceId, upfrontBps, address(0)); vm.stopPrank(); // Protocol withdraws fees immediately @@ -253,12 +253,12 @@ contract TestUpfrontProtocolFee is CommonSetup { vm.stopPrank(); vm.startPrank(underwriter); - bullaFactoring.approveInvoice(invoiceId, interestApr, spreadBps, upfrontBps, 0); + _approveInvoice(invoiceId, interestApr, spreadBps, upfrontBps, 0); vm.stopPrank(); vm.startPrank(bob); bullaClaim.approve(address(bullaFactoring), invoiceId); - bullaFactoring.fundInvoice(invoiceId, upfrontBps, address(0)); + _fundInvoice(invoiceId, upfrontBps, address(0)); vm.stopPrank(); // Protocol withdraws fees immediately @@ -293,14 +293,14 @@ contract TestUpfrontProtocolFee is CommonSetup { vm.stopPrank(); vm.startPrank(underwriter); - bullaFactoring.approveInvoice(invoiceId, interestApr, spreadBps, upfrontBps, 0); + _approveInvoice(invoiceId, interestApr, spreadBps, upfrontBps, 0); vm.stopPrank(); uint256 feeBefore = bullaFactoring.protocolFeeBalance(); vm.startPrank(bob); bullaClaim.approve(address(bullaFactoring), invoiceId); - bullaFactoring.fundInvoice(invoiceId, upfrontBps, address(0)); + _fundInvoice(invoiceId, upfrontBps, address(0)); vm.stopPrank(); uint256 feeAfter = bullaFactoring.protocolFeeBalance(); diff --git a/test/foundry/TestViewPoolStatusPagination.t.sol b/test/foundry/TestViewPoolStatusPagination.t.sol index 2bd4aeb..c04ad87 100644 --- a/test/foundry/TestViewPoolStatusPagination.t.sol +++ b/test/foundry/TestViewPoolStatusPagination.t.sol @@ -29,11 +29,11 @@ contract TestViewPoolStatusPagination is CommonSetup { vm.prank(bob); invoiceIds[i] = createClaim(bob, alice, 100, dueBy); vm.startPrank(underwriter); - bullaFactoring.approveInvoice(invoiceIds[i], interestApr, spreadBps, upfrontBps, 0); + _approveInvoice(invoiceIds[i], interestApr, spreadBps, upfrontBps, 0); vm.stopPrank(); vm.startPrank(bob); bullaClaim.approve(address(bullaFactoring), invoiceIds[i]); - bullaFactoring.fundInvoice(invoiceIds[i], upfrontBps, address(0)); + _fundInvoice(invoiceIds[i], upfrontBps, address(0)); vm.stopPrank(); } @@ -62,11 +62,11 @@ contract TestViewPoolStatusPagination is CommonSetup { vm.prank(bob); invoiceIds[i] = createClaim(bob, alice, 100, dueBy); vm.startPrank(underwriter); - bullaFactoring.approveInvoice(invoiceIds[i], interestApr, spreadBps, upfrontBps, 0); + _approveInvoice(invoiceIds[i], interestApr, spreadBps, upfrontBps, 0); vm.stopPrank(); vm.startPrank(bob); bullaClaim.approve(address(bullaFactoring), invoiceIds[i]); - bullaFactoring.fundInvoice(invoiceIds[i], upfrontBps, address(0)); + _fundInvoice(invoiceIds[i], upfrontBps, address(0)); vm.stopPrank(); } @@ -118,11 +118,11 @@ contract TestViewPoolStatusPagination is CommonSetup { vm.prank(bob); uint256 invoiceId = createClaim(bob, alice, 100, dueBy); vm.startPrank(underwriter); - bullaFactoring.approveInvoice(invoiceId, interestApr, spreadBps, upfrontBps, 0); + _approveInvoice(invoiceId, interestApr, spreadBps, upfrontBps, 0); vm.stopPrank(); vm.startPrank(bob); bullaClaim.approve(address(bullaFactoring), invoiceId); - bullaFactoring.fundInvoice(invoiceId, upfrontBps, address(0)); + _fundInvoice(invoiceId, upfrontBps, address(0)); vm.stopPrank(); } @@ -151,11 +151,11 @@ contract TestViewPoolStatusPagination is CommonSetup { vm.prank(bob); invoiceIds[i] = createClaim(bob, alice, 100, dueBy); vm.startPrank(underwriter); - bullaFactoring.approveInvoice(invoiceIds[i], interestApr, spreadBps, upfrontBps, 0); + _approveInvoice(invoiceIds[i], interestApr, spreadBps, upfrontBps, 0); vm.stopPrank(); vm.startPrank(bob); bullaClaim.approve(address(bullaFactoring), invoiceIds[i]); - bullaFactoring.fundInvoice(invoiceIds[i], upfrontBps, address(0)); + _fundInvoice(invoiceIds[i], upfrontBps, address(0)); vm.stopPrank(); } diff --git a/test/foundry/TestWithdraw.t.sol b/test/foundry/TestWithdraw.t.sol index 45284e4..fef1f9e 100644 --- a/test/foundry/TestWithdraw.t.sol +++ b/test/foundry/TestWithdraw.t.sol @@ -43,13 +43,13 @@ contract TestWithdraw is CommonSetup { // Underwriter approves the invoice vm.startPrank(underwriter); - bullaFactoring.approveInvoice(invoiceId, interestApr, spreadBps, upfrontBps, 0); + _approveInvoice(invoiceId, interestApr, spreadBps, upfrontBps, 0); vm.stopPrank(); // creditor funds the invoice vm.startPrank(bob); bullaClaim.approve(address(bullaFactoring), invoiceId); - bullaFactoring.fundInvoice(invoiceId, upfrontBps, address(0)); + _fundInvoice(invoiceId, upfrontBps, address(0)); vm.stopPrank(); // Simulate debtor paying in 30 days instead of 60 @@ -94,12 +94,12 @@ contract TestWithdraw is CommonSetup { vm.prank(bob); uint256 invoiceId1 = createClaim(bob, alice, invoiceAmount, dueDate); vm.startPrank(underwriter); - bullaFactoring.approveInvoice(invoiceId1, interestApr, spreadBps, 10000, 0); // 100% upfront + _approveInvoice(invoiceId1, interestApr, spreadBps, 10000, 0); // 100% upfront vm.stopPrank(); vm.startPrank(bob); bullaClaim.approve(address(bullaFactoring), invoiceId1); - bullaFactoring.fundInvoice(invoiceId1, 10000, address(0)); + _fundInvoice(invoiceId1, 10000, address(0)); vm.stopPrank(); // Simulate invoices being paid on time @@ -129,12 +129,12 @@ contract TestWithdraw is CommonSetup { vm.prank(bob); uint256 invoiceId2 = createClaim(bob, alice, invoiceAmount, dueDate); vm.startPrank(underwriter); - bullaFactoring.approveInvoice(invoiceId2, interestApr, spreadBps, 10000, 0); // 100% upfront + _approveInvoice(invoiceId2, interestApr, spreadBps, 10000, 0); // 100% upfront vm.stopPrank(); vm.startPrank(bob); bullaClaim.approve(address(bullaFactoring), invoiceId2); - bullaFactoring.fundInvoice(invoiceId2, 10000, address(0)); + _fundInvoice(invoiceId2, 10000, address(0)); vm.stopPrank(); // Simulate invoices being paid on time @@ -175,13 +175,13 @@ contract TestWithdraw is CommonSetup { // Underwriter approves the invoice vm.startPrank(underwriter); - bullaFactoring.approveInvoice(invoiceId, interestApr, spreadBps, upfrontBps, 0); + _approveInvoice(invoiceId, interestApr, spreadBps, upfrontBps, 0); vm.stopPrank(); // creditor funds the invoice vm.startPrank(bob); bullaClaim.approve(address(bullaFactoring), invoiceId); - bullaFactoring.fundInvoice(invoiceId, upfrontBps, address(0)); + _fundInvoice(invoiceId, upfrontBps, address(0)); vm.stopPrank(); // Simulate debtor paying in 30 days instead of 60 @@ -227,13 +227,13 @@ contract TestWithdraw is CommonSetup { // Underwriter approves the invoice vm.startPrank(underwriter); - bullaFactoring.approveInvoice(invoiceId, interestApr, spreadBps, upfrontBps, 0); + _approveInvoice(invoiceId, interestApr, spreadBps, upfrontBps, 0); vm.stopPrank(); // creditor funds the invoice vm.startPrank(bob); bullaClaim.approve(address(bullaFactoring), invoiceId); - bullaFactoring.fundInvoice(invoiceId, upfrontBps, address(0)); + _fundInvoice(invoiceId, upfrontBps, address(0)); vm.stopPrank(); // Simulate debtor paying in 30 days instead of 60 diff --git a/test/foundry/helpers/Builders.sol b/test/foundry/helpers/Builders.sol new file mode 100644 index 0000000..89e73e2 --- /dev/null +++ b/test/foundry/helpers/Builders.sol @@ -0,0 +1,86 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.20; + +import "contracts/interfaces/IBullaFactoring.sol"; + +contract ApproveInvoiceBuilder { + uint256 _invoiceId; + uint16 _targetYieldBps; + uint16 _spreadBps; + uint16 _upfrontBps; + uint256 _initialInvoiceValueOverride; + + function withInvoiceId(uint256 invoiceId) external returns (ApproveInvoiceBuilder) { + _invoiceId = invoiceId; + return this; + } + + function withTargetYieldBps(uint16 targetYieldBps) external returns (ApproveInvoiceBuilder) { + _targetYieldBps = targetYieldBps; + return this; + } + + function withSpreadBps(uint16 spreadBps) external returns (ApproveInvoiceBuilder) { + _spreadBps = spreadBps; + return this; + } + + function withUpfrontBps(uint16 upfrontBps) external returns (ApproveInvoiceBuilder) { + _upfrontBps = upfrontBps; + return this; + } + + function withInitialInvoiceValueOverride(uint256 initialInvoiceValueOverride) external returns (ApproveInvoiceBuilder) { + _initialInvoiceValueOverride = initialInvoiceValueOverride; + return this; + } + + function build() external returns (IBullaFactoringV2_2.ApproveInvoiceParams memory) { + IBullaFactoringV2_2.ApproveInvoiceParams memory params = IBullaFactoringV2_2.ApproveInvoiceParams({ + invoiceId: _invoiceId, + targetYieldBps: _targetYieldBps, + spreadBps: _spreadBps, + upfrontBps: _upfrontBps, + initialInvoiceValueOverride: _initialInvoiceValueOverride + }); + _invoiceId = 0; + _targetYieldBps = 0; + _spreadBps = 0; + _upfrontBps = 0; + _initialInvoiceValueOverride = 0; + return params; + } +} + +contract FundInvoiceBuilder { + uint256 _invoiceId; + uint16 _factorerUpfrontBps; + uint8 _receiverAddressIndex; + + function withInvoiceId(uint256 invoiceId) external returns (FundInvoiceBuilder) { + _invoiceId = invoiceId; + return this; + } + + function withFactorerUpfrontBps(uint16 factorerUpfrontBps) external returns (FundInvoiceBuilder) { + _factorerUpfrontBps = factorerUpfrontBps; + return this; + } + + function withReceiverAddressIndex(uint8 receiverAddressIndex) external returns (FundInvoiceBuilder) { + _receiverAddressIndex = receiverAddressIndex; + return this; + } + + function build() external returns (IBullaFactoringV2_2.FundInvoiceParams memory) { + IBullaFactoringV2_2.FundInvoiceParams memory params = IBullaFactoringV2_2.FundInvoiceParams({ + invoiceId: _invoiceId, + factorerUpfrontBps: _factorerUpfrontBps, + receiverAddressIndex: _receiverAddressIndex + }); + _invoiceId = 0; + _factorerUpfrontBps = 0; + _receiverAddressIndex = 0; + return params; + } +} diff --git a/test/foundry/helpers/TestHelpers.sol b/test/foundry/helpers/TestHelpers.sol new file mode 100644 index 0000000..ed9ed38 --- /dev/null +++ b/test/foundry/helpers/TestHelpers.sol @@ -0,0 +1,64 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.20; + +import "contracts/interfaces/IBullaFactoring.sol"; +import "./Builders.sol"; + +// ============ Convenience Helpers ============ + +/// @dev Base contract providing single-invoice convenience wrappers around batch interfaces +abstract contract BatchTestHelpers { + /// @dev Override to return the factoring contract instance + function _factoringContract() internal view virtual returns (IBullaFactoringV2_2); + + function _approveInvoice( + uint256 invoiceId, + uint16 _targetYieldBps, + uint16 _spreadBps, + uint16 _upfrontBps, + uint256 _initialInvoiceValueOverride + ) internal { + IBullaFactoringV2_2.ApproveInvoiceParams[] memory params = new IBullaFactoringV2_2.ApproveInvoiceParams[](1); + params[0] = IBullaFactoringV2_2.ApproveInvoiceParams({ + invoiceId: invoiceId, + targetYieldBps: _targetYieldBps, + spreadBps: _spreadBps, + upfrontBps: _upfrontBps, + initialInvoiceValueOverride: _initialInvoiceValueOverride + }); + _factoringContract().approveInvoices(params); + } + + function _fundInvoice( + uint256 invoiceId, + uint16 factorerUpfrontBps, + address receiverAddress + ) internal returns (uint256) { + IBullaFactoringV2_2.FundInvoiceParams[] memory params = new IBullaFactoringV2_2.FundInvoiceParams[](1); + address[] memory receivers = new address[](1); + receivers[0] = receiverAddress; + params[0] = IBullaFactoringV2_2.FundInvoiceParams({ + invoiceId: invoiceId, + factorerUpfrontBps: factorerUpfrontBps, + receiverAddressIndex: 0 + }); + uint256[] memory amounts = _factoringContract().fundInvoices(params, receivers); + return amounts[0]; + } + + function _fundInvoiceExpectRevert( + uint256 invoiceId, + uint16 factorerUpfrontBps, + address receiverAddress + ) internal { + IBullaFactoringV2_2.FundInvoiceParams[] memory params = new IBullaFactoringV2_2.FundInvoiceParams[](1); + address[] memory receivers = new address[](1); + receivers[0] = receiverAddress; + params[0] = IBullaFactoringV2_2.FundInvoiceParams({ + invoiceId: invoiceId, + factorerUpfrontBps: factorerUpfrontBps, + receiverAddressIndex: 0 + }); + _factoringContract().fundInvoices(params, receivers); + } +}