Tadle

Tadle
DeFi
30,000 USDC
View results
Submission Details
Severity: low
Valid

`createOffer` function creates `offerAddr` and `stockAddr` using current `offerId` but sets `offerInfoMap[offerAddr]`'s and `stockInfoMap[stockAddr]`'s `id` to incremented `offerId`

Summary

Because the createOffer function creates offerAddr and stockAddr using the current offerId but sets offerInfoMap[offerAddr]'s and stockInfoMap[stockAddr]'s id to the incremented offerId, the offerId that such offer's and stock's addresses correspond to and the offerId that such offer's and stock's id are set to are not consistent. Moreover, the id of the stocks created by the createOffer and createTaker function calls can be the same, and the id of the offers created by the createOffer and listOffer function calls can be the same. As a result, users can match incorrect offers or stocks when only the id of the offers or stocks that they want to match are known.

Vulnerability Details

At the beginning, offerId equals 0.

In a sequential order, the following createOffer function creates offerAddr and stockAddr using the current offerId, increments offerId by 1, and sets offerInfoMap[offerAddr]'s and stockInfoMap[stockAddr]'s id to the incremented offerId. For example, the created offer's offerAddr and stock's stockAddr can correspond to the offerId that equals 0 but their id are the offerId that equals 1.

https://github.com/Cyfrin/2024-08-tadle/blob/c249cdb68c37c47025cdc4c4782c8ee3f20a5b98/src/core/PreMarkets.sol#L39-L157

function createOffer(CreateOfferParams calldata params) external payable {
...
if (params.points == 0x0 || params.amount == 0x0) {
revert Errors.AmountIsZero();
}
if (params.eachTradeTax > Constants.EACH_TRADE_TAX_DECIMAL_SCALER) {
revert InvalidEachTradeTaxRate();
}
if (params.collateralRate < Constants.COLLATERAL_RATE_DECIMAL_SCALER) {
revert InvalidCollateralRate();
}
/// @dev market place must be online
ISystemConfig systemConfig = tadleFactory.getSystemConfig();
MarketPlaceInfo memory marketPlaceInfo = systemConfig
.getMarketPlaceInfo(params.marketPlace);
marketPlaceInfo.checkMarketPlaceStatus(
block.timestamp,
MarketPlaceStatus.Online
);
/// @dev generate address for maker, offer, stock.
address makerAddr = GenerateAddress.generateMakerAddress(offerId);
@> address offerAddr = GenerateAddress.generateOfferAddress(offerId);
@> address stockAddr = GenerateAddress.generateStockAddress(offerId);
if (makerInfoMap[makerAddr].authority != address(0x0)) {
revert MakerAlreadyExist();
}
if (offerInfoMap[offerAddr].authority != address(0x0)) {
revert OfferAlreadyExist();
}
if (stockInfoMap[stockAddr].authority != address(0x0)) {
revert StockAlreadyExist();
}
@> offerId = offerId + 1;
{
/// @dev transfer collateral from _msgSender() to capital pool
uint256 transferAmount = OfferLibraries.getDepositAmount(
params.offerType,
params.collateralRate,
params.amount,
true,
Math.Rounding.Ceil
);
ITokenManager tokenManager = tadleFactory.getTokenManager();
tokenManager.tillIn{value: msg.value}(
_msgSender(),
params.tokenAddress,
transferAmount,
false
);
}
/// @dev update maker info
makerInfoMap[makerAddr] = MakerInfo({
offerSettleType: params.offerSettleType,
authority: _msgSender(),
marketPlace: params.marketPlace,
tokenAddress: params.tokenAddress,
originOffer: offerAddr,
platformFee: 0,
eachTradeTax: params.eachTradeTax
});
/// @dev update offer info
@> offerInfoMap[offerAddr] = OfferInfo({
@> id: offerId,
authority: _msgSender(),
maker: makerAddr,
offerStatus: OfferStatus.Virgin,
offerType: params.offerType,
points: params.points,
amount: params.amount,
collateralRate: params.collateralRate,
abortOfferStatus: AbortOfferStatus.Initialized,
usedPoints: 0,
tradeTax: 0,
settledPoints: 0,
settledPointTokenAmount: 0,
settledCollateralAmount: 0
});
/// @dev update stock info
@> stockInfoMap[stockAddr] = StockInfo({
@> id: offerId,
stockStatus: StockStatus.Initialized,
stockType: params.offerType == OfferType.Ask
? StockType.Bid
: StockType.Ask,
authority: _msgSender(),
maker: makerAddr,
preOffer: address(0x0),
offer: offerAddr,
points: params.points,
amount: params.amount
});
...
}

