However, certain ERC20 tokens enforce restrictions on the maximum approval value. For example, both UNI and COMP tokens revert when the value passed to the approve function exceeds type(uint96).max.
As a result, when the TokenManager calls the approve function on these tokens with type(uint256).max, an error occurs. During withdrawal, the contract subsequently fails to transfer tokens back to the user, resulting in a lock-up of their funds.
The primary impact of this vulnerability is the indefinite lock-up of user funds for specific ERC20 tokens that implement approval restrictions. Users can successfully deposit such tokens into the TokenManager, but will be unable to withdraw them due to the approval failure during the withdrawal process.
pragma solidity ^0.8.13;
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
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 MockToken is IERC20 {
string public constant name = "Mock Token";
string public constant symbol = "M1hI";
uint8 public constant decimals = 18;
uint256 public totalSupply;
mapping(address => uint256) private balances;
mapping(address => mapping(address => uint256)) private allowances;
constructor(uint256 initialSupply) {
totalSupply = initialSupply;
balances[msg.sender] = initialSupply;
}
function balanceOf(address account) external view override returns (uint256) {
return balances[account];
}
function transfer(address recipient, uint256 amount) external override returns (bool) {
require(balances[msg.sender] >= amount, "Insufficient balance");
balances[msg.sender] -= amount;
balances[recipient] += amount;
return true;
}
function allowance(address owner, address spender) external view override returns (uint256) {
return allowances[owner][spender];
}
function approve(address spender, uint256 amount) external override returns (bool) {
require(amount <= type(uint96).max, "Approval exceeds token maximum");
allowances[msg.sender][spender] = amount;
return true;
}
function transferFrom(address sender, address recipient, uint256 amount) external override returns (bool) {
require(balances[sender] >= amount, "Insufficient balance");
require(allowances[sender][msg.sender] >= amount, "Allowance exceeded");
allowances[sender][msg.sender] -= amount;
balances[sender] -= amount;
balances[recipient] += amount;
return true;
}
}
contract TokenManagerTest is Test {
uint256 constant initialSupply = 5000 * 10**18;
SystemConfig systemConfig;
CapitalPool capitalPool;
TokenManager tokenManager;
PreMarktes preMarktes;
DeliveryPlace deliveryPlace;
MockToken public mockToken;
TadleFactory tadleFactory;
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 = new TadleFactory(user1);
mockToken = new MockToken(initialSupply);
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[1] = address(weth9);
tokenAddressList[0] = address(mockToken);
tokenManager.updateTokenWhiteListed(tokenAddressList, true);
systemConfig.createMarketPlace("Backpack", false);
capitalPool.approve(address(mockUSDCToken));
vm.stopPrank();
deal(address(mockToken), user, 100000 * 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);
deal(address(mockUSDCToken), capitalPoolProxy, 100000 * 10 ** 18);
}
function testTokensStuckInPool() public {
vm.deal(user, 10 ether);
uint256 amount = 10 * 10**18;
vm.startPrank(user);
mockToken.approve(address(tokenManager), amount);
vm.stopPrank();
vm.prank(address(preMarktes));
tokenManager.tillIn(
user, address(mockToken), amount, false
);
vm.prank(address(preMarktes));
tokenManager.addTokenBalance(
TokenBalanceType.RemainingCash,
user,
address(mockToken),
amount
);
vm.startPrank(user);
try tokenManager.withdraw(
address(mockToken),
TokenBalanceType.RemainingCash
) {
console2.log("Withdrawal succeeded unexpectedly");
} catch (bytes memory error) {
console2.log("Withdrawal failed as expected with error:");
console2.logBytes(error);
}
vm.stopPrank();
}
function testApproveInCapitalPool() public {
vm.prank(address(tokenManager));
try capitalPool.approve(address(mockToken)) {
console2.log("Approval succeeded unexpectedly");
} catch (bytes memory error) {
console2.log("Approval failed as expected with error:");
console2.logBytes(error);
}
}
}
Modify the approve function within the CapitalPool contract to handle conditional approval amounts. If a large approval amount is requested, split it into smaller approved amounts that do not exceed the token's constraints.
For example, instead of approving type(uint256).max, the contract could approve type(uint96).max and dynamically adjust according to the token's specific limits.