Tadle

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

Inconsistent CollateralRate Usage in Transfer Calculations Leading to Potential Financial Discrepancies.

Summary

NOTE: This happens in Protected Mode.

Please Read This First: https://tadle.gitbook.io/tadle/how-tadle-works/mechanics-of-tadle/protected-mode

In Protected Mode, all sellers, whether they are the original or subsequent ones, are required to deposit cryptocurrency as collateral

Read This Also: Transaction #3: Bob’s Relisting: https://tadle.gitbook.io/tadle/how-tadle-works/mechanics-of-tadle/protected-mode#for-sell-offers

Bob, now a maker, lists the 500 points he purchased at a price of $1.10 per point and deposits $550 as collateral.

A critical issue has been identified where the system uses an outdated collateral rate during the calculation
of transfer amounts in transactions. Although users specify a new collateral rate when listing or relisting assets,
the system mistakenly calculates transferAmount using old collateralRate.

Vulnerability Details

According to the Tadle documentation, during the process of relisting assets, the user can specify a new collateral rate _collateralRate.
For example, if Bob specifies a _collateralRate of 15000, the system should use this rate for all subsequent calculations.

However, during the transfer amount calculation, the system erroneously uses the old collateral rate offerInfo.collateralRate, which may still be set at 10000. This incorrect rate is used for calculating the transferAmount, leading to a miscalculation.
After the transaction, the offerInfoMap[offerAddr] is updated with the new _collateralRate 15000.
This inconsistency results in a refund to the user based on the new collateral rate, causing discrepancies between the transferred amount and the refund.

POC: add this in file test/PreMarkets.t.sol

uint256 collateralRate = 10000;
function createOffer_Test(uint256 amount, uint256 points) public {
preMarktes.createOffer(
CreateOfferParams(
marketPlace,
address(mockUSDCToken),
points,
amount,
collateralRate,
300,
OfferType.Ask,
OfferSettleType.Protected
)
);
}
function test_Wrong_CollateralRate() public {
// @notice approve tokenManager to spent tokens.
mockUSDCToken.approve(address(tokenManager), type(uint256).max);
address offerAddr1 = GenerateAddress.generateOfferAddress(0);
address stockAddr1 = GenerateAddress.generateStockAddress(1);
createOffer_Test(1 ether, 1 ether);
preMarktes.createTaker(offerAddr1, 1 ether);
@>>> // @notice 15000 is new collateral.
preMarktes.listOffer(stockAddr1, 1 ether, 15000);
}

file: PreMarkets.sol

function listOffer(address _stock, uint256 _amount, uint256 _collateralRate) external payable {
//....
if (makerInfo.offerSettleType == OfferSettleType.Protected) {
uint256 transferAmount = OfferLibraries.getDepositAmount(
offerInfo.offerType,
@>>> // old collateralRate
@>>> offerInfo.collateralRate,
_amount,
true,
Math.Rounding.Ceil
);
@>>> console.log("TransferAmount:", transferAmount);
//....
}
}

forge test --mt test_Wrong_CollateralRate -vvv

Result:

Ran 1 test for test/PreMarkets.t.sol:PreMarketsTest
[PASS] test_Wrong_CollateralRate() (gas: 1064846)
Logs:
TransferAmount: 1000000000000000000
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 9.88ms (1.56ms CPU time)

As we see the transferAmount is 1000000000000000000 which is wrong it should be 1500000000000000000

and this is the right amount for 15000 collateral rate.

Impact

The transferAmount will be calculated using outdated collateral rate, this will lead to incorrect collateral deposits and refunds.

function listOffer(address _stock, uint256 _amount, uint256 _collateralRate) external payable {
// ...
if (makerInfo.offerSettleType == OfferSettleType.Protected) {
uint256 transferAmount = OfferLibraries.getDepositAmount(
offerInfo.offerType,
@>>> // This is old collateralRate.
@>>> offerInfo.collateralRate,
_amount,
true,
Math.Rounding.Ceil
);
ITokenManager tokenManager = tadleFactory.getTokenManager();
tokenManager.tillIn{value: msg.value}(
_msgSender(),
makerInfo.tokenAddress,
@>>> // transferAmount based on old collateral.
@>>> transferAmount,
false
);
}
//...
offerInfoMap[offerAddr] = OfferInfo({
//...
@>>> // This is new collateralRate.
@>>> collateralRate: _collateralRate,
//...
});
}

But when it comes to refund the new collateralRate 15000 will be used.

function abortAskOffer(address _stock, address _offer) external {
// ...
uint256 totalDepositAmount = OfferLibraries.getDepositAmount(
offerInfo.offerType,
@>>> // this is new collateralRate (15000)
@>>> offerInfo.collateralRate,
totalUsedAmount,
false,
Math.Rounding.Ceil
);
// ...
}
function abortBidTaker(address _stock, address _offer) external {
// ...
uint256 transferAmount = OfferLibraries.getDepositAmount(
preOfferInfo.offerType,
@>>> // this is new collateralRate (15000)
@>>> preOfferInfo.collateralRate,
depositAmount,
false,
Math.Rounding.Floor
);
MakerInfo storage makerInfo = makerInfoMap[preOfferInfo.maker];
ITokenManager tokenManager = tadleFactory.getTokenManager();
@>>> tokenManager.addTokenBalance(
TokenBalanceType.MakerRefund,
_msgSender(),
makerInfo.tokenAddress,
@>>> transferAmount
);
}

Tools Used

Recommendations

Ensure that the transferAmount calculation uses the new collateral rate specified by the user _collateralRate rather than the old offerInfo.collateralRate

function listOffer(address _stock, uint256 _amount, uint256 _collateralRate) external payable {
// ....
if (makerInfo.offerSettleType == OfferSettleType.Protected) {
uint256 transferAmount = OfferLibraries.getDepositAmount(
offerInfo.offerType,
- offerInfo.collateralRate,
+ _collateralRate,
_amount,
true,
Math.Rounding.Ceil
);
// ....
}
}
Updates

Lead Judging Commences

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

finding-PreMarkets-listOffer-collateralRate-manipulate

Valid high severity, because the collateral rate utilized when creating an offer is stale and retrieved from a previously set collateral rate, it allows possible manipilation of refund amounts using an inflated collateral rate to drain funds from the CapitalPool contract

Support

FAQs

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