Tadle

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

The `DeliveryPlace::closeBidTaker()` function incorrectly uses the `makerInfo.tokenAddress` to update the balance of the `TokenBalanceType.PointToken` branch in the user's `userTokenBalanceMap` mapping

Summary

The DeliveryPlace::closeBidTaker() function incorrectly uses the makerInfo.tokenAddress to update the balance of the TokenBalanceType.PointToken branch in the user's userTokenBalanceMap mapping, leading to significant errors in the balance data within the mapping.

Vulnerability Details

The DeliveryPlace::closeBidTaker() function is designed to update the token balance and the StockStatus status after a transaction is completed by the createTaker() creator. Upon reviewing the code, it is evident that the same makerInfo.tokenAddress is used as a parameter twice when updating the userTokenBalanceMap mapping.

function closeBidTaker(address _stock) external {
IPerMarkets perMarkets = tadleFactory.getPerMarkets();
ITokenManager tokenManager = tadleFactory.getTokenManager();
StockInfo memory stockInfo = perMarkets.getStockInfo(_stock);
// SNIP...
(
OfferInfo memory preOfferInfo,
@> MakerInfo memory makerInfo,
,
) = getOfferInfo(stockInfo.preOffer);
// SNIP...
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
);
// SNIP...
}

The makerInfo.tokenAddress corresponds to the token used for payment during the createTaker() call, which is correct for the TokenBalanceType.RemainingCash item. However, this address is incorrectly used for the TokenBalanceType.PointToken item, leading to significant errors. The provided PoC below demonstrates this issue in detail.

Poc

Add the following test code to test/PreMarkets.t.sol and execute it:

function testDeliveryPlace_closeBidTaker_error() public {
// Get token address
console2.log("mockPointToken address:",address(mockPointToken));
console2.log("mockUSDCToken address:",address(mockUSDCToken));
// User calls the PreMarktes::createOffer()
vm.startPrank(user);
preMarktes.createOffer(
CreateOfferParams(
marketPlace,
address(mockUSDCToken),
1000,
0.01 * 1e18,
12000,
300,
OfferType.Ask,
OfferSettleType.Turbo
)
);
vm.stopPrank();
// Cache user's offer address
address userOfferAddr = GenerateAddress.generateOfferAddress(0);
// User2 calls the PreMarktes::createTaker()
vm.startPrank(user2);
mockUSDCToken.approve(address(tokenManager), type(uint256).max);
preMarktes.createTaker(userOfferAddr, 1000);
vm.stopPrank();
// Admin calls SystemConfig::updateMarket() to update the market
vm.prank(user1);
systemConfig.updateMarket(
"Backpack", // _marketPlaceName
address(mockPointToken), // _tokenAddress
0.01 * 1e18, // _tokenPerPoint
block.timestamp - 1, // _tge
3600 // _settlementPeriod
);
// user calls the DeliveryPlace::settleAskMaker()
vm.startPrank(user);
mockPointToken.approve(address(tokenManager), 10000 * 10 ** 18);
deliveryPlace.settleAskMaker(userOfferAddr, 1000);
vm.stopPrank();
// user transfers 1e19 mockPointToken to capitalPool
assertEq(mockPointToken.balanceOf(address(capitalPool)),1e19);
// Cache user2's stock address
address user2StockAddr = GenerateAddress.generateStockAddress(1);
// User2 calls the DeliveryPlace::closeBidTaker()
vm.prank(user2);
deliveryPlace.closeBidTaker(user2StockAddr);
// The balance of the `TokenBalanceType.PointToken` branch under `mockPointToken` of user2 in the `userTokenBalanceMap` mapping is 0, while the balance under `mockUSDCToken` is equal to the `mockPointToken` amount transferred by the user
// Get the balance of user2 in the userTokenBalanceMap mapping
// TokenBalanceType.PointToken
uint256 user2mockPointTokenAmount_PointToken = tokenManager.userTokenBalanceMap(
address(user2),
address(mockPointToken),
TokenBalanceType.PointToken
);
console2.log("user2mockPointTokenAmount-PointToken:",user2mockPointTokenAmount_PointToken);
assertEq(user2mockPointTokenAmount_PointToken,0); // 0
uint256 user2mockUSDCTokenAmount_PointToken = tokenManager.userTokenBalanceMap(
address(user2),
address(mockUSDCToken),
TokenBalanceType.PointToken
);
console2.log("user2mockUSDCTokenAmount-PointToken:",user2mockUSDCTokenAmount_PointToken);
assertEq(user2mockUSDCTokenAmount_PointToken,1e19); // 1e19
// The mockUSDCToken balance of capitalPool address is only 22350000000000000
uint256 capitalPool_mockUSDCTokenAmount = mockUSDCToken.balanceOf(address(capitalPool));
console2.log("capitalPool USDC token balance:", capitalPool_mockUSDCTokenAmount);
// Due to a problem with the authorization in the `TokenManager::_transfer()` method, it is manually executed here
capitalPool.approve(address(mockUSDCToken));
// User2 will fail to withdraw the corresponding balance
vm.startPrank(user2);
vm.expectRevert();
tokenManager.withdraw(address(mockUSDCToken),TokenBalanceType.PointToken);
// Because the capitalPool balance is insufficient
// ERC20InsufficientBalance(0x76006C4471fb6aDd17728e9c9c8B67d5AF06cDA0, 22350000000000000 [2.235e16], 10000000000000000000 [1e19])
vm.stopPrank();
}
// [PASS] testDeliveryPlace_closeBidTaker_error() (gas: 1211713)
// Logs:
// mockPointToken address: 0x5991A2dF15A8F6A256D3Ec51E99254Cd3fb576A9
// mockUSDCToken address: 0xF62849F9A0B5Bf2913b396098F7c7019b51A820a
// user2mockPointTokenAmount-PointToken: 0
// user2mockUSDCTokenAmount-PointToken: 10000000000000000000
// capitalPool USDC token balance: 22350000000000000

Code Snippet

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

Impact

The incorrect usage of makerInfo.tokenAddress in the DeliveryPlace::closeBidTaker() function results in serious errors in the user's balance data in the userTokenBalanceMap mapping.

Tools Used

Manual Review

Recommendations

  1. Call getOfferInfo() to retrieve MarketPlaceInfo memory marketPlaceInfo.

  2. Use the marketPlaceInfo.tokenAddress parameter when updating the TokenBalanceType.PointToken branch balance:

function closeBidTaker(address _stock) external {
IPerMarkets perMarkets = tadleFactory.getPerMarkets();
ITokenManager tokenManager = tadleFactory.getTokenManager();
StockInfo memory stockInfo = perMarkets.getStockInfo(_stock);
// SNIP...
(
OfferInfo memory preOfferInfo,
MakerInfo memory makerInfo,
+ MarketPlaceInfo memory marketPlaceInfo
,
) = getOfferInfo(stockInfo.preOffer);
// SNIP...
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,
+ marketPlaceInfo.tokenAddress,
pointTokenAmount
);
// SNIP...
}
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.