The following createTaker function can be called to create a new stock for the offer created by the createOffer function call. This new stock's stockAddr corresponds to and its id is set to the current offerId. Then, this function increments offerId by 1. For example, for the stock created by the createTaker function call, the stockAddr can correspond to and id can be the offerId that equals 1. As a comparison, the stocks created by the createTaker and createOffer function calls can end up with the same id, such as 1.

https://github.com/Cyfrin/2024-08-tadle/blob/c249cdb68c37c47025cdc4c4782c8ee3f20a5b98/src/core/PreMarkets.sol#L164-L284

function createTaker(address _offer, uint256 _points) external payable {
...
if (_points == 0x0) {
revert Errors.AmountIsZero();
}
OfferInfo storage offerInfo = offerInfoMap[_offer];
MakerInfo storage makerInfo = makerInfoMap[offerInfo.maker];
if (offerInfo.offerStatus != OfferStatus.Virgin) {
revert InvalidOfferStatus();
}
if (offerInfo.points < _points + offerInfo.usedPoints) {
revert NotEnoughPoints(
offerInfo.points,
offerInfo.usedPoints,
_points
);
}
/// @dev market place must be online
ISystemConfig systemConfig = tadleFactory.getSystemConfig();
{
MarketPlaceInfo memory marketPlaceInfo = systemConfig
.getMarketPlaceInfo(makerInfo.marketPlace);
marketPlaceInfo.checkMarketPlaceStatus(
block.timestamp,
MarketPlaceStatus.Online
);
}
ReferralInfo memory referralInfo = systemConfig.getReferralInfo(
_msgSender()
);
uint256 platformFeeRate = systemConfig.getPlatformFeeRate(_msgSender());
/// @dev generate stock address
@> address stockAddr = GenerateAddress.generateStockAddress(offerId);
if (stockInfoMap[stockAddr].authority != address(0x0)) {
revert StockAlreadyExist();
}
/// @dev Transfer token from user to capital pool as collateral
uint256 depositAmount = _points.mulDiv(
offerInfo.amount,
offerInfo.points,
Math.Rounding.Ceil
);
uint256 platformFee = depositAmount.mulDiv(
platformFeeRate,
Constants.PLATFORM_FEE_DECIMAL_SCALER
);
uint256 tradeTax = depositAmount.mulDiv(
makerInfo.eachTradeTax,
Constants.EACH_TRADE_TAX_DECIMAL_SCALER
);
ITokenManager tokenManager = tadleFactory.getTokenManager();
_depositTokenWhenCreateTaker(
platformFee,
depositAmount,
tradeTax,
makerInfo,
offerInfo,
tokenManager
);
offerInfo.usedPoints = offerInfo.usedPoints + _points;
/// @dev update stock info
@> stockInfoMap[stockAddr] = StockInfo({
@> id: offerId,
stockStatus: StockStatus.Initialized,
stockType: offerInfo.offerType == OfferType.Ask
? StockType.Bid
: StockType.Ask,
authority: _msgSender(),
maker: offerInfo.maker,
preOffer: _offer,
points: _points,
amount: depositAmount,
offer: address(0x0)
});
@> offerId = offerId + 1;
uint256 remainingPlatformFee = _updateReferralBonus(
platformFee,
depositAmount,
stockAddr,
makerInfo,
referralInfo,
tokenManager
);
makerInfo.platformFee = makerInfo.platformFee + remainingPlatformFee;
_updateTokenBalanceWhenCreateTaker(
_offer,
tradeTax,
depositAmount,
offerInfo,
makerInfo,
tokenManager
);
...
}

Afterwards, the following listOffer function can be called to create a new offer for the stock created by the createTaker function call. This new offer's offerAddr corresponds to and its id is set to the id of the associated stock. For example, for the offer created by the listOffer function call, the offerAddr can correspond to and id can be the offerId that equals 1, which is the id of the stock created by the createTaker function call. As a comparison, the offers created by the listOffer and createOffer function calls can end up with the same id, such as 1.

https://github.com/Cyfrin/2024-08-tadle/blob/c249cdb68c37c47025cdc4c4782c8ee3f20a5b98/src/core/PreMarkets.sol#L295-L396

