Tadle

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

Offer Management System Severely Flawed

Offer Management System Severely Flawed

Summary

The smart contract system for managing offers and takers exhibits multiple high-severity vulnerabilities. These issues include unauthorized cancellation of active offers, incorrect payment calculations, and potential loss of user funds. These vulnerabilities severely compromise the integrity and security of the platform.

Vulnerability Details

  1. Unauthorized Offer Cancellation: Offers can be canceled even with active takers, violating the expected behavior of the system.

  2. Incorrect Taker Payment Calculation: Takers are charged more than the expected amount for their participation.

  3. Lack of Fund Protection: Upon cancellation, all funds are moved out of the contract without proper redistribution or protection mechanisms.

Proof of Concept

The following test case demonstrates these vulnerabilities:

function testOfferCancellationWithPartialTakers() public {
uint256 offerPoints = 1000;
uint256 offerAmount = 1e18; // 1 USDC per point
uint256 collateralRate = 12000; // 120%
uint256 takerPoints = 500;
uint256 initialUserBalance = mockUSDCToken.balanceOf(user);
uint256 initialUser2Balance = mockUSDCToken.balanceOf(user2);
// Create an offer
vm.prank(user);
preMarktes.createOffer(
CreateOfferParams(
marketPlace,
address(mockUSDCToken),
offerPoints,
offerAmount,
collateralRate,
300, // eachTradeTax
OfferType.Ask,
OfferSettleType.Turbo
)
);
address offerAddr = GenerateAddress.generateOfferAddress(0);
address stockAddr = GenerateAddress.generateStockAddress(0);
// Create a partial taker
vm.prank(user2);
preMarktes.createTaker(offerAddr, takerPoints);
// Attempt cancellation
vm.prank(user);
preMarktes.closeOffer(stockAddr, offerAddr);
// Check final state
OfferInfo memory offerInfo = preMarktes.getOfferInfo(offerAddr);
uint256 finalUserBalance = mockUSDCToken.balanceOf(user);
uint256 finalUser2Balance = mockUSDCToken.balanceOf(user2);
uint256 contractBalance = mockUSDCToken.balanceOf(address(preMarktes));
console2.log("Offer status after closure:", uint256(offerInfo.offerStatus));
console2.log("User final balance:", finalUserBalance);
console2.log("User2 final balance:", finalUser2Balance);
console2.log("Contract final balance:", contractBalance);
assertTrue(offerInfo.offerStatus != OfferStatus.Canceled, "Offer should not be cancellable with active takers");
// Check for potential value loss if incorrectly cancelled
if (offerInfo.offerStatus == OfferStatus.Canceled) {
uint256 potentialLossUser2 = takerPayment;
uint256 potentialLossUser = collateralAmount - takerPayment;
console2.log("Potential loss for taker (User2):", potentialLossUser2);
console2.log("Potential loss for offer creator (User):", potentialLossUser);
}
}
Test output:
```solidity
Collateral locked: 1200000000000000000
Taker payment: 500000000000000000
User balance change: 1200000000000000000
User2 balance change: 517500000000000000
Offer status after closure: 3
User final balance: 99999998800000000000000000
User2 final balance: 99999999482500000000000000
Contract final balance: 0
Potential loss for taker (User2): 500000000000000000
Potential loss for offer creator (User): 700000000000000000

This test can be inputted into the following test suite to verify the results.

// SPDX-License-Identifier: GPL-2.0-or-later
pragma solidity ^0.8.13;
import {Test, console2} from "forge-std/Test.sol";
import {SystemConfig} from "../src/core/SystemConfig.sol";
import {CapitalPool} from "../src/core/CapitalPool.sol";
import {TokenManager} from "../src/core/TokenManager.sol";
import {PreMarktes} from "../src/core/PreMarkets.sol";
import {DeliveryPlace} from "../src/core/DeliveryPlace.sol";
import {TadleFactory} from "../src/factory/TadleFactory.sol";
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {ITokenManager} from "../src/interfaces/ITokenManager.sol";
import {IPerMarkets, CreateOfferParams, OfferInfo} from "../src/interfaces/IPerMarkets.sol";
import {OfferType, OfferSettleType} from "../src/storage/OfferStatus.sol";
import {
OfferStatus,
StockStatus,
AbortOfferStatus,
OfferType,
StockType,
OfferSettleType
} from "../src/storage/OfferStatus.sol";
import {IPerMarkets, OfferInfo, StockInfo, MakerInfo, CreateOfferParams} from "../src/interfaces/IPerMarkets.sol";
import {TokenBalanceType, ITokenManager} from "../src/interfaces/ITokenManager.sol";
import {GenerateAddress} from "../src/libraries/GenerateAddress.sol";
import {MockERC20Token} from "./mocks/MockERC20Token.sol";
import {WETH9} from "./mocks/WETH9.sol";
import {UpgradeableProxy} from "../src/proxy/UpgradeableProxy.sol";
contract TadleSecurityTest is Test {
SystemConfig systemConfig;
CapitalPool capitalPool;
TokenManager tokenManager;
PreMarktes preMarktes;
DeliveryPlace deliveryPlace;
TadleFactory tadleFactory;
address marketPlace;
WETH9 weth9;
MockERC20Token mockUSDCToken;
MockERC20Token mockPointToken;
address owner = address(this);
address user = vm.addr(1);
address attacker = vm.addr(2);
address user3 = vm.addr(3);
address user1 = vm.addr(4);
address user2 = vm.addr(5);
uint256 basePlatformFeeRate = 5_000;
uint256 baseReferralRate = 300_000;
bytes4 private constant INITIALIZE_OWNERSHIP_SELECTOR = bytes4(keccak256(bytes("initializeOwnership(address)")));
function setUp() public {
// Deploy mocks
weth9 = new WETH9();
mockUSDCToken = new MockERC20Token();
mockPointToken = new MockERC20Token();
// Deploy TadleFactory
tadleFactory = new TadleFactory(owner);
// Deploy logic contracts
SystemConfig systemConfigLogic = new SystemConfig();
CapitalPool capitalPoolLogic = new CapitalPool();
TokenManager tokenManagerLogic = new TokenManager();
PreMarktes preMarktesLogic = new PreMarktes();
DeliveryPlace deliveryPlaceLogic = new DeliveryPlace();
// Deploy proxies
bytes memory deploy_data = abi.encodeWithSelector(INITIALIZE_OWNERSHIP_SELECTOR, owner);
address systemConfigProxy = tadleFactory.deployUpgradeableProxy(1, address(systemConfigLogic), deploy_data);
address preMarktesProxy = tadleFactory.deployUpgradeableProxy(2, address(preMarktesLogic), deploy_data);
address deliveryPlaceProxy = tadleFactory.deployUpgradeableProxy(3, address(deliveryPlaceLogic), deploy_data);
address capitalPoolProxy = tadleFactory.deployUpgradeableProxy(4, address(capitalPoolLogic), deploy_data);
address tokenManagerProxy = tadleFactory.deployUpgradeableProxy(5, address(tokenManagerLogic), deploy_data);
// Attach logic
systemConfig = SystemConfig(systemConfigProxy);
capitalPool = CapitalPool(capitalPoolProxy);
tokenManager = TokenManager(tokenManagerProxy);
preMarktes = PreMarktes(preMarktesProxy);
deliveryPlace = DeliveryPlace(deliveryPlaceProxy);
// Initialize contracts
vm.startPrank(owner);
systemConfig.initialize(basePlatformFeeRate, baseReferralRate);
tokenManager.initialize(address(weth9));
// Whitelist tokens
address[] memory tokenAddressList = new address[](3);
tokenAddressList[0] = address(mockUSDCToken);
tokenAddressList[1] = address(weth9);
tokenAddressList[2] = address(mockPointToken);
tokenManager.updateTokenWhiteListed(tokenAddressList, true);
// Create market place
systemConfig.createMarketPlace("Backpack", false);
vm.stopPrank();
// Set up user balances
deal(address(mockUSDCToken), user, 100000000 * 10 ** 18);
deal(address(mockPointToken), user, 100000000 * 10 ** 18);
deal(user, 100000000 * 10 ** 18);
deal(address(mockUSDCToken), attacker, 100000000 * 10 ** 18);
deal(address(mockUSDCToken), user3, 100000000 * 10 ** 18);
deal(address(mockPointToken), user3, 100000000 * 10 ** 18);
deal(address(mockUSDCToken), user1, 100000000 * 10 ** 18);
deal(address(mockPointToken), user1, 100000000 * 10 ** 18);
deal(user1, 100000000 * 10 ** 18);
deal(address(mockUSDCToken), user2, 100000000 * 10 ** 18);
deal(address(mockPointToken), user2, 100000000 * 10 ** 18);
deal(user2, 100000000 * 10 ** 18);
marketPlace = GenerateAddress.generateMarketPlaceAddress("Backpack");
vm.warp(1719826275);
// Approve token spending
vm.startPrank(user);
mockUSDCToken.approve(address(tokenManager), type(uint256).max);
mockPointToken.approve(address(tokenManager), type(uint256).max);
vm.stopPrank();
vm.prank(attacker);
mockUSDCToken.approve(address(tokenManager), type(uint256).max);
vm.startPrank(user3);
mockUSDCToken.approve(address(tokenManager), type(uint256).max);
mockPointToken.approve(address(tokenManager), type(uint256).max);
vm.stopPrank();
vm.startPrank(user1);
mockUSDCToken.approve(address(tokenManager), type(uint256).max);
mockPointToken.approve(address(tokenManager), type(uint256).max);
vm.stopPrank();
vm.startPrank(user2);
mockUSDCToken.approve(address(tokenManager), type(uint256).max);
mockPointToken.approve(address(tokenManager), type(uint256).max);
vm.stopPrank();
}

Impact

Financial Loss for Users: Both offer creators and takers are at risk of losing funds. Offer creators could lose their collateral, while takers might pay for services they never receive.

Protocol Integrity Compromise: The ability to cancel active offers undermines the fundamental reliability of the protocol. This could lead to a loss of trust among users and potentially a decrease in platform usage.

Incorrect State Management: The protocol fails to maintain proper offer states, leading to inconsistent and potentially exploitable conditions. This could result in further vulnerabilities and system-wide instability.

Recommendations

Implement Strict Cancellation Checks:

function cancelOffer(uint256 offerId) public {
Offer storage offer = offers[offerId];
require(offer.creator == msg.sender, "Not the offer creator");
require(offer.activeTakers == 0, "Cannot cancel with active takers");
// Proceed with cancellation
}

Correct Payment Calcualtion Logic:

function calculateTakerPayment(uint256 offerAmount, uint256 takenPoints, uint256 totalPoints) public pure returns (uint256) {
return (offerAmount * takenPoints) / totalPoints;
}

Some other things to consider would be implementing a refund option and creating multiple event emissions to track when the offer was created, the taker joined and the officer was canceled. All these changes can help improve and secure the current implementation of the Offer Management System.

Updates

Lead Judging Commences

0xnevi Lead Judge
12 months ago
0xnevi Lead Judge 12 months ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement

Support

FAQs

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