Tadle

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

Double deposit in `DeliveryPlace::settleAskTaker` leads to a loss of funds for takers

Summary

DeliveryPlace::settleAskTaker incorrectly requires the taker to deposit tokens again during settlement, even though they have already provided tokens when creating the taker order. This leads to a double deposit situation, potentially causing the taker to lose funds.

DeliveryPlace.sol#L377-L382

// src/core/DeliveryPlace.sol
function settleAskTaker(address _stock, uint256 _settledPoints) external {
// existing code...
uint256 settledPointTokenAmount = marketPlaceInfo.tokenPerPoint * _settledPoints;
ITokenManager tokenManager = tadleFactory.getTokenManager();
if (settledPointTokenAmount > 0) {
@> tokenManager.tillIn(_msgSender(), marketPlaceInfo.tokenAddress, settledPointTokenAmount, true);
tokenManager.addTokenBalance(
TokenBalanceType.PointToken, offerInfo.authority, makerInfo.tokenAddress, settledPointTokenAmount
);
}
// the rest of the function...
}

Impact

When settling an ask order, takers are forced to deposit tokens they've already provided, effectively doubling their required contribution. This not only locks up more of the taker's funds than necessary but also creates an imbalance in the protocol's token accounting.

In extreme cases, if a taker doesn't have sufficient additional tokens, they might be unable to settle their own orders, leading to failed transactions and potential loss of opportunities.

Proof of Concept

NOTE: for this test to run, my other finding by the title of "Incorrect authorization check in DeliveryPlace::settleAskTaker allows maker to settle taker's ask order" must be resolved first. It will fix the authorization issue of the function and allow us to continue with the flow of taker settling their offer.

Place the following test in PreMarkets.t.sol:

function test_settleAskTaker_takerDoubleDeposit() public {
// ~~~~~~~~~~~~~~~~~~~~ Setup ~~~~~~~~~~~~~~~~~~~~
address alice = makeAddr("alice");
address bob = makeAddr("bob");
uint256 INITIAL_BALANCE = 1000 * 1e18;
uint256 MAKER_POINTS = 10_000;
uint256 TAKER_POINTS = 5_000;
uint256 TOKEN_AMOUNT = 100 * 1e18;
uint256 COLLATERAL_RATE = 10_000 * 1.2; // 120% collateral rate
uint256 EACH_TRADE_TAX = 10_000 * 0.03; // 3% trade tax
// Provide Alice and Bob with initial token balances
deal(address(mockUSDCToken), alice, INITIAL_BALANCE);
deal(address(mockUSDCToken), bob, INITIAL_BALANCE);
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// ~~~~~~~~~~ 1) Create a maker BID offer ~~~~~~~~~~
vm.startPrank(alice);
mockUSDCToken.approve(address(tokenManager), type(uint256).max);
preMarktes.createMaker(
CreateMakerParams(
marketPlace,
address(mockUSDCToken),
MAKER_POINTS,
TOKEN_AMOUNT,
COLLATERAL_RATE,
EACH_TRADE_TAX,
OfferType.Bid,
OfferSettleType.Turbo
)
);
address offerAddr = GenerateAddress.generateOfferAddress(0);
vm.stopPrank();
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// ~~~~~~~~~~ 2) Create a taker ASK order ~~~~~~~~~~
vm.startPrank(bob);
mockUSDCToken.approve(address(tokenManager), type(uint256).max);
console.log("> Bob's actual balance BEFORE createTaker: %18e", mockUSDCToken.balanceOf(bob));
preMarktes.createTaker(offerAddr, TAKER_POINTS);
console.log(
"> Bob's actual balance AFTER createTaker (and after deposit): %18e\n", mockUSDCToken.balanceOf(bob)
);
address stockAddr = GenerateAddress.generateStockAddress(1);
vm.stopPrank();
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// ~~~~~~~~~~ 3) Update marketplace to AskSettling status ~~~~~~~~~~
vm.startPrank(user1);
systemConfig.updateMarket("Backpack", address(mockUSDCToken), 0.01 * 1e18, block.timestamp - 1, 3600);
vm.stopPrank();
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// ~~~~~~~~~~ 4) Bob settles the ask ~~~~~~~~~~
vm.startPrank(bob);
uint256 bobBalanceBeforeSettlement = mockUSDCToken.balanceOf(bob);
deliveryPlace.settleAskTaker(stockAddr, TAKER_POINTS);
uint256 bobBalanceAfterSettlement = mockUSDCToken.balanceOf(bob);
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
console.log(">> Bob's actual balance BEFORE settleAskTaker: %18e", bobBalanceBeforeSettlement);
console.log(
">> Bob's actual balance AFTER settleAskTaker (and after second deposit): %18e\n", bobBalanceAfterSettlement
);
uint256 bobSalesRevenue =
tokenManager.userTokenBalanceMap(bob, address(mockUSDCToken), TokenBalanceType.SalesRevenue);
uint256 collateralRefund =
tokenManager.userTokenBalanceMap(bob, address(mockUSDCToken), TokenBalanceType.RemainingCash);
console.log("- Bob's sales revenue: %18e", bobSalesRevenue);
console.log("- Bob's collateral refund: %18e", collateralRefund);
uint256 expectedFinalBalance = bobBalanceBeforeSettlement + bobSalesRevenue + collateralRefund; // bobBalanceBeforeSettlement == Bob's balance with trade tax, but without extra deposit + other
uint256 actualFinalBalance = bobBalanceAfterSettlement + bobSalesRevenue + collateralRefund; // bobBalanceAfterSettlement == Bob's balance with trade tax and extra deposit + other
console.log(">> Bob's expected final balance: %18e", expectedFinalBalance);
console.log(">> Bob's actual final balance: %18e\n", actualFinalBalance);
assertLt(
actualFinalBalance,
expectedFinalBalance,
"Bob's final balance should be higher than initial, accounting for token sale"
);
console.log(
">>>> Bob's funds are reduced due to double deposit. Lost amount: %18e",
expectedFinalBalance - actualFinalBalance
);
}

