Skip to content

Commit 532426d

Browse files
authored
fix: 3. [LOW] BullaFrendLendV2::_offerLoan(): Skips offerId == 0 thr… (#85)
fix: 3. [LOW] BullaFrendLendV2::_offerLoan(): Skips offerId == 0 through prefix operation.
1 parent ada5386 commit 532426d

5 files changed

Lines changed: 151 additions & 12 deletions

File tree

src/BullaFrendLendV2.sol

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,7 @@ contract BullaFrendLendV2 is BullaClaimControllerBase, BoringBatchable, ERC165,
146146
* @return The loan offer details
147147
*/
148148
function getLoanOffer(uint256 offerId) public view returns (LoanOffer memory) {
149+
if (offerId >= loanOfferCount) revert LoanOfferNotFound();
149150
return _loanOffers[offerId];
150151
}
151152

@@ -155,6 +156,7 @@ contract BullaFrendLendV2 is BullaClaimControllerBase, BoringBatchable, ERC165,
155156
* @return The metadata for the loan offer
156157
*/
157158
function getLoanOfferMetadata(uint256 offerId) public view returns (ClaimMetadata memory) {
159+
if (offerId >= loanOfferCount) revert LoanOfferNotFound();
158160
return _loanOfferMetadata[offerId];
159161
}
160162

@@ -191,7 +193,7 @@ contract BullaFrendLendV2 is BullaClaimControllerBase, BoringBatchable, ERC165,
191193

192194
_validateLoanOffer(offer, requestedByCreditor);
193195

194-
uint256 offerId = ++loanOfferCount;
196+
uint256 offerId = loanOfferCount++;
195197
_loanOffers[offerId] = LoanOffer({params: offer, requestedByCreditor: requestedByCreditor});
196198

197199
if (bytes(metadata.tokenURI).length > 0 || bytes(metadata.attachmentURI).length > 0) {

test/foundry/BullaFrendLend/BatchFunctionality.t.sol

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@ contract TestBullaFrendLendBatchFunctionality is BullaFrendLendTestHelper {
109109
bullaFrendLend.batch(calls, true);
110110

111111
// Verify loan offer was created
112-
LoanOffer memory loanOffer = bullaFrendLend.getLoanOffer(1);
112+
LoanOffer memory loanOffer = bullaFrendLend.getLoanOffer(0);
113113
assertEq(loanOffer.params.creditor, creditor);
114114
assertEq(loanOffer.params.debtor, debtor);
115115
assertTrue(loanOffer.requestedByCreditor);
@@ -779,9 +779,9 @@ contract TestBullaFrendLendBatchFunctionality is BullaFrendLendTestHelper {
779779
// Verify all loan offers were created
780780
assertEq(bullaFrendLend.loanOfferCount(), 3);
781781

782-
LoanOffer memory offer1 = bullaFrendLend.getLoanOffer(1);
783-
LoanOffer memory offer2 = bullaFrendLend.getLoanOffer(2);
784-
LoanOffer memory offer3 = bullaFrendLend.getLoanOffer(3);
782+
LoanOffer memory offer1 = bullaFrendLend.getLoanOffer(0);
783+
LoanOffer memory offer2 = bullaFrendLend.getLoanOffer(1);
784+
LoanOffer memory offer3 = bullaFrendLend.getLoanOffer(2);
785785

786786
assertEq(offer1.params.loanAmount, 1 ether);
787787
assertEq(offer1.params.debtor, debtor);
@@ -791,7 +791,7 @@ contract TestBullaFrendLendBatchFunctionality is BullaFrendLendTestHelper {
791791
assertEq(offer3.params.token, address(permitToken));
792792

793793
// Check metadata for the third offer
794-
ClaimMetadata memory metadata = bullaFrendLend.getLoanOfferMetadata(3);
794+
ClaimMetadata memory metadata = bullaFrendLend.getLoanOfferMetadata(2);
795795
assertEq(metadata.tokenURI, "test-uri");
796796
assertEq(metadata.attachmentURI, "test-attachment");
797797
}

test/foundry/BullaFrendLend/BullaFrendLend.t.sol

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -304,12 +304,12 @@ contract TestBullaFrendLend is Test {
304304

305305
// Expect the LoanOffered event to be emitted with the correct parameters
306306
vm.expectEmit(true, true, false, true);
307-
emit LoanOffered(1, creditor, offer, ClaimMetadata({tokenURI: "", attachmentURI: ""}));
307+
emit LoanOffered(0, creditor, offer, ClaimMetadata({tokenURI: "", attachmentURI: ""}));
308308

309309
vm.prank(creditor);
310310
uint256 loanId = bullaFrendLend.offerLoan(offer);
311311

312-
assertEq(loanId, 1, "Loan ID should be 1");
312+
assertEq(loanId, 0, "Loan ID should be 0");
313313
}
314314

315315
function testLoanOfferedEventEmittedByDebtorWithOriginationFee() public {
@@ -318,12 +318,12 @@ contract TestBullaFrendLend is Test {
318318

319319
// Expect the LoanOffered event to be emitted with the correct parameters (by debtor)
320320
vm.expectEmit(true, true, false, true);
321-
emit LoanOffered(1, debtor, request, ClaimMetadata({tokenURI: "", attachmentURI: ""}));
321+
emit LoanOffered(0, debtor, request, ClaimMetadata({tokenURI: "", attachmentURI: ""}));
322322

323323
vm.prank(debtor);
324324
uint256 requestId = bullaFrendLend.offerLoan(request);
325325

326-
assertEq(requestId, 1, "Request ID should be 1");
326+
assertEq(requestId, 0, "Request ID should be 0");
327327
}
328328

329329
function testCannotAcceptCreditorOfferIfNotDebtor() public {

test/foundry/BullaFrendLend/LoanOfferExpiry.t.sol

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -356,8 +356,8 @@ contract TestLoanOfferExpiry is BullaFrendLendTestHelper {
356356
bullaFrendLend.batch(calls, true);
357357

358358
// Verify both offers were created with correct expiry times
359-
LoanOffer memory offer1 = bullaFrendLend.getLoanOffer(1);
360-
LoanOffer memory offer2 = bullaFrendLend.getLoanOffer(2);
359+
LoanOffer memory offer1 = bullaFrendLend.getLoanOffer(0);
360+
LoanOffer memory offer2 = bullaFrendLend.getLoanOffer(1);
361361

362362
assertEq(offer1.params.expiresAt, futureExpiry, "First offer should have correct expiry");
363363
assertEq(offer2.params.expiresAt, futureExpiry + 1 days, "Second offer should have correct expiry");
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
// SPDX-License-Identifier: GPL-2.0-or-later
2+
pragma solidity ^0.8.30;
3+
4+
import "forge-std/Test.sol";
5+
import {WETH} from "contracts/mocks/weth.sol";
6+
import {BullaFrendLendV2, LoanOfferNotFound} from "contracts/BullaFrendLendV2.sol";
7+
import {BullaClaimV2} from "contracts/BullaClaimV2.sol";
8+
import {DeployContracts} from "script/DeployContracts.s.sol";
9+
import {LoanRequestParams, LoanOffer} from "contracts/interfaces/IBullaFrendLendV2.sol";
10+
import {InterestConfig} from "contracts/libraries/CompoundInterestLib.sol";
11+
import {LockState} from "contracts/types/Types.sol";
12+
13+
/// @title Test to validate LoanOfferId behavior - whether first offerId starts from 0
14+
/// @notice This test demonstrates the FIXED behavior where first offerId = 0
15+
contract TestLoanOfferIdStartsFromZero is Test {
16+
BullaFrendLendV2 internal bullaFrendLend;
17+
WETH internal weth;
18+
19+
address creditor = makeAddr("creditor");
20+
address debtor = makeAddr("debtor");
21+
22+
function setUp() public {
23+
DeployContracts deployer = new DeployContracts();
24+
DeployContracts.DeploymentResult memory result = deployer.deployForTest(
25+
address(this), // deployer
26+
LockState.Unlocked, // initialLockState
27+
10000000000000000, // coreProtocolFee (0.01 ETH)
28+
500, // invoiceProtocolFeeBPS (5%)
29+
500, // frendLendProtocolFeeBPS (5%)
30+
address(this) // admin
31+
);
32+
33+
BullaClaimV2 bullaClaim = BullaClaimV2(result.bullaClaim);
34+
bullaFrendLend = new BullaFrendLendV2(address(bullaClaim), address(this), 500);
35+
36+
weth = new WETH();
37+
38+
// Fund creditor with WETH
39+
vm.deal(creditor, 10 ether);
40+
vm.prank(creditor);
41+
weth.deposit{value: 5 ether}();
42+
}
43+
44+
/// @notice Test that validates first loanOfferId = 0 and sequential assignment
45+
function testFirstLoanOfferIdIsZero() public {
46+
// Verify initial state
47+
assertEq(bullaFrendLend.loanOfferCount(), 0, "Initial loanOfferCount should be 0");
48+
49+
LoanRequestParams memory loanParams = LoanRequestParams({
50+
termLength: 30 days,
51+
interestConfig: InterestConfig({
52+
interestRateBps: 500, // 5%
53+
numberOfPeriodsPerYear: 12
54+
}),
55+
loanAmount: 1 ether,
56+
creditor: creditor,
57+
debtor: debtor,
58+
description: "First loan offer",
59+
token: address(weth),
60+
impairmentGracePeriod: 7 days,
61+
expiresAt: 0,
62+
callbackContract: address(0),
63+
callbackSelector: bytes4(0)
64+
});
65+
66+
// Approve the contract to spend creditor's WETH (needed for when loan is accepted)
67+
vm.prank(creditor);
68+
weth.approve(address(bullaFrendLend), 2 ether);
69+
70+
// Create first loan offer - should get offerId = 0 (FIXED behavior with post-increment)
71+
vm.prank(creditor);
72+
uint256 firstOfferId = bullaFrendLend.offerLoan(loanParams);
73+
assertEq(firstOfferId, 0, "First loan offer should have offerId = 0 (FIXED behavior)");
74+
assertEq(bullaFrendLend.loanOfferCount(), 1, "loanOfferCount should be 1 after first offer");
75+
76+
// Verify offer exists and can be retrieved
77+
LoanOffer memory retrievedOffer = bullaFrendLend.getLoanOffer(0);
78+
assertEq(retrievedOffer.params.loanAmount, 1 ether, "First offer should be retrievable");
79+
assertEq(retrievedOffer.params.creditor, creditor, "Creditor should match");
80+
assertEq(retrievedOffer.requestedByCreditor, true, "Should be requested by creditor");
81+
82+
// Create second loan offer - should get offerId = 1
83+
loanParams.loanAmount = 2 ether;
84+
loanParams.description = "Second loan offer";
85+
86+
vm.prank(creditor);
87+
uint256 secondOfferId = bullaFrendLend.offerLoan(loanParams);
88+
assertEq(secondOfferId, 1, "Second loan offer should have offerId = 1");
89+
assertEq(bullaFrendLend.loanOfferCount(), 2, "loanOfferCount should be 2 after second offer");
90+
91+
// Verify second offer exists and can be retrieved
92+
LoanOffer memory secondOffer = bullaFrendLend.getLoanOffer(1);
93+
assertEq(secondOffer.params.loanAmount, 2 ether, "Second offer should be retrievable");
94+
assertEq(secondOffer.params.description, "Second loan offer", "Description should match");
95+
}
96+
97+
/// @notice Test boundary checking for getLoanOffer function
98+
function testGetLoanOfferBoundaryChecking() public {
99+
// Initially no offers exist, loanOfferCount = 0
100+
assertEq(bullaFrendLend.loanOfferCount(), 0);
101+
102+
vm.expectRevert(LoanOfferNotFound.selector);
103+
bullaFrendLend.getLoanOffer(0);
104+
105+
vm.expectRevert(LoanOfferNotFound.selector);
106+
bullaFrendLend.getLoanOfferMetadata(0);
107+
108+
// Create one offer
109+
LoanRequestParams memory loanParams = LoanRequestParams({
110+
termLength: 30 days,
111+
interestConfig: InterestConfig({
112+
interestRateBps: 500, // 5%
113+
numberOfPeriodsPerYear: 12
114+
}),
115+
loanAmount: 1 ether,
116+
creditor: creditor,
117+
debtor: debtor,
118+
description: "Test offer",
119+
token: address(weth),
120+
impairmentGracePeriod: 7 days,
121+
expiresAt: 0,
122+
callbackContract: address(0),
123+
callbackSelector: bytes4(0)
124+
});
125+
126+
vm.prank(creditor);
127+
weth.approve(address(bullaFrendLend), 1 ether);
128+
129+
vm.prank(creditor);
130+
uint256 offerId = bullaFrendLend.offerLoan(loanParams);
131+
132+
// Now offerId = 0 should work (actual offer exists)
133+
LoanOffer memory validOffer = bullaFrendLend.getLoanOffer(0);
134+
assertEq(validOffer.params.loanAmount, 1 ether, "Valid offer should be retrievable");
135+
assertEq(validOffer.params.creditor, creditor, "Creditor should match");
136+
}
137+
}

0 commit comments

Comments
 (0)