Tadle

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

Points settlers lose all of their collateral if they do a partial point settlement

Summary

Tadle allows users to seamlessly trade points with collateral tokens. Sellers provide points guarded with deposited collateral, and buyers provide collateral with which they buy the set points. When the trades are complete and the market progresses, sellers settle their points to the buyers to acquire the corresponding collateral. The platform allows settlers to specify the amount of points they want to settle and do partial settlements in case they are short of points. However, currently, if a partial settlement is done, the seller will be debited points but will lose all of his/her collateral, without receiving any funds in return for his points.

Vulnerability Details

When the Tadle marketplace reaches the AskSettling phase, sellers need to invoke either the DeliveryPlace::settleAskTaker(...) or the DeliveryPlace::settleAskMaker(...), depending if they are taking on a BID offer or have created an ASK offer. This is done so that they can transfer their point tokens and in return receive collateral for them. Both functions allow sellers to specify the amount of points that they want to settle and do a partial settlement (if they do not have all of the points right away). However, the current logic in these functions takes into account only the cases where either a full settlement is done, or no points have been settled. If a user provides, let's say half of the required points, he will be debited the point amount, but will not receive any collateral in return, and even worse all of his collateral will be given to the buyer. Even if the seller is obliged to lose his collateral in this case, he/she should at least receive some funds for the settle point tokens.

function settleAskMaker(address _offer, uint256 _settledPoints) external {
__SNIP__
ITokenManager tokenManager = tadleFactory.getTokenManager();
if (settledPointTokenAmount > 0) {
@> tokenManager.tillIn(_msgSender(), marketPlaceInfo.tokenAddress, settledPointTokenAmount, true); // Seller provides all or part of the points
}
uint256 makerRefundAmount;
@> if (_settledPoints == offerInfo.usedPoints) { // handles only the case where the seller provides all of the points; if a partial settlement is done the seller does not receive anything
if (offerInfo.offerStatus == OfferStatus.Virgin) {
makerRefundAmount = 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
);
makerRefundAmount = OfferLibraries.getDepositAmount(
offerInfo.offerType,
offerInfo.collateralRate,
usedAmount,
true,
Math.Rounding.Floor
);
}
tokenManager.addTokenBalance(
TokenBalanceType.SalesRevenue,
_msgSender(),
makerInfo.tokenAddress,
makerRefundAmount
);
}
__SNIP__
}
function settleAskTaker(address _stock, uint256 _settledPoints) external {
__SNIP__
uint256 settledPointTokenAmount = marketPlaceInfo.tokenPerPoint * _settledPoints;
ITokenManager tokenManager = tadleFactory.getTokenManager();
if (settledPointTokenAmount > 0) {
@> tokenManager.tillIn(_msgSender(), marketPlaceInfo.tokenAddress, settledPointTokenAmount, true); // Seller gets points debited
tokenManager.addTokenBalance(
TokenBalanceType.PointToken,
offerInfo.authority,
makerInfo.tokenAddress,
settledPointTokenAmount
);
}
uint256 collateralFee = OfferLibraries.getDepositAmount(
offerInfo.offerType, offerInfo.collateralRate, stockInfo.amount, false, Math.Rounding.Floor
);
@> if (_settledPoints == stockInfo.points) { // Seller will only get collateral if he settles all of the points
tokenManager.addTokenBalance(
// @audit - if owner invokes it, then the owner will get the collateral
TokenBalanceType.RemainingCash,
_msgSender(),
makerInfo.tokenAddress,
collateralFee
);
@> } else { // If he/she has provided part of the point tokens, no funds will be received and the seller will lose all of the provided collateral to the buyer
tokenManager.addTokenBalance(
TokenBalanceType.MakerRefund, offerInfo.authority, makerInfo.tokenAddress, collateralFee
);
}
}

The below PoC shows how the exploit can occur. For the sake of the test, I have fixed the issue with the invalid point token address being passed when assigning PointTokenBalance to users. I have described this issue in my other issue Users can drain the protocol by withdrawing collateral instead of points due to invalid point token address in DeliveryPlace::closeBidTaker(...) and DeliveryPlace::settleAskTaker(...). I have fixed the PreMarkets.sol contract name as it was PreMarktes. I also use a helper function added in TokenManager.sol:

function getUserAccountBalance(address _accountAddress, address _tokenAddress, TokenBalanceType _tokenBalanceType)
external
view
returns (uint256)
{
return userTokenBalanceMap[_accountAddress][_tokenAddress][_tokenBalanceType];
}

