Skip to content

Commit 099bcfc

Browse files
authored
feat: track whitelist tokens, do not track all tokens, only iterate t… (#98)
feat: track whitelist tokens, do not track all tokens, only iterate through whitelisted tokens
1 parent 273f068 commit 099bcfc

6 files changed

Lines changed: 46 additions & 147 deletions

File tree

src/BullaFrendLendV2.sol

Lines changed: 23 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -43,9 +43,8 @@ contract BullaFrendLendV2 is BullaClaimControllerBase, ERC165, Ownable, IBullaFr
4343
uint256 public loanOfferCount;
4444
uint16 public protocolFeeBPS;
4545

46-
address[] public protocolFeeTokens;
46+
address[] public whitelistedProtocolFeeTokens;
4747
mapping(address => uint256) public protocolFeesByToken;
48-
mapping(address => bool) private _tokenExists;
4948

5049
// Whitelist for protocol fee token withdrawals
5150
mapping(address => bool) public protocolFeeTokenWhitelist;
@@ -373,12 +372,8 @@ contract BullaFrendLendV2 is BullaClaimControllerBase, ERC165, Ownable, IBullaFr
373372

374373
// Transfer the total amount from sender to this contract, to avoid double approval
375374
if (paymentAmount > 0) {
376-
// Track protocol fee for this token if any interest was paid
375+
// Track protocol fee for this token if any interest was paid, even if token is not whitelisted
377376
if (protocolFee > 0) {
378-
if (!_tokenExists[loan.token]) {
379-
protocolFeeTokens.push(loan.token);
380-
_tokenExists[loan.token] = true;
381-
}
382377
protocolFeesByToken[loan.token] += protocolFee;
383378
}
384379

@@ -423,12 +418,12 @@ contract BullaFrendLendV2 is BullaClaimControllerBase, ERC165, Ownable, IBullaFr
423418
* @notice Allows owner to withdraw accumulated protocol fees
424419
*/
425420
function withdrawAllFees() external onlyOwner {
426-
// Withdraw protocol fees in all tracked tokens that are whitelisted
427-
for (uint256 i = 0; i < protocolFeeTokens.length; i++) {
428-
address token = protocolFeeTokens[i];
421+
// Withdraw protocol fees in all whitelisted tokens
422+
for (uint256 i = 0; i < whitelistedProtocolFeeTokens.length; i++) {
423+
address token = whitelistedProtocolFeeTokens[i];
429424
uint256 feeAmount = protocolFeesByToken[token];
430425

431-
if (feeAmount > 0 && protocolFeeTokenWhitelist[token]) {
426+
if (feeAmount > 0) {
432427
protocolFeesByToken[token] = 0; // Reset fee amount before transfer
433428
ERC20(token).safeTransfer(owner(), feeAmount);
434429
emit FeeWithdrawn(owner(), token, feeAmount);
@@ -454,7 +449,10 @@ contract BullaFrendLendV2 is BullaClaimControllerBase, ERC165, Ownable, IBullaFr
454449
* @param token The token address to whitelist for withdrawals
455450
*/
456451
function addToFeeTokenWhitelist(address token) external onlyOwner {
457-
protocolFeeTokenWhitelist[token] = true;
452+
if (!protocolFeeTokenWhitelist[token]) {
453+
protocolFeeTokenWhitelist[token] = true;
454+
whitelistedProtocolFeeTokens.push(token);
455+
}
458456

459457
emit TokenAddedToFeesWhitelist(token);
460458
}
@@ -464,7 +462,19 @@ contract BullaFrendLendV2 is BullaClaimControllerBase, ERC165, Ownable, IBullaFr
464462
* @param token The token address to remove from withdrawal whitelist
465463
*/
466464
function removeFromFeeTokenWhitelist(address token) external onlyOwner {
467-
protocolFeeTokenWhitelist[token] = false;
465+
if (protocolFeeTokenWhitelist[token]) {
466+
protocolFeeTokenWhitelist[token] = false;
467+
468+
// Remove from whitelistedProtocolFeeTokens array
469+
for (uint256 i = 0; i < whitelistedProtocolFeeTokens.length; i++) {
470+
if (whitelistedProtocolFeeTokens[i] == token) {
471+
whitelistedProtocolFeeTokens[i] =
472+
whitelistedProtocolFeeTokens[whitelistedProtocolFeeTokens.length - 1];
473+
whitelistedProtocolFeeTokens.pop();
474+
break;
475+
}
476+
}
477+
}
468478

469479
emit TokenRemovedFromFeesWhitelist(token);
470480
}

src/BullaInvoice.sol

Lines changed: 22 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -41,9 +41,8 @@ contract BullaInvoice is BullaClaimControllerBase, ERC165, Ownable, IBullaInvoic
4141

4242
ClaimMetadata public EMPTY_METADATA = ClaimMetadata({attachmentURI: "", tokenURI: ""});
4343

44-
address[] public protocolFeeTokens;
44+
address[] public whitelistedProtocolFeeTokens;
4545
mapping(address => uint256) public protocolFeesByToken;
46-
mapping(address => bool) private _tokenExists;
4746

4847
// Whitelist for protocol fee token withdrawals
4948
mapping(address => bool) public protocolFeeTokenWhitelist;
@@ -329,10 +328,6 @@ contract BullaInvoice is BullaClaimControllerBase, ERC165, Ownable, IBullaInvoic
329328
// Track protocol fee for this token if any interest was paid
330329
// No need to track gas fee as it is the balance of the contract
331330
if (protocolFee > 0) {
332-
if (!_tokenExists[claim.token]) {
333-
protocolFeeTokens.push(claim.token);
334-
_tokenExists[claim.token] = true;
335-
}
336331
protocolFeesByToken[claim.token] += protocolFee;
337332
}
338333
// Handle ERC20 payments
@@ -496,12 +491,12 @@ contract BullaInvoice is BullaClaimControllerBase, ERC165, Ownable, IBullaInvoic
496491
emit FeeWithdrawn(owner(), address(0), ethBalance);
497492
}
498493

499-
// Withdraw protocol fees in all tracked tokens that are whitelisted
500-
for (uint256 i = 0; i < protocolFeeTokens.length; i++) {
501-
address token = protocolFeeTokens[i];
494+
// Withdraw protocol fees in all whitelisted tokens
495+
for (uint256 i = 0; i < whitelistedProtocolFeeTokens.length; i++) {
496+
address token = whitelistedProtocolFeeTokens[i];
502497
uint256 feeAmount = protocolFeesByToken[token];
503498

504-
if (feeAmount > 0 && protocolFeeTokenWhitelist[token]) {
499+
if (feeAmount > 0) {
505500
protocolFeesByToken[token] = 0; // Reset fee amount before transfer
506501
ERC20(token).safeTransfer(owner(), feeAmount);
507502
emit FeeWithdrawn(owner(), token, feeAmount);
@@ -527,7 +522,10 @@ contract BullaInvoice is BullaClaimControllerBase, ERC165, Ownable, IBullaInvoic
527522
* @param token The token address to whitelist for withdrawals
528523
*/
529524
function addToFeeTokenWhitelist(address token) external onlyOwner {
530-
protocolFeeTokenWhitelist[token] = true;
525+
if (!protocolFeeTokenWhitelist[token]) {
526+
protocolFeeTokenWhitelist[token] = true;
527+
whitelistedProtocolFeeTokens.push(token);
528+
}
531529

532530
emit TokenAddedToFeesWhitelist(token);
533531
}
@@ -537,7 +535,19 @@ contract BullaInvoice is BullaClaimControllerBase, ERC165, Ownable, IBullaInvoic
537535
* @param token The token address to remove from withdrawal whitelist
538536
*/
539537
function removeFromFeeTokenWhitelist(address token) external onlyOwner {
540-
protocolFeeTokenWhitelist[token] = false;
538+
if (protocolFeeTokenWhitelist[token]) {
539+
protocolFeeTokenWhitelist[token] = false;
540+
541+
// Remove from whitelistedProtocolFeeTokens array
542+
for (uint256 i = 0; i < whitelistedProtocolFeeTokens.length; i++) {
543+
if (whitelistedProtocolFeeTokens[i] == token) {
544+
whitelistedProtocolFeeTokens[i] =
545+
whitelistedProtocolFeeTokens[whitelistedProtocolFeeTokens.length - 1];
546+
whitelistedProtocolFeeTokens.pop();
547+
break;
548+
}
549+
}
550+
}
541551

542552
emit TokenRemovedFromFeesWhitelist(token);
543553
}

test/foundry/BullaFrendLend/BullaFrendLend.t.sol

Lines changed: 0 additions & 101 deletions
Original file line numberDiff line numberDiff line change
@@ -952,21 +952,6 @@ contract TestBullaFrendLend is Test {
952952
assertEq(bullaFrendLend.protocolFeeBPS(), newProtocolFeeBPS, "Protocol fee not updated correctly");
953953
}
954954

955-
// helper function to check if token is in protocol fee tokens
956-
function isTokenInProtocolFeeTokens(address token) internal view returns (bool) {
957-
// we've defined at most 3 tokens in our test
958-
for (uint256 i = 0; i < 3; i++) {
959-
try bullaFrendLend.protocolFeeTokens(i) returns (address tokenAddr) {
960-
if (tokenAddr == token) {
961-
return true;
962-
}
963-
} catch {
964-
break;
965-
}
966-
}
967-
return false;
968-
}
969-
970955
function testProtocolFeeWithMultipleTokens() public {
971956
vm.startPrank(creditor);
972957
weth.approve(address(bullaFrendLend), 10 ether);
@@ -1052,10 +1037,6 @@ contract TestBullaFrendLend is Test {
10521037
assertEq(bullaFrendLend.protocolFeesByToken(address(weth)), wethProtocolFee, "WETH fee tracking incorrect");
10531038
assertEq(bullaFrendLend.protocolFeesByToken(address(usdc)), usdcProtocolFee, "USDC fee tracking incorrect");
10541039
assertEq(bullaFrendLend.protocolFeesByToken(address(dai)), daiProtocolFee, "DAI fee tracking incorrect");
1055-
1056-
assertTrue(isTokenInProtocolFeeTokens(address(weth)), "WETH not found in protocol fee tokens array");
1057-
assertTrue(isTokenInProtocolFeeTokens(address(usdc)), "USDC not found in protocol fee tokens array");
1058-
assertTrue(isTokenInProtocolFeeTokens(address(dai)), "DAI not found in protocol fee tokens array");
10591040
}
10601041

10611042
function testWithdrawAllFees() public {
@@ -1357,88 +1338,6 @@ contract TestBullaFrendLend is Test {
13571338
}
13581339
}
13591340

1360-
function testTokenTrackingUniqueness() public {
1361-
vm.startPrank(creditor);
1362-
weth.approve(address(bullaFrendLend), 10 ether);
1363-
vm.stopPrank();
1364-
1365-
vm.startPrank(debtor);
1366-
weth.approve(address(bullaFrendLend), 10 ether);
1367-
vm.stopPrank();
1368-
1369-
bullaClaim.approvalRegistry().permitCreateClaim({
1370-
user: debtor,
1371-
controller: address(bullaFrendLend),
1372-
approvalType: CreateClaimApprovalType.Approved,
1373-
approvalCount: 2,
1374-
isBindingAllowed: true,
1375-
signature: sigHelper.signCreateClaimPermit({
1376-
pk: debtorPK,
1377-
user: debtor,
1378-
controller: address(bullaFrendLend),
1379-
approvalType: CreateClaimApprovalType.Approved,
1380-
approvalCount: 2,
1381-
isBindingAllowed: true
1382-
})
1383-
});
1384-
1385-
// Create first WETH loan
1386-
LoanRequestParams memory wethOffer1 = new LoanRequestParamsBuilder().withCreditor(creditor).withDebtor(debtor)
1387-
.withDescription("WETH Loan 1").withToken(address(weth)).withLoanAmount(1 ether).withNumberOfPeriodsPerYear(365)
1388-
.build();
1389-
1390-
vm.prank(creditor);
1391-
uint256 wethLoanId1 = bullaFrendLend.offerLoan(wethOffer1);
1392-
1393-
vm.prank(debtor);
1394-
uint256 wethClaimId1 = bullaFrendLend.acceptLoan{value: FEE}(wethLoanId1);
1395-
1396-
// Create second WETH loan
1397-
LoanRequestParams memory wethOffer2 = new LoanRequestParamsBuilder().withCreditor(creditor).withDebtor(debtor)
1398-
.withDescription("WETH Loan 2").withToken(address(weth)).withLoanAmount(0.5 ether).withNumberOfPeriodsPerYear(
1399-
365
1400-
).build();
1401-
1402-
vm.prank(creditor);
1403-
uint256 wethLoanId2 = bullaFrendLend.offerLoan(wethOffer2);
1404-
1405-
vm.prank(debtor);
1406-
uint256 wethClaimId2 = bullaFrendLend.acceptLoan{value: FEE}(wethLoanId2);
1407-
1408-
vm.warp(block.timestamp + 15 days);
1409-
1410-
// Make payments on both loans
1411-
vm.startPrank(debtor);
1412-
1413-
uint256 initialContractBalance = weth.balanceOf(address(bullaFrendLend));
1414-
(uint256 principal1, uint256 interest1) = bullaFrendLend.getTotalAmountDue(wethClaimId1);
1415-
bullaFrendLend.payLoan(wethClaimId1, principal1 + interest1);
1416-
uint256 fee1 = weth.balanceOf(address(bullaFrendLend)) - initialContractBalance;
1417-
1418-
uint256 contractBalanceAfterFirst = weth.balanceOf(address(bullaFrendLend));
1419-
(uint256 principal2, uint256 interest2) = bullaFrendLend.getTotalAmountDue(wethClaimId2);
1420-
bullaFrendLend.payLoan(wethClaimId2, principal2 + interest2);
1421-
uint256 fee2 = weth.balanceOf(address(bullaFrendLend)) - contractBalanceAfterFirst;
1422-
vm.stopPrank();
1423-
1424-
// Count WETH tokens in the array
1425-
uint256 wethTokenCount = 0;
1426-
for (uint256 i = 0; i < 3; i++) {
1427-
try bullaFrendLend.protocolFeeTokens(i) returns (address token) {
1428-
if (token == address(weth)) {
1429-
wethTokenCount++;
1430-
}
1431-
} catch {
1432-
break;
1433-
}
1434-
}
1435-
1436-
assertEq(wethTokenCount, 1, "WETH should only appear once in protocol fee tokens array");
1437-
assertEq(
1438-
bullaFrendLend.protocolFeesByToken(address(weth)), fee1 + fee2, "WETH fees should accumulate correctly"
1439-
);
1440-
}
1441-
14421341
function testProtocolFeeVariations() public {
14431342
vm.startPrank(creditor);
14441343
weth.approve(address(bullaFrendLend), 10 ether);

test/foundry/BullaFrendLend/BullaFrendLendTokenWhitelist.t.sol

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -139,9 +139,6 @@ contract TestBullaFrendLendTokenWhitelist is Test {
139139
vm.prank(debtor);
140140
bullaFrendLend.payLoan(claimId, PAYMENT_AMOUNT);
141141

142-
// Verify token was added to protocol fee tracking
143-
assertEq(bullaFrendLend.protocolFeeTokens(0), address(token1));
144-
145142
// Verify protocol fees accumulated for this token
146143
assertGt(bullaFrendLend.protocolFeesByToken(address(token1)), 0);
147144
}

test/foundry/BullaInvoice/BullaInvoiceProtocolFee.t.sol

Lines changed: 1 addition & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -277,19 +277,14 @@ contract TestBullaInvoiceProtocolFee is Test {
277277

278278
Invoice memory invoice = bullaInvoice.getInvoice(invoiceId);
279279
uint256 accruedInterest = invoice.interestComputationState.accruedInterest;
280-
281-
vm.expectRevert();
282-
bullaInvoice.protocolFeeTokens(0);
283-
284280
// Make payment with interest
285281
vm.prank(debtor);
286282
token1.approve(address(bullaInvoice), accruedInterest);
287283

288284
vm.prank(debtor);
289285
bullaInvoice.payInvoice(invoiceId, accruedInterest);
290286

291-
// Verify token added to array
292-
assertEq(bullaInvoice.protocolFeeTokens(0), address(token1), "Token should be added to protocolFeeTokens array");
287+
// Verify protocol fee was tracked for whitelisted token
293288
assertGt(bullaInvoice.protocolFeesByToken(address(token1)), 0, "Protocol fee should be tracked");
294289
}
295290

@@ -324,11 +319,6 @@ contract TestBullaInvoiceProtocolFee is Test {
324319
"Total protocol fee should be double since it is the same amount twice"
325320
);
326321
assertGt(bullaInvoice.protocolFeesByToken(address(token1)), 0, "Protocol fee is not 0");
327-
328-
// Verify only one entry in array
329-
assertEq(bullaInvoice.protocolFeeTokens(0), address(token1), "Token should still be at index 0");
330-
vm.expectRevert();
331-
bullaInvoice.protocolFeeTokens(1); // Should revert - no second token
332322
}
333323

334324
// ==================== 5. ETH PAYMENT TESTS ====================
@@ -724,10 +714,6 @@ contract TestBullaInvoiceProtocolFee is Test {
724714
assertEq(address(bullaInvoice).balance, expectedEthFee, "ETH fees in contract balance");
725715
assertEq(bullaInvoice.protocolFeesByToken(address(token1)), expectedToken1Fee, "Token1 fees tracked");
726716
assertEq(bullaInvoice.protocolFeesByToken(address(token2)), expectedToken2Fee, "Token2 fees tracked");
727-
728-
// Verify token array contains both tokens
729-
assertEq(bullaInvoice.protocolFeeTokens(0), address(token1), "First token in array");
730-
assertEq(bullaInvoice.protocolFeeTokens(1), address(token2), "Second token in array");
731717
}
732718

733719
// ==================== 9. EDGE CASES & ERROR HANDLING ====================

test/foundry/BullaInvoice/BullaInvoiceTokenWhitelist.t.sol

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -128,9 +128,6 @@ contract TestBullaInvoiceTokenWhitelist is Test {
128128
vm.prank(debtor);
129129
bullaInvoice.payInvoice(invoiceId, PAYMENT_AMOUNT);
130130

131-
// Verify token was added to protocol fee tracking
132-
assertEq(bullaInvoice.protocolFeeTokens(0), address(token1));
133-
134131
// Verify protocol fees accumulated for this token
135132
assertGt(bullaInvoice.protocolFeesByToken(address(token1)), 0);
136133
}

0 commit comments

Comments
 (0)