Running forge test --mt test_settleAskTaker_takerDoubleDeposit -vvv should yield the following output:

Ran 1 test for test/PreMarkets.t.sol:PreMarketsTest
[PASS] test_settleAskTaker_takerDoubleDeposit() (gas: 1291577)
Logs:
> Bob's actual balance BEFORE createTaker: 1000
> Bob's actual balance AFTER createTaker (and after deposit): 938.25
>> Bob's actual balance BEFORE settleAskTaker: 938.25
>> Bob's actual balance AFTER settleAskTaker (and after second deposit): 888.25
- Bob's sales revenue: 50
- Bob's collateral refund: 60
>> Bob's expected final balance: 1048.25
>> Bob's actual final balance: 998.25
>>>> Bob's funds are reduced due to double deposit. Lost amount: 50

This test shows that due to double deposit (one in createTaker and another one in settleAskTaker), Bob lost 50 tokens, which is exactly the amount of the second deposit.

Tools Used

Foundry, Manual code review

Recommended Mitigation Steps

  • Remove the additional token deposit requirement in the settleAskTaker function.

  • Ensure that the function only handles the settlement process by transferring the already deposited tokens from the contract to the maker, by increasing internal balance tracking:

function settleAskTaker(address _stock, uint256 _settledPoints) external {
// existing code...
uint256 settledPointTokenAmount = marketPlaceInfo.tokenPerPoint * _settledPoints;
ITokenManager tokenManager = tadleFactory.getTokenManager();
if (settledPointTokenAmount > 0) {
- tokenManager.tillIn(_msgSender(), marketPlaceInfo.tokenAddress, settledPointTokenAmount, true);
tokenManager.addTokenBalance(
TokenBalanceType.PointToken, offerInfo.authority, makerInfo.tokenAddress, settledPointTokenAmount
);
}
// the rest of the function...
}
Updates

Lead Judging Commences

0xnevi Lead Judge over 1 year ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement
Assigned finding tags:

[invalid] finding-PreMarkets-settleAskMaker-settleAskTaker-no-msg.value-sent

Invalid, in `settleAskMaker` and `settleAskTaker` you are settling the point token to be given to the takers, which is an ERC20 token, so no native ETH is involved and thus no msg.value is required.

Appeal created

irondevx Submitter
over 1 year ago
0xnevi Lead Judge
about 1 year ago
0xnevi Lead Judge about 1 year ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement

Support

FAQs

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

Give us feedback!