function listOffer(
address _stock,
uint256 _amount,
uint256 _collateralRate
) external payable {
if (_amount == 0x0) {
revert Errors.AmountIsZero();
}
if (_collateralRate < Constants.COLLATERAL_RATE_DECIMAL_SCALER) {
revert InvalidCollateralRate();
}
StockInfo storage stockInfo = stockInfoMap[_stock];
if (_msgSender() != stockInfo.authority) {
revert Errors.Unauthorized();
}
OfferInfo storage offerInfo = offerInfoMap[stockInfo.preOffer];
MakerInfo storage makerInfo = makerInfoMap[offerInfo.maker];
/// @dev market place must be online
ISystemConfig systemConfig = tadleFactory.getSystemConfig();
MarketPlaceInfo memory marketPlaceInfo = systemConfig
.getMarketPlaceInfo(makerInfo.marketPlace);
marketPlaceInfo.checkMarketPlaceStatus(
block.timestamp,
MarketPlaceStatus.Online
);
if (stockInfo.offer != address(0x0)) {
revert OfferAlreadyExist();
}
if (stockInfo.stockType != StockType.Bid) {
revert InvalidStockType(StockType.Bid, stockInfo.stockType);
}
/// @dev change abort offer status when offer settle type is turbo
if (makerInfo.offerSettleType == OfferSettleType.Turbo) {
address originOffer = makerInfo.originOffer;
OfferInfo memory originOfferInfo = offerInfoMap[originOffer];
if (_collateralRate != originOfferInfo.collateralRate) {
revert InvalidCollateralRate();
}
originOfferInfo.abortOfferStatus = AbortOfferStatus.SubOfferListed;
}
/// @dev transfer collateral when offer settle type is protected
if (makerInfo.offerSettleType == OfferSettleType.Protected) {
uint256 transferAmount = OfferLibraries.getDepositAmount(
offerInfo.offerType,
offerInfo.collateralRate,
_amount,
true,
Math.Rounding.Ceil
);
ITokenManager tokenManager = tadleFactory.getTokenManager();
tokenManager.tillIn{value: msg.value}(
_msgSender(),
makerInfo.tokenAddress,
transferAmount,
false
);
}
@> address offerAddr = GenerateAddress.generateOfferAddress(stockInfo.id);
if (offerInfoMap[offerAddr].authority != address(0x0)) {
revert OfferAlreadyExist();
}
/// @dev update offer info
@> offerInfoMap[offerAddr] = OfferInfo({
@> id: stockInfo.id,
authority: _msgSender(),
maker: offerInfo.maker,
offerStatus: OfferStatus.Virgin,
offerType: offerInfo.offerType,
abortOfferStatus: AbortOfferStatus.Initialized,
points: stockInfo.points,
amount: _amount,
collateralRate: _collateralRate,
usedPoints: 0,
tradeTax: 0,
settledPoints: 0,
settledPointTokenAmount: 0,
settledCollateralAmount: 0
});
stockInfo.offer = offerAddr;
...
}

Impact

For the offer and stock created by the createOffer function call, the offerId that such offer's offerAddr and stock's stockAddr correspond to and the offerId that such offer's and stock's id are set to are inconsistent. Users, who want to match such offer or stock, might apply the GenerateAddress.generateOfferAddress or GenerateAddress.generateStockAddress function' logics with such offer's or stock's id as the input but the resulting offer or stock address would incorrectly be for the offer or stock that is not the wanted offer or stock, which cause them to match incorrect offer or stock.

Since the id of the stocks created by the createOffer and createTaker functions can be the same, users, who apply the GenerateAddress.generateStockAddress function' logics with such id as the input, can only retrieve the address of the stock created by the createTaker function. These users are not able to match the stock created by the createOffer function if only knowing such stock's id.

Similarly, since the id of the offers created by the createOffer and listOffer functions can be the same, users, who apply the GenerateAddress.generateOfferAddress function' logics with such id as the input, can only retrieve the address of the offer created by the listOffer function. These users are not able to match the offer created by the createOffer function if only knowing such offer's id.

Tools Used

Manual Review

Recommended Mitigation

The createOffer function can be updated to only increment offerId after setting offerInfoMap[offerAddr]'s and stockInfoMap[stockAddr]'s id to the current offerId.

Updates

Lead Judging Commences

0xnevi Lead Judge 10 months ago
Submission Judgement Published
Validated
Assigned finding tags:

finding-PreMarkets-createOffer-offerId-increment-after

I believe this is valid low severity, although there is inconsistency here when using the correct `offerId` for assigning offerIds and generating the unique addresses as seen [here](https://github.com/Cyfrin/2024-08-tadle/blob/04fd8634701697184a3f3a5558b41c109866e5f8/src/core/PreMarkets.sol#L67-L69), this is purely an accounting error for offerIds. If we generate the offerId using current `offerId - 1`, the appropriate listing/taker orders can still be created against those offers.

Support

FAQs

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