The following test can be run by adding the snippets in PreMarkets.t.sol and running forge test --mt testSellerGetsNoCollateralForPartialSettle1 -vv and `forge test --mt testSellerGetsNoCollateralForPartialSettle2 -vv. I am using the following setup for the tests:

PoC Set-up
function setUp() public {
// deploy mocks
weth9 = new WETH9();
TadleFactory tadleFactory = new TadleFactory(user1);
mockUSDCToken = new MockERC20Token();
mockPointToken = new MockERC20Token();
SystemConfig systemConfigLogic = new SystemConfig();
CapitalPool capitalPoolLogic = new CapitalPool();
TokenManager tokenManagerLogic = new TokenManager();
PreMarkets preMarketsLogic = new PreMarkets();
DeliveryPlace deliveryPlaceLogic = new DeliveryPlace();
bytes memory deploy_data = abi.encodeWithSelector(INITIALIZE_OWNERSHIP_SELECTOR, user1);
vm.startPrank(user1);
address systemConfigProxy =
tadleFactory.deployUpgradeableProxy(1, address(systemConfigLogic), bytes(deploy_data));
address preMarketsProxy = tadleFactory.deployUpgradeableProxy(2, address(preMarketsLogic), bytes(deploy_data));
address deliveryPlaceProxy =
tadleFactory.deployUpgradeableProxy(3, address(deliveryPlaceLogic), bytes(deploy_data));
address capitalPoolProxy = tadleFactory.deployUpgradeableProxy(4, address(capitalPoolLogic), bytes(deploy_data));
address tokenManagerProxy =
tadleFactory.deployUpgradeableProxy(5, address(tokenManagerLogic), bytes(deploy_data));
vm.stopPrank();
// attach logic
systemConfig = SystemConfig(systemConfigProxy);
capitalPool = CapitalPool(capitalPoolProxy);
tokenManager = TokenManager(tokenManagerProxy);
preMarkets = PreMarkets(preMarketsProxy);
deliveryPlace = DeliveryPlace(deliveryPlaceProxy);
vm.startPrank(user1);
// initialize
systemConfig.initialize(basePlatformFeeRate, baseReferralRate);
tokenManager.initialize(address(weth9));
address[] memory tokenAddressList = new address[](2);
tokenAddressList[0] = address(mockUSDCToken);
tokenAddressList[1] = address(weth9);
tokenManager.updateTokenWhiteListed(tokenAddressList, true);
// create market place
systemConfig.createMarketPlace("Backpack", false);
systemConfig.updateMarket("Backpack", address(mockUSDCToken), 0.01 * 1e18, block.timestamp - 1, 3600);
vm.stopPrank();
deal(address(mockUSDCToken), user, 10000 * 10 ** 18);
deal(address(mockPointToken), user, 10000 * 10 ** 18);
deal(user, 100 * 10 ** 18);
deal(address(mockUSDCToken), user1, 10000 * 10 ** 18);
deal(address(mockUSDCToken), user2, 10000 * 10 ** 18);
deal(address(mockUSDCToken), user3, 10000 * 10 ** 18);
deal(address(mockPointToken), user2, 10000 * 10 ** 18);
deal(address(mockPointToken), user3, 10000 * 10 ** 18);
marketPlace = GenerateAddress.generateMarketPlaceAddress("Backpack");
vm.warp(1719826275);
vm.prank(user);
mockUSDCToken.approve(address(tokenManager), type(uint256).max);
vm.prank(user);
mockPointToken.approve(address(tokenManager), type(uint256).max);
vm.startPrank(user2);
mockUSDCToken.approve(address(tokenManager), type(uint256).max);
mockPointToken.approve(address(tokenManager), type(uint256).max);
vm.stopPrank();
vm.startPrank(user3);
mockUSDCToken.approve(address(tokenManager), type(uint256).max);
mockPointToken.approve(address(tokenManager), type(uint256).max);
vm.stopPrank();
capitalPool.approve(address(mockUSDCToken)); // I explicitly set the approval here as it is not working properly in the code; this is shown in my other issue
capitalPool.approve(address(mockPointToken));
}
PoC with ASK offer
function testSellerDoesNotGetAnythingOnPartialSettlement() public {
uint256 userInitialCollateralBalance = mockUSDCToken.balanceOf(user);
uint256 userInitialPointBalance = mockPointToken.balanceOf(user);
uint256 user2InitialCollateralBalance = mockUSDCToken.balanceOf(user2);
uint256 user2InitialPointBalance = mockPointToken.balanceOf(user2);
console.log("User token balance before all:", mockUSDCToken.balanceOf(user));
console.log("User point token balance before all:", mockPointToken.balanceOf(user));
console.log("User point token balance before all:", mockUSDCToken.balanceOf(user));
console.log("User2 token balance before all:", mockUSDCToken.balanceOf(user2));
console.log("User2 point token balance before all:", mockPointToken.balanceOf(user2));
console.log("--------------------");
console.log("Capital pool balance before all:", mockUSDCToken.balanceOf(address(capitalPool)));
console.log("Capital pool point token balance before all:", mockPointToken.balanceOf(address(capitalPool)));
console.log("Token manager balance before all:", mockUSDCToken.balanceOf(address(tokenManager)));
console.log("Token manager point token balance before all:", mockPointToken.balanceOf(address(tokenManager)));
console.log("--------------------");
vm.prank(user);
preMarkets.createOffer(
CreateOfferParams(
marketPlace, address(mockUSDCToken), 1000, 1e18, 15000, 300, OfferType.Ask, OfferSettleType.Turbo
)
); // user wants to sell 1000 points, so he/she provides 15e17 collateral
console.log("User point token balance after user creates ASK offer:", mockPointToken.balanceOf(user));
console.log("User token balance after user creates ASK offer:", mockUSDCToken.balanceOf(user));
console.log("User2 token balance after user creates ASK offer:", mockUSDCToken.balanceOf(user2));
console.log("User2 point token balance after user creates ASK offer:", mockPointToken.balanceOf(user2));
console.log("--------------------");
console.log("Capital pool balance after user creates ASK offer:", mockUSDCToken.balanceOf(address(capitalPool)));
console.log("Capital pool point token balance after user creates ASK offer:", mockPointToken.balanceOf(address(capitalPool)));
console.log("Token manager balance after user creates ASK offer:", mockUSDCToken.balanceOf(address(tokenManager)));
console.log("Token manager point token balance after user creates ASK offer:", mockPointToken.balanceOf(address(tokenManager)));
console.log("--------------------");
assertEq(mockUSDCToken.balanceOf(address(capitalPool)), 15e17); // Capital Pool receives the seller collateral deposit
address offerAddrUserTurbo = GenerateAddress.generateOfferAddress(0);
vm.prank(user2);
preMarkets.createTaker(offerAddrUserTurbo, 1000); // user2 buys the 1000 points from user, deposits 15e17 collateral, user gets 1e17 sales revenue
console.log("User point token balance after user2 creates BID stock:", mockPointToken.balanceOf(user));
console.log("User token balance after user2 creates BID stock:", mockUSDCToken.balanceOf(user));
console.log("User2 token balance after after user2 creates BID stock:", mockUSDCToken.balanceOf(user2));
console.log("User2 point token balance after user2 creates BID stock:", mockPointToken.balanceOf(user2));
console.log("--------------------");
console.log("Capital pool balance after user2 creates BID stock:", mockUSDCToken.balanceOf(address(capitalPool)));
console.log("Capital pool point token balance after user2 creates BID stock:", mockPointToken.balanceOf(address(capitalPool)));
console.log("Token manager balance after after user2 creates BID stock:", mockUSDCToken.balanceOf(address(tokenManager)));
console.log("Token manager point token balance after user2 creates BID stock:", mockPointToken.balanceOf(address(tokenManager)));
console.log("--------------------");
assertEq(mockUSDCToken.balanceOf(address(capitalPool)), 2535000000000000000); // Capital Pool receives the buyers deposit + fees
console.log(
"User tax income",
tokenManager.getUserAccountBalance(user, address(mockUSDCToken), TokenBalanceType.TaxIncome)
);
console.log(
"User sales revenue",
tokenManager.getUserAccountBalance(user, address(mockUSDCToken), TokenBalanceType.SalesRevenue)
);
console.log(
"User point token with point token address",
tokenManager.getUserAccountBalance(user, address(mockPointToken), TokenBalanceType.PointToken)
);
console.log(
"User maker refund",
tokenManager.getUserAccountBalance(user, address(mockUSDCToken), TokenBalanceType.MakerRefund)
);
console.log(
"User remianing cash",
tokenManager.getUserAccountBalance(user, address(mockUSDCToken), TokenBalanceType.RemainingCash)
);
console.log("--------------------");
console.log(
"User2 tax income",
tokenManager.getUserAccountBalance(user2, address(mockUSDCToken), TokenBalanceType.TaxIncome)
);
console.log(
"User2 sales revenue",
tokenManager.getUserAccountBalance(user2, address(mockUSDCToken), TokenBalanceType.SalesRevenue)
);
console.log(
"User2 point token with point token address",
tokenManager.getUserAccountBalance(user2, address(mockPointToken), TokenBalanceType.PointToken)
);
console.log(
"User2 maker refund",
tokenManager.getUserAccountBalance(user2, address(mockUSDCToken), TokenBalanceType.MakerRefund)
);
console.log(
"User2 remianing cash",
tokenManager.getUserAccountBalance(user2, address(mockUSDCToken), TokenBalanceType.RemainingCash)
);
console.log("--------------------");
assertEq(tokenManager.getUserAccountBalance(user, address(mockUSDCToken), TokenBalanceType.SalesRevenue), 1e18); // User should get 1e18 collateral tokens for his points as sales revenue
vm.prank(user1);
systemConfig.updateMarket("Backpack", address(mockPointToken), 1e18, block.timestamp - 1, 3600); // Market is updated and one collateral token is worth 1e18 points
vm.prank(user1);
systemConfig.updateMarketPlaceStatus("Backpack", MarketPlaceStatus.AskSettling);
vm.prank(user);
deliveryPlace.settleAskMaker(offerAddrUserTurbo, 500); // User settles half the points, points are debitted from user -> user does not get anything in return
console.log("User point token balance after user settles half:", mockPointToken.balanceOf(user));
console.log("User token balance after user settles half:", mockUSDCToken.balanceOf(user));
console.log("User2 token balance after after user settles half:", mockUSDCToken.balanceOf(user2));
console.log("User2 point token balance after user settles half:", mockPointToken.balanceOf(user2));
console.log("--------------------");
console.log("Capital pool balance after user settles half:", mockUSDCToken.balanceOf(address(capitalPool)));
console.log("Capital pool point token balance after user settles half:", mockPointToken.balanceOf(address(capitalPool)));
console.log("Token manager balance after after user settles half:", mockUSDCToken.balanceOf(address(tokenManager)));
console.log("Token manager point token balance after user settles half:", mockPointToken.balanceOf(address(tokenManager)));
console.log("--------------------");
console.log(
"User tax income",
tokenManager.getUserAccountBalance(user, address(mockUSDCToken), TokenBalanceType.TaxIncome)
);
console.log(
"User sales revenue",
tokenManager.getUserAccountBalance(user, address(mockUSDCToken), TokenBalanceType.SalesRevenue)
);
console.log(
"User point token with point token address",
tokenManager.getUserAccountBalance(user, address(mockPointToken), TokenBalanceType.PointToken)
);
console.log(
"User maker refund",
tokenManager.getUserAccountBalance(user, address(mockUSDCToken), TokenBalanceType.MakerRefund)
);
console.log(
"User remianing cash",
tokenManager.getUserAccountBalance(user, address(mockUSDCToken), TokenBalanceType.RemainingCash)
);
console.log("--------------------");
console.log(
"User2 tax income",
tokenManager.getUserAccountBalance(user2, address(mockUSDCToken), TokenBalanceType.TaxIncome)
);
console.log(
"User2 sales revenue",
tokenManager.getUserAccountBalance(user2, address(mockUSDCToken), TokenBalanceType.SalesRevenue)
);
console.log(
"User2 point token with point token address",
tokenManager.getUserAccountBalance(user2, address(mockPointToken), TokenBalanceType.PointToken)
);
console.log(
"User2 maker refund",
tokenManager.getUserAccountBalance(user2, address(mockUSDCToken), TokenBalanceType.MakerRefund)
);
console.log(
"User2 remianing cash",
tokenManager.getUserAccountBalance(user2, address(mockUSDCToken), TokenBalanceType.RemainingCash)
);
console.log("--------------------");
assertEq(tokenManager.getUserAccountBalance(user, address(mockUSDCToken), TokenBalanceType.SalesRevenue), 1e18); // user should receive funds for half of the points but none are received
address stockAddrUser2Turbo = GenerateAddress.generateStockAddress(1);
vm.prank(user2);
deliveryPlace.closeBidTaker(stockAddrUser2Turbo); // User2 closes the bid, to receive the points; he/she also gets the full collateral amount from the user
console.log("User point token balance after user2 closes BID:", mockPointToken.balanceOf(user));
console.log("User token balance after user2 closes BID:", mockUSDCToken.balanceOf(user));
console.log("User2 token balance after after user2 closes BID:", mockUSDCToken.balanceOf(user2));
console.log("User2 point token balance after user2 closes BID:", mockPointToken.balanceOf(user2));
console.log("--------------------");
console.log("Capital pool balance after user2 closes BID:", mockUSDCToken.balanceOf(address(capitalPool)));
console.log("Capital pool point token balance after user2 closes BID:", mockPointToken.balanceOf(address(capitalPool)));
console.log("Token manager balance after after user2 closes BID:", mockUSDCToken.balanceOf(address(tokenManager)));
console.log("Token manager point token balance after user2 closes BID:", mockPointToken.balanceOf(address(tokenManager)));
console.log("--------------------");
console.log(
"User tax income",
tokenManager.getUserAccountBalance(user, address(mockUSDCToken), TokenBalanceType.TaxIncome)
);
console.log(
"User sales revenue",
tokenManager.getUserAccountBalance(user, address(mockUSDCToken), TokenBalanceType.SalesRevenue)
);
console.log(
"User point token with point token address",
tokenManager.getUserAccountBalance(user, address(mockPointToken), TokenBalanceType.PointToken)
);
console.log(
"User maker refund",
tokenManager.getUserAccountBalance(user, address(mockUSDCToken), TokenBalanceType.MakerRefund)
);
console.log(
"User remianing cash",
tokenManager.getUserAccountBalance(user, address(mockUSDCToken), TokenBalanceType.RemainingCash)
);
console.log("--------------------");
console.log(
"User2 tax income",
tokenManager.getUserAccountBalance(user2, address(mockUSDCToken), TokenBalanceType.TaxIncome)
);
console.log(
"User2 sales revenue",
tokenManager.getUserAccountBalance(user2, address(mockUSDCToken), TokenBalanceType.SalesRevenue)
);
console.log(
"User2 point token with point token address",
tokenManager.getUserAccountBalance(user2, address(mockPointToken), TokenBalanceType.PointToken)
);
console.log(
"User2 maker refund",
tokenManager.getUserAccountBalance(user2, address(mockUSDCToken), TokenBalanceType.MakerRefund)
);
console.log(
"User2 remianing cash",
tokenManager.getUserAccountBalance(user2, address(mockUSDCToken), TokenBalanceType.RemainingCash)
);
console.log("--------------------");
vm.startPrank(user);
tokenManager.withdraw(address(mockUSDCToken), TokenBalanceType.TaxIncome);
tokenManager.withdraw(address(mockUSDCToken), TokenBalanceType.SalesRevenue);
tokenManager.withdraw(address(mockPointToken), TokenBalanceType.PointToken);
tokenManager.withdraw(address(mockUSDCToken), TokenBalanceType.MakerRefund);
tokenManager.withdraw(address(mockUSDCToken), TokenBalanceType.RemainingCash);
vm.stopPrank();
vm.startPrank(user2);
tokenManager.withdraw(address(mockUSDCToken), TokenBalanceType.TaxIncome);
tokenManager.withdraw(address(mockUSDCToken), TokenBalanceType.SalesRevenue);
tokenManager.withdraw(address(mockPointToken), TokenBalanceType.PointToken);
tokenManager.withdraw(address(mockUSDCToken), TokenBalanceType.MakerRefund);
tokenManager.withdraw(address(mockUSDCToken), TokenBalanceType.RemainingCash);
vm.stopPrank();
console.log("User point token balance after users withdraws:", mockPointToken.balanceOf(user));
console.log("User token balance after users withdraws:", mockUSDCToken.balanceOf(user));
console.log("User2 token balance after users withdraws:", mockUSDCToken.balanceOf(user2));
console.log("User2 point token balance after users withdraws:", mockPointToken.balanceOf(user2));
console.log("--------------------");
console.log("Capital pool balance after users withdraws:", mockUSDCToken.balanceOf(address(capitalPool)));
console.log("Capital pool point token balance after users users withdraws:", mockPointToken.balanceOf(address(capitalPool)));
console.log("Token manager balance after user users withdraws:", mockUSDCToken.balanceOf(address(tokenManager)));
console.log("Token manager point token balance after user users withdraws:", mockPointToken.balanceOf(address(tokenManager)));
console.log("--------------------");
assertEq(mockPointToken.balanceOf(user), userInitialPointBalance - 5e20); // user has settled 500 points, the marketplace ratio is 1e18 points per token, so user gets debitter 5e20
assertEq(mockUSDCToken.balanceOf(user), userInitialCollateralBalance - 5e17 + 3e16); // user loses 5e17 (gets 3e16 tax fee) collateral tokens, as he does not get anything for the points and user2 get all of his colllateral
assertEq(mockUSDCToken.balanceOf(user2), user2InitialCollateralBalance + 5e17 - (5e15 + 3e16)); // user2 gets 5e17 collateral tokens from user (pays tax and platform fee)
assertEq(mockPointToken.balanceOf(user2), user2InitialPointBalance + 5e20); // user2 gets 5e20 points from user
}
Ran 1 test for test/PreMarkets.t.sol:PreMarketsTest
[PASS] testSellerGetsNoCollateralForPartialSettle1() (gas: 1388626)
Logs:
User token balance before all: 10000000000000000000000
User point token balance before all: 10000000000000000000000
User point token balance before all: 10000000000000000000000
User2 token balance before all: 10000000000000000000000
User2 point token balance before all: 10000000000000000000000
--------------------
Capital pool balance before all: 0
Capital pool point token balance before all: 0
Token manager balance before all: 0
Token manager point token balance before all: 0
--------------------
User point token balance after user creates ASK offer: 10000000000000000000000
User token balance after user creates ASK offer: 9998500000000000000000
User2 token balance after user creates ASK offer: 10000000000000000000000
User2 point token balance after user creates ASK offer: 10000000000000000000000
--------------------
Capital pool balance after user creates ASK offer: 1500000000000000000
Capital pool point token balance after user creates ASK offer: 0
Token manager balance after user creates ASK offer: 0
Token manager point token balance after user creates ASK offer: 0
--------------------
User point token balance after user2 creates BID stock: 10000000000000000000000
User token balance after user2 creates BID stock: 9998500000000000000000
User2 token balance after after user2 creates BID stock: 9998965000000000000000
User2 point token balance after user2 creates BID stock: 10000000000000000000000
--------------------
Capital pool balance after user2 creates BID stock: 2535000000000000000
Capital pool point token balance after user2 creates BID stock: 0
Token manager balance after after user2 creates BID stock: 0
Token manager point token balance after user2 creates BID stock: 0
--------------------
User tax income 30000000000000000
User sales revenue 1000000000000000000
User point token with point token address 0
User maker refund 0
User remianing cash 0
--------------------
User2 tax income 0
User2 sales revenue 0
User2 point token with point token address 0
User2 maker refund 0
User2 remianing cash 0
--------------------
User point token balance after user settles half: 9500000000000000000000
User token balance after user settles half: 9998500000000000000000
User2 token balance after after user settles half: 9998965000000000000000
User2 point token balance after user settles half: 10000000000000000000000
--------------------
Capital pool balance after user settles half: 2535000000000000000
Capital pool point token balance after user settles half: 500000000000000000000
Token manager balance after after user settles half: 0
Token manager point token balance after user settles half: 0
--------------------
User tax income 30000000000000000
User sales revenue 1000000000000000000
User point token with point token address 0
User maker refund 0
User remianing cash 0
--------------------
User2 tax income 0
User2 sales revenue 0
User2 point token with point token address 0
User2 maker refund 0
User2 remianing cash 0
--------------------
User point token balance after user2 closes BID: 9500000000000000000000
User token balance after user2 closes BID: 9998500000000000000000
User2 token balance after after user2 closes BID: 9998965000000000000000
User2 point token balance after user2 closes BID: 10000000000000000000000
--------------------
Capital pool balance after user2 closes BID: 2535000000000000000
Capital pool point token balance after user2 closes BID: 500000000000000000000
Token manager balance after after user2 closes BID: 0
Token manager point token balance after user2 closes BID: 0
--------------------
User tax income 30000000000000000
User sales revenue 1000000000000000000
User point token with point token address 0
User maker refund 0
User remianing cash 0
--------------------
User2 tax income 0
User2 sales revenue 0
User2 point token with point token address 500000000000000000000
User2 maker refund 0
User2 remianing cash 1500000000000000000
--------------------
User point token balance after users withdraw: 9500000000000000000000
User token balance after users withdraw: 9999530000000000000000
User2 token balance after users withdraw: 10000465000000000000000
User2 point token balance after users withdraw: 10500000000000000000000
--------------------
Capital pool balance after users withdraw: 5000000000000000
Capital pool point token balance after users withdraw: 0
Token manager balance after users withdraw: 0
Token manager point token balance after users withdraw: 0
--------------------
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 20.80ms (4.55ms CPU time)
Ran 1 test suite in 161.00ms (20.80ms CPU time): 1 tests passed, 0 failed, 0 skipped (1 total tests)
PoC BID offer
function testSellerGetsNoCollateralForPartialSettle2() public {
uint256 userInitialCollateralBalance = mockUSDCToken.balanceOf(user);
uint256 userInitialPointBalance = mockPointToken.balanceOf(user);
uint256 user2InitialCollateralBalance = mockUSDCToken.balanceOf(user2);
uint256 user2InitialPointBalance = mockPointToken.balanceOf(user2);
console.log("User token balance before all:", mockUSDCToken.balanceOf(user));
console.log("User point token balance before all:", mockPointToken.balanceOf(user));
console.log("User point token balance before all:", mockUSDCToken.balanceOf(user));
console.log("User2 token balance before all:", mockUSDCToken.balanceOf(user2));
console.log("User2 point token balance before all:", mockPointToken.balanceOf(user2));
console.log("--------------------");
console.log("Capital pool balance before all:", mockUSDCToken.balanceOf(address(capitalPool)));
console.log("Capital pool point token balance before all:", mockPointToken.balanceOf(address(capitalPool)));
console.log("Token manager balance before all:", mockUSDCToken.balanceOf(address(tokenManager)));
console.log("Token manager point token balance before all:", mockPointToken.balanceOf(address(tokenManager)));
console.log("--------------------");
vm.prank(user);
preMarkets.createOffer(
CreateOfferParams(
marketPlace, address(mockUSDCToken), 1000, 1e18, 15000, 300, OfferType.Bid, OfferSettleType.Turbo
)
); // user wants to buy 1000 points, so he/she provides 1e18 collateral
assertEq(mockUSDCToken.balanceOf(address(capitalPool)), 1e18);
console.log("User point token balance after user creates BID offer:", mockPointToken.balanceOf(user));
console.log("User token balance after user creates BID offer:", mockUSDCToken.balanceOf(user));
console.log("User2 token balance after user creates BID offer:", mockUSDCToken.balanceOf(user2));
console.log("User2 point token balance after user creates BID offer:", mockPointToken.balanceOf(user2));
console.log("--------------------");
console.log("Capital pool balance after user creates BID offer:", mockUSDCToken.balanceOf(address(capitalPool)));
console.log("Capital pool point token balance after user creates BID offer:", mockPointToken.balanceOf(address(capitalPool)));
console.log("Token manager balance after user creates BID offer:", mockUSDCToken.balanceOf(address(tokenManager)));
console.log("Token manager point token balance after user creates BID offer:", mockPointToken.balanceOf(address(tokenManager)));
console.log("--------------------");
address offerAddrUserTurbo = GenerateAddress.generateOfferAddress(0);
vm.prank(user2);
preMarkets.createTaker(offerAddrUserTurbo, 1000); // user2 agrees to sell the points to user and provides 1.5e18 collateral + fees and gets 1e18 in sales revenue
assertEq(mockUSDCToken.balanceOf(address(capitalPool)), 2535e15); // Capital Pool receives the sellers deposit + fees
console.log(
"User tax income",
tokenManager.getUserAccountBalance(user, address(mockUSDCToken), TokenBalanceType.TaxIncome)
);
console.log(
"User sales revenue",
tokenManager.getUserAccountBalance(user, address(mockUSDCToken), TokenBalanceType.SalesRevenue)
);
console.log(
"User point token with point token address",
tokenManager.getUserAccountBalance(user, address(mockPointToken), TokenBalanceType.PointToken)
);
console.log(
"User maker refund",
tokenManager.getUserAccountBalance(user, address(mockUSDCToken), TokenBalanceType.MakerRefund)
);
console.log(
"User remianing cash",
tokenManager.getUserAccountBalance(user, address(mockUSDCToken), TokenBalanceType.RemainingCash)
);
console.log("--------------------");
console.log(
"User2 tax income",
tokenManager.getUserAccountBalance(user2, address(mockUSDCToken), TokenBalanceType.TaxIncome)
);
console.log(
"User2 sales revenue",
tokenManager.getUserAccountBalance(user2, address(mockUSDCToken), TokenBalanceType.SalesRevenue)
);
console.log(
"User2 point token with point token address",
tokenManager.getUserAccountBalance(user2, address(mockPointToken), TokenBalanceType.PointToken)
);
console.log(
"User2 maker refund",
tokenManager.getUserAccountBalance(user2, address(mockUSDCToken), TokenBalanceType.MakerRefund)
);
console.log(
"User2 remianing cash",
tokenManager.getUserAccountBalance(user2, address(mockUSDCToken), TokenBalanceType.RemainingCash)
);
console.log("--------------------");
assertEq(tokenManager.getUserAccountBalance(user2, address(mockUSDCToken), TokenBalanceType.SalesRevenue), 1e18); // user2 has 1e18 sales revenue
vm.prank(user1);
systemConfig.updateMarket("Backpack", address(mockPointToken), 1e18, block.timestamp - 1, 3600);
vm.prank(user1);
systemConfig.updateMarketPlaceStatus("Backpack", MarketPlaceStatus.AskSettling);
vm.prank(user2);
deliveryPlace.settleAskTaker(GenerateAddress.generateStockAddress(1), 500); // user2 settles only half of the points -> gets debited but does not receive anything in return -> all of user2's collateral is transferred to user
console.log(
"User tax income",
tokenManager.getUserAccountBalance(user, address(mockUSDCToken), TokenBalanceType.TaxIncome)
);
console.log(
"User sales revenue",
tokenManager.getUserAccountBalance(user, address(mockUSDCToken), TokenBalanceType.SalesRevenue)
);
console.log(
"User point token with point token address",
tokenManager.getUserAccountBalance(user, address(mockPointToken), TokenBalanceType.PointToken)
);
console.log(
"User maker refund",
tokenManager.getUserAccountBalance(user, address(mockUSDCToken), TokenBalanceType.MakerRefund)
);
console.log(
"User remianing cash",
tokenManager.getUserAccountBalance(user, address(mockUSDCToken), TokenBalanceType.RemainingCash)
);
console.log("--------------------");
console.log(
"User2 tax income",
tokenManager.getUserAccountBalance(user2, address(mockUSDCToken), TokenBalanceType.TaxIncome)
);
console.log(
"User2 sales revenue",
tokenManager.getUserAccountBalance(user2, address(mockUSDCToken), TokenBalanceType.SalesRevenue)
);
console.log(
"User2 point token with point token address",
tokenManager.getUserAccountBalance(user2, address(mockPointToken), TokenBalanceType.PointToken)
);
console.log(
"User2 maker refund",
tokenManager.getUserAccountBalance(user2, address(mockUSDCToken), TokenBalanceType.MakerRefund)
);
console.log(
"User2 remianing cash",
tokenManager.getUserAccountBalance(user2, address(mockUSDCToken), TokenBalanceType.RemainingCash)
);
console.log("--------------------");
assertEq(tokenManager.getUserAccountBalance(user, address(mockPointToken), TokenBalanceType.PointToken), 5e20); // user gets accredited the settled points
assertEq(tokenManager.getUserAccountBalance(user, address(mockUSDCToken), TokenBalanceType.MakerRefund), 15e17); // user gets the whole collateral from user
vm.prank(user);
deliveryPlace.closeBidOffer(offerAddrUserTurbo); // user closes the bid, nothing changes
console.log("User point token balance before user withdraw:", mockPointToken.balanceOf(user));
console.log("User token balance before user withdraw:", mockUSDCToken.balanceOf(user));
console.log("User2 token balance before user withdraw:", mockUSDCToken.balanceOf(user2));
console.log("User2 point token before user withdraw:", mockPointToken.balanceOf(user2));
console.log("--------------------");
console.log("Capital pool balance before user withdraw:", mockUSDCToken.balanceOf(address(capitalPool)));
console.log("Capital pool point token balance before user withdraw:", mockPointToken.balanceOf(address(capitalPool)));
console.log("Token manager balance after before user withdraw:", mockUSDCToken.balanceOf(address(tokenManager)));
console.log("Token manager point token balance before user withdraw:", mockPointToken.balanceOf(address(tokenManager)));
console.log("--------------------");
vm.startPrank(user);
tokenManager.withdraw(address(mockUSDCToken), TokenBalanceType.TaxIncome);
tokenManager.withdraw(address(mockUSDCToken), TokenBalanceType.SalesRevenue);
tokenManager.withdraw(address(mockPointToken), TokenBalanceType.PointToken);
tokenManager.withdraw(address(mockUSDCToken), TokenBalanceType.MakerRefund);
tokenManager.withdraw(address(mockUSDCToken), TokenBalanceType.RemainingCash);
vm.stopPrank();
vm.startPrank(user2);
tokenManager.withdraw(address(mockUSDCToken), TokenBalanceType.TaxIncome);
tokenManager.withdraw(address(mockUSDCToken), TokenBalanceType.SalesRevenue);
tokenManager.withdraw(address(mockPointToken), TokenBalanceType.PointToken);
tokenManager.withdraw(address(mockUSDCToken), TokenBalanceType.MakerRefund);
tokenManager.withdraw(address(mockUSDCToken), TokenBalanceType.RemainingCash);
vm.stopPrank();
console.log("User point token balance after user withdraw:", mockPointToken.balanceOf(user));
console.log("User token balance after user withdraw:", mockUSDCToken.balanceOf(user));
console.log("User2 token balance after user withdraw:", mockUSDCToken.balanceOf(user2));
console.log("User2 point token after user withdraw:", mockPointToken.balanceOf(user2));
console.log("--------------------");
console.log("Capital pool balance after user withdraw:", mockUSDCToken.balanceOf(address(capitalPool)));
console.log("Capital pool point token balance after user withdraw:", mockPointToken.balanceOf(address(capitalPool)));
console.log("Token manager balance after after user withdraw:", mockUSDCToken.balanceOf(address(tokenManager)));
console.log("Token manager point token balance after user withdraw:", mockPointToken.balanceOf(address(tokenManager)));
console.log("--------------------");
assertEq(mockPointToken.balanceOf(user), userInitialPointBalance + 5e20); // user receives the settled 500 points from user2; marketplace is 1e18 points per token so 5e20
assertEq(mockUSDCToken.balanceOf(user), userInitialCollateralBalance + 53e16); // user receives the whole collateral from user2, also get 3e16 tax from user2
assertEq(mockUSDCToken.balanceOf(user2), user2InitialCollateralBalance - 5e17 - 35e15); // user2 loses the whole collateral to user and pays fees
assertEq(mockPointToken.balanceOf(user2), user2InitialPointBalance - 5e20); // user2 loses the settled points as well
}
Ran 1 test for test/PreMarkets.t.sol:PreMarketsTest
[PASS] testSellerGetsNoCollateralForPartialSettle2() (gas: 1323963)
Logs:
User token balance before all: 10000000000000000000000
User point token balance before all: 10000000000000000000000
User point token balance before all: 10000000000000000000000
User2 token balance before all: 10000000000000000000000
User2 point token balance before all: 10000000000000000000000
--------------------
Capital pool balance before all: 0
Capital pool point token balance before all: 0
Token manager balance before all: 0
Token manager point token balance before all: 0
--------------------
User point token balance after user creates BID offer: 10000000000000000000000
User token balance after user creates BID offer: 9999000000000000000000
User2 token balance after user creates BID offer: 10000000000000000000000
User2 point token balance after user creates BID offer: 10000000000000000000000
--------------------
Capital pool balance after user creates BID offer: 1000000000000000000
Capital pool point token balance after user creates BID offer: 0
Token manager balance after user creates BID offer: 0
Token manager point token balance after user creates BID offer: 0
--------------------
User tax income 30000000000000000
User sales revenue 0
User point token with point token address 0
User maker refund 0
User remianing cash 0
--------------------
User2 tax income 0
User2 sales revenue 1000000000000000000
User2 point token with point token address 0
User2 maker refund 0
User2 remianing cash 0
--------------------
User tax income 30000000000000000
User sales revenue 0
User point token with point token address 500000000000000000000
User maker refund 1500000000000000000
User remianing cash 0
--------------------
User2 tax income 0
User2 sales revenue 1000000000000000000
User2 point token with point token address 0
User2 maker refund 0
User2 remianing cash 0
--------------------
User point token balance before user withdraw: 10000000000000000000000
User token balance before user withdraw: 9999000000000000000000
User2 token balance before user withdraw: 9998465000000000000000
User2 point token before user withdraw: 9500000000000000000000
--------------------
Capital pool balance before user withdraw: 2535000000000000000
Capital pool point token balance before user withdraw: 500000000000000000000
Token manager balance after before user withdraw: 0
Token manager point token balance before user withdraw: 0
--------------------
User point token balance after user withdraw: 10500000000000000000000
User token balance after user withdraw: 10000530000000000000000
User2 token balance after user withdraw: 9999465000000000000000
User2 point token after user withdraw: 9500000000000000000000
--------------------
Capital pool balance after user withdraw: 5000000000000000
Capital pool point token balance after user withdraw: 0
Token manager balance after after user withdraw: 0
Token manager point token balance after user withdraw: 0
--------------------
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 15.44ms (2.88ms CPU time)
Ran 1 test suite in 160.02ms (15.44ms CPU time): 1 tests passed, 0 failed, 0 skipped (1 total tests)
Impact

Sellers lose all of their collateral to the buyer in the event of a partial settlement and do not receive anything in return for their points. This de-incentivizes users to settle points if they do not have the full amount.

Tools Used

Manual review

Recommendations

Add logic to DeliveryPlace::settleAskTaker(...) and the DeliveryPlace::settleAskMaker(...) functions to handle cases, where the seller provides part of the settled points so that he/she receives funds based on the provided cases.

Updates

Lead Judging Commences

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

finding-DeliveryPlace-settleAskTaker-settleAskMaker-partial-settlements

Valid high, in settleAskTaker/settleAskMaker, if the original offer maker performs a partial final settlement, the existing checks [here](https://github.com/Cyfrin/2024-08-tadle/blob/main/src/core/DeliveryPlace.sol#L356-L358) and [here](https://github.com/Cyfrin/2024-08-tadle/blob/04fd8634701697184a3f3a5558b41c109866e5f8/src/core/DeliveryPlace.sol#L230-L232) will cause an revert when attempting to complete a full settlement, resulting in their collateral being locked and requiring a rescue from the admin. To note, although examples in the documentation implies settlement in a single click, it is not stated that partial settlements are not allowed, so I believe it is a valid user flow.

Support

FAQs

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