Tadle

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

`closeBidTaker` uses `makerInfo.tokenAddress` for point token update making users to not receive their points

Summary

closeBidTaker in DeliveryPlace contract uses makerInfo.tokenAddress for user point token update, causing users to not receive their points.

Vulnerability Details

The code in the closeBidTaker function is using makerInfo.tokenAddress when updating the point balance of a bid taker (point buyer):

tokenManager.addTokenBalance(
TokenBalanceType.PointToken,
_msgSender(),
makerInfo.tokenAddress, //<---
pointTokenAmount
);

makerInfo.tokenAddressis the address of the token used for trading (i.e. collateral token).

/**
* @title MakerInfo
* @dev Struct of MakerInfo
* @notice offerSettleType, authority, marketPlace, tokenAddress, originOffer, platformFee, eachTradeTax
* @param offerSettleType the settle type of offer.
* @param authority the owner of maker, same as the authority of originOffer.
* @param marketPlace the marketPlace of maker.
*/
struct MakerInfo {
OfferSettleType offerSettleType;
address authority;
address marketPlace;
address tokenAddress; //<--- Collateral/Trading token
address originOffer;
uint256 platformFee;
uint256 eachTradeTax;
}

The marketPlaceInfo structure contains a tokenAddress field, which is set to the points token address. This should be the address used for point token operations/updates.

/**
* @title MarketPlaceInfo
* @dev Struct of MarketPlaceInfo
* @notice fixedratio, status, tokenAddress, tokenPerPoint, tge, settlementPeriod
* @param fixedratio maketPlace is fixedratio type or not
* @param status marketPlace status, detail see MarketPlaceStatus
* @param tokenAddress the point token address //<---
* @param tokenPerPoint token per point
* @param tge Token Generation Even
* @param settlementPeriod settlement period
*/
struct MarketPlaceInfo {
bool fixedratio;
MarketPlaceStatus status;
address tokenAddress; //<--- Point token
uint256 tokenPerPoint;
uint256 tge;
uint256 settlementPeriod;
}

Impact

This could lead to accounting issues, where a user's points are not updated for withdrawal but instead, the trading token is updated. This could lead to users receiving not receiving their points when they close a bid and withdraw. Their points will be stuck in the CapitalPool contract.

Tools Used

Foundry

POC

// SPDX-License-Identifier: GPL-2.0-or-later
pragma solidity ^0.8.13;
import {Test} from "forge-std/Test.sol";
import "forge-std/console.sol";
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.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 {OfferStatus, StockStatus, AbortOfferStatus, OfferType, StockType, OfferSettleType} from "../src/storage/OfferStatus.sol";
import {MarketPlaceStatus} from "src/interfaces/ISystemConfig.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 {UpgradeableProxy} from "../src/proxy/UpgradeableProxy.sol";
contract TadleTest is Test {
TadleFactory tadleFactory;
SystemConfig systemConfig;
PreMarktes preMarktes;
DeliveryPlace deliveryPlace;
CapitalPool capitalPool;
TokenManager tokenManager;
address marketPlace;
MockERC20Token mockPointToken;
uint256 basePlatformFeeRate = 5_000; //5_000/1_000_000 = 0.005 / 0.5%
uint256 baseReferralRate = 300_000; //300_000/1_000_000 = 0.3 = 30%
bytes4 private constant INITIALIZE_OWNERSHIP_SELECTOR =
bytes4(keccak256(bytes("initializeOwnership(address)"))); //From `Rescueable`
address public deployer = makeAddr("deployer");
address public tadleAdmin = makeAddr("tadleAdmin"); //0xa1324b4Cbf954Ddb55D505f7aDC5f647Bc31162C -> a.k.a "guardian". Guardian is a multisig
address public backpackAdmin = makeAddr("backpackAdmin");
// Users
address[] users;
address payable user1; //0x29E3b139f4393aDda86303fcdAa35F60Bb7092bF
address payable user2; //0x537C8f3d3E18dF5517a58B3fB9D9143697996802
address payable user3; //0xc0A55e2205B289a967823662B841Bd67Aa362Aec
address payable user4; //0x90561e5Cd8025FA6F52d849e8867C14A77C94BA0
address payable user5; //0x22068447936722AcB3481F41eE8a0B7125526D55
address payable userX; // 0x56aF545C7Db5d63e5f9FBBf8a9F7C5aDc84b1615
address payable public WETH = payable(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2); //WETH
address USDC = 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48;
address USDT = 0xdAC17F958D2ee523a2206206994597C13D831ec7;
address DAI = 0x6B175474E89094C44Da98b954EedeAC495271d0F;
address binanceWhale = 0x47ac0Fb4F2D84898e4D9E7b4DaB3C24507a6D503;
function setUp() public {
vm.createSelectFork("https://rpc.ankr.com/eth");
_createAndDealUsersTokens();
vm.startPrank(deployer);
tadleFactory = new TadleFactory(address(tadleAdmin));
SystemConfig systemConfigLogic = new SystemConfig();
PreMarktes preMarktesLogic = new PreMarktes();
DeliveryPlace deliveryPlaceLogic = new DeliveryPlace();
CapitalPool capitalPoolLogic = new CapitalPool();
TokenManager tokenManagerLogic = new TokenManager();
vm.stopPrank();
vm.startPrank(address(tadleAdmin));
// Data to set ownership
bytes memory deploy_data = abi.encodeWithSelector(
INITIALIZE_OWNERSHIP_SELECTOR,
tadleAdmin
);
//Only guardian (tadleAdmin) can deploy related contracts
address systemConfigProxy = tadleFactory.deployUpgradeableProxy(
1,
address(systemConfigLogic),
bytes(deploy_data)
);
address preMarktesProxy = tadleFactory.deployUpgradeableProxy(
2,
address(preMarktesLogic),
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();
vm.startPrank(address(deployer));
// Deployer attaches logic to proxy. The "proxied" instance is what the users will call.
systemConfig = SystemConfig(systemConfigProxy);
preMarktes = PreMarktes(preMarktesProxy);
deliveryPlace = DeliveryPlace(deliveryPlaceProxy);
capitalPool = CapitalPool(capitalPoolProxy);
tokenManager = TokenManager(tokenManagerProxy);
vm.stopPrank();
vm.startPrank(address(backpackAdmin));
mockPointToken = new MockERC20Token("Backpack Points", "BPP");
deal(address(mockPointToken), tadleAdmin, 100_000_000e18); //Settler
deal(address(mockPointToken), user1, 100_000_000e18); //Maker
deal(address(mockPointToken), user2, 50_000_000e18); //Maker
vm.stopPrank();
vm.startPrank(tadleAdmin);
// Initialize necesary parameters
systemConfig.initialize(basePlatformFeeRate, baseReferralRate);
tokenManager.initialize(address(WETH));
address[] memory tokenAddressList = new address[](4);
tokenAddressList[0] = address(WETH);
tokenAddressList[1] = address(DAI);
tokenAddressList[2] = address(USDC);
tokenAddressList[2] = address(USDT);
tokenManager.updateTokenWhiteListed(tokenAddressList, true);
// create market place
systemConfig.createMarketPlace("Backpack", false); //This sets the market status to online at init time.
systemConfig.updateMarket(
"Backpack",
address(mockPointToken),
0.01 * 1e18, //Token/point = $0.01. Points are scaled to 18 decimals.
1724112000, //Tuesday, August 20, 2024 00:00:00 AM GMT+00:00
1 days
);
marketPlace = GenerateAddress.generateMarketPlaceAddress("Backpack");
vm.stopPrank();
vm.label(address(tadleFactory), "TadleFactory");
vm.label(address(systemConfig), "SystemConfig");
vm.label(address(preMarktes), "PreMarktes");
vm.label(address(deliveryPlace), "DeliveryPlace");
vm.label(address(capitalPool), "CapitalPool");
vm.label(address(tokenManager), "TokenManager");
vm.label(address(marketPlace), "Backpack MarketPlace");
vm.label(address(mockPointToken), "Backpack Points");
vm.label(address(DAI), "DAI");
}
function _createAndDealUsersTokens() internal {
user1 = payable(makeAddr("user1"));
user2 = payable(makeAddr("user2"));
user3 = payable(makeAddr("user3"));
user4 = payable(makeAddr("user4"));
user5 = payable(makeAddr("user5"));
userX = payable(makeAddr("userX"));
users.push(user1);
users.push(user2);
users.push(user3);
users.push(user4);
users.push(user5);
for (uint256 i = 0; i < users.length; i++) {
vm.deal(users[i], 20 ether);
vm.prank(address(binanceWhale));
IERC20(USDC).transfer(users[i], 100_000e6);
deal(USDT, users[i], 100_000e6);
deal(DAI, users[i], 100_000e18);
deal(WETH, users[i], 20e18);
}
// Deal attacker
vm.deal(userX, 20 ether);
vm.prank(address(binanceWhale));
IERC20(USDC).transfer(userX, 100_000e6);
deal(USDT, userX, 100_000e6);
deal(DAI, userX, 100_000e18);
deal(WETH, userX, 20e18);
}
function testTadle() public {
createOffer();
}
function createOffer() public {
/**
struct CreateOfferParams {
address marketPlace;
address tokenAddress;
uint256 points;
uint256 amount;
uint256 collateralRate;
uint256 eachTradeTax;
OfferType offerType;
OfferSettleType offerSettleType;
}
*/
vm.startPrank(address(user1));
IERC20(DAI).approve(address(tokenManager), type(uint256).max);
CreateOfferParams memory createOfferParams = CreateOfferParams(
marketPlace,
address(DAI),
1_000, // ~ $10,000 worth of points
0.01 * 1e18, //1 point = 0.01 DAI
12000, //1.2%
300, //0.03%
OfferType.Ask, //Ask -> create offer to sell points
OfferSettleType.Turbo //Only original trader deposits collateral
);
// Called as "AskMaker" i.e. to sell points
preMarktes.createOffer(createOfferParams); //`OfferId` becomes 1 after offer is created.
/**
_offer: 0xE619a2899a8db14983538159ccE0d238074a235d,
_maker: 0x6a6E1BCA653147228386D44b6B93bE715c9f4497,
_stock: 0x41e7A7cD0C389cD6015D23df7A112c6CC19A192F,
*/
vm.stopPrank();
vm.startPrank(address(user3));
//address offerAddr = GenerateAddress.generateOfferAddress(0);
IERC20(DAI).approve(address(tokenManager), 20_000e18); //Approve 10_000 DAI
address offerAddr = GenerateAddress.generateOfferAddress(0);
preMarktes.createTaker(offerAddr, 1_000);
address stock1Addr = GenerateAddress.generateStockAddress(1);
//preMarktes.listOffer(stock1Addr, 0.01 * 1e18, 12000);
//address offer1Addr = GenerateAddress.generateOfferAddress(1);
// preMarktes.closeOffer(stock1Addr, offer1Addr);
// preMarktes.relistOffer(stock1Addr, offer1Addr);
IERC20(DAI).balanceOf(user3);
IERC20(mockPointToken).balanceOf(user3);
vm.stopPrank();
/**
warp(1724112000 [1.724e9])
warp(1724198460 [1.724e9])
*/
//vm.warp(1724112000);
uint256 currentTime = block.timestamp;
uint256 timeToSkip = 1724112000 - currentTime;
skip(timeToSkip);
skip(1 days);
vm.startPrank(address(user1));
IERC20(mockPointToken).approve(address(tokenManager), type(uint256).max);
deliveryPlace.settleAskMaker(offerAddr, 1_000);
vm.stopPrank();
vm.startPrank(address(user3));
deliveryPlace.closeBidTaker(stock1Addr);
IERC20(mockPointToken).balanceOf(user3); // 0 Balance. Points token was not updated. The collateral token was.
IERC20(mockPointToken).balanceOf(address(capitalPool)); // 10e18
tokenManager.withdraw(address(mockPointToken), TokenBalanceType.PointToken);
vm.stopPrank();
IERC20(mockPointToken).balanceOf(user3); //0 balance. User gets no token
IERC20(mockPointToken).balanceOf(address(capitalPool)); //10e18. Balance still in the CapitalPool contract
}
}

Recommendations

The closeBidTaker function should be updated to use marketPlaceInfo.tokenAddress for point token operations.

tokenManager.addTokenBalance(
TokenBalanceType.PointToken,
_msgSender(),
marketPlaceInfo.tokenAddress, //<---
pointTokenAmount
);
Updates

Lead Judging Commences

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

finding-DeliveryPlace-settleAskTaker-closeBidTaker-wrong-makerinfo-token-address-addToken-balance

Valid high severity, In `settleAskTaker/closeBidTaker`, by assigning collateral token to user balance instead of point token, if collateral token is worth more than point, this can cause stealing of other users collateral tokens within the CapitalPool contract, If the opposite occurs, user loses funds based on the points they are supposed to receive

Support

FAQs

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