Tadle

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

Invalid Usage of CapitalPool.approve()

Summary

The capitalPool.approve() function is incorrectly used during the withdrawal process for native tokens, causing the approval to revert. This issue only affects native tokens, as the approval is unreachable for non-native wrapped tokens. Due to incorrect function usage approvals revert, causing all nativeWrappedToken withdrawals to also revert.

Vulnerability Details

Overview

The capitalPool.approve() function is designed to approve tokens for the token manager. However, it is currently being used in the TokenManager._transfer() function with input parameters address(this) where
address(this) incorrectly is the TokenManager contract, not the tokenAddr. Since the TokenManager contract does not have the approve() function, the call reverts.

Proof of Concept

Please run the following test to reproduce the issue: forge test --via-ir --mt testCloseBidOfferBalancesForWrappedNativeToken

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;
import "forge-std/Test.sol";
import "../src/core/PreMarkets.sol";
import "../src/libraries/GenerateAddress.sol";
import "../src/libraries/Constants.sol";
import "../src/interfaces/ITokenManager.sol";
import "../src/interfaces/ISystemConfig.sol";
import {MockERC20Token} from "./mocks/MockERC20Token.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 {WETH9} from "./mocks/WETH9.sol";
contract IssueTestTemplate is Test {
SystemConfig systemConfig;
CapitalPool capitalPool;
TokenManager tokenManager;
PreMarktes preMarktes;
DeliveryPlace deliveryPlace;
address marketPlace;
string marketPlaceName = "Backpack";
WETH9 weth9;
MockERC20Token mockUSDCToken;
MockERC20Token mockPointToken;
address guardian;
address maker;
address taker1;
address taker2;
address taker3;
uint256 basePlatformFeeRate = 5_000;
uint256 baseReferralRate = 300_000;
bytes4 private constant INITIALIZE_OWNERSHIP_SELECTOR =
bytes4(keccak256(bytes("initializeOwnership(address)")));
function setUp() public {
// Set up accounts
guardian = makeAddr("guardian");
maker = makeAddr("maker");
taker1 = makeAddr("taker1");
taker2 = makeAddr("taker2");
taker3 = makeAddr("taker3");
vm.label(guardian, "guardian");
vm.label(maker, "maker");
vm.label(taker1, "taker1");
vm.label(taker2, "taker2");
vm.label(taker3, "taker3");
// deploy mocks
weth9 = new WETH9();
TadleFactory tadleFactory = new TadleFactory(guardian);
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,
guardian
);
vm.startPrank(guardian);
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.label(systemConfigProxy, "systemConfigProxy");
vm.label(preMarktesProxy, "preMarktesProxy");
vm.label(deliveryPlaceProxy, "deliveryPlaceProxy");
vm.label(capitalPoolProxy, "capitalPoolProxy");
vm.label(tokenManagerProxy, "tokenManagerProxy");
vm.stopPrank();
// attach logic
systemConfig = SystemConfig(systemConfigProxy);
capitalPool = CapitalPool(capitalPoolProxy);
tokenManager = TokenManager(tokenManagerProxy);
preMarktes = PreMarktes(preMarktesProxy);
deliveryPlace = DeliveryPlace(deliveryPlaceProxy);
vm.label(address(systemConfig), "systemConfig");
vm.label(address(tokenManager), "tokenManager");
vm.label(address(preMarktes), "preMarktes");
vm.label(address(deliveryPlace), "deliveryPlace");
vm.startPrank(guardian);
// 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(marketPlaceName, false);
vm.stopPrank();
deal(address(mockUSDCToken), maker, 100000000 * 10 ** 18);
deal(address(mockPointToken), maker, 100000000 * 10 ** 18);
deal(maker, 100000000 * 10 ** 18);
deal(address(mockUSDCToken), taker1, 100000000 * 10 ** 18);
deal(address(mockUSDCToken), taker2, 100000000 * 10 ** 18);
deal(address(mockUSDCToken), taker3, 100000000 * 10 ** 18);
deal(address(mockPointToken), taker2, 100000000 * 10 ** 18);
marketPlace = GenerateAddress.generateMarketPlaceAddress("Backpack");
vm.warp(1719826275);
vm.prank(maker);
mockUSDCToken.approve(address(tokenManager), type(uint256).max);
vm.startPrank(taker2);
mockUSDCToken.approve(address(tokenManager), type(uint256).max);
mockPointToken.approve(address(tokenManager), type(uint256).max);
vm.stopPrank();
}
/* @audit - POC: [capitalpool-approval-invalid-usage.md]
Due to incorrect usage of the capitalPool.approve() function, approvaal during withdrawal reverts for native tokens only (since the approval itself is unreachable for non native wrapped tokens.)
Funds are up to the discretion of the rescuable owner contract to be returned.
Impact: Medium, Likelihood: High
*/
function testCloseBidOfferBalancesForWrappedNativeToken() public {
deal(maker, 0.01 * 1e18);
vm.startPrank(maker);
preMarktes.createOffer{value: 0.01 * 1e18}(
CreateOfferParams(
marketPlace,
address(weth9),
1000, // points
0.01 * 1e18, // amount
12000, // collateral rate
300, // trade tax
OfferType.Bid,
OfferSettleType.Turbo
)
);
vm.stopPrank();
address offerAddr = GenerateAddress.generateOfferAddress(0);
// Get initial balances
uint256 makerInitialRefundBalance = getAccountTypeBalance(address(weth9), maker, TokenBalanceType.MakerRefund);
// Calculate expected refund
uint256 collateralAmount = (0.01 * 1e18 * 12000) / Constants.COLLATERAL_RATE_DECIMAL_SCALER;
uint256 expectedRefund = collateralAmount * 1000;
// Warp time to enter AskSettling state
vm.prank(guardian);
systemConfig.updateMarket(
"Backpack",
address(mockPointToken),
0.01 * 1e18,
block.timestamp - 1,
3600
);
// Close bid offer
vm.prank(maker);
deliveryPlace.closeBidOffer(offerAddr);
// Verify offer status
(OfferInfo memory offerInfo, , , ) = deliveryPlace.getOfferInfo(offerAddr);
assertEq(uint(offerInfo.offerStatus), uint(OfferStatus.Settled), "Offer status should be Settled");
// Optional: Withdraw refund and verify final balance
vm.startPrank(maker);
tokenManager.withdraw(address(weth9), TokenBalanceType.TaxIncome);
tokenManager.withdraw(address(weth9), TokenBalanceType.MakerRefund);
tokenManager.withdraw(address(weth9), TokenBalanceType.ReferralBonus);
tokenManager.withdraw(address(weth9), TokenBalanceType.SalesRevenue);
tokenManager.withdraw(address(weth9), TokenBalanceType.PointToken);
vm.stopPrank();
uint256 makerFinalActualBalance = weth9.balanceOf(maker);
assertEq(makerFinalActualBalance, makerInitialRefundBalance + expectedRefund, "Incorrect maker final actual balance after withdrawal");
}
// Helper functions
function createOffer(address offerer, address tokenAddress, uint256 totalPoints, uint256 tokenAmount, uint256 collateralRate, uint256 tradeTax, OfferType offerType, OfferSettleType settleType) internal returns (address offerAddr) {
vm.startPrank(offerer);
mockUSDCToken.approve(address(tokenManager), type(uint256).max);
preMarktes.createOffer(
CreateOfferParams(
marketPlace,
tokenAddress,
totalPoints,
tokenAmount,
collateralRate,
tradeTax,
offerType,
settleType
)
);
offerAddr = GenerateAddress.generateOfferAddress(0);
vm.stopPrank();
return offerAddr;
}
function createTakerAndGetInitialBalances(address referrer, address offerAddr, uint256 _purchasedPoints) internal returns (uint256 initialReferrerBalance, uint256 initialTakerBalance) {
initialReferrerBalance = tokenManager.userTokenBalanceMap(
referrer,
address(mockUSDCToken),
TokenBalanceType.ReferralBonus
);
initialTakerBalance = tokenManager.userTokenBalanceMap(
taker1,
address(mockUSDCToken),
TokenBalanceType.ReferralBonus
);
vm.startPrank(taker1);
mockUSDCToken.approve(address(tokenManager), type(uint256).max);
uint256 purchasedPoints = _purchasedPoints;
preMarktes.createTaker(offerAddr, purchasedPoints);
vm.stopPrank();
return (initialReferrerBalance, initialTakerBalance);
}
function closeOffer(address offerer, address offerAddr) internal {
vm.prank(offerer);
preMarktes.closeOffer(GenerateAddress.generateStockAddress(0), offerAddr);
}
function abortAskOffer(address offerer, address _stock, address _offer) internal {
vm.prank(offerer);
preMarktes.abortAskOffer(_stock, _offer);
}
function abortBidTaker(address offerer, address _stock, address _offer) internal {
vm.prank(offerer);
preMarktes.abortBidTaker(_stock, _offer);
}
function abortBidTakerFixed(address offerer, address _stock, address _offer) internal {
vm.prank(offerer);
preMarktes.abortBidTakerFixed(_stock, _offer);
}
function verifyAccountTypeBalance(address _token, address account, TokenBalanceType accountType, uint256 expectedBalance) internal returns(uint256 balance) {
balance = getAccountTypeBalance(_token, account, accountType);
console2.log("AccountType", uint256(accountType));
assertEq(
balance,
expectedBalance,
"Account should have the correct amount for account type"
);
return balance;
}
function getAccountTypeBalance(address _token, address account, TokenBalanceType accountType) public returns(uint256 balance) {
balance = tokenManager.userTokenBalanceMap(
account,
_token,
accountType
);
return balance;
}
function calculateExpectedReferralBonuses(address offerAddr, uint256 offerPoints, uint256 offerAmount, uint256 purchasedPoints) internal view returns (uint256 expectedReferrerBonus, uint256 expectedTakerBonus) {
OfferInfo memory offerInfo = preMarktes.getOfferInfo(offerAddr);
uint256 depositAmount = purchasedPoints * offerAmount / offerPoints;
uint256 platformFeeRate = systemConfig.getPlatformFeeRate(taker1);
uint256 platformFee = depositAmount * platformFeeRate / Constants.PLATFORM_FEE_DECIMAL_SCALER;
ReferralInfo memory referralInfo = systemConfig.getReferralInfo(taker1);
expectedReferrerBonus = (platformFee * referralInfo.referrerRate) / Constants.REFERRAL_RATE_DECIMAL_SCALER;
expectedTakerBonus = (platformFee * referralInfo.authorityRate) / Constants.REFERRAL_RATE_DECIMAL_SCALER;
return (expectedReferrerBonus, expectedTakerBonus);
}
function calculateExpectedSalesRevenue(address offerAddr, uint256 purchasedPoints) internal view returns (uint256 expectedSalesRevenue) {
OfferInfo memory offerInfo = preMarktes.getOfferInfo(offerAddr);
expectedSalesRevenue = purchasedPoints * offerInfo.amount / offerInfo.points;
return expectedSalesRevenue;
}
function calculateExpectedTaxIncome(address offerAddr, bool isMaker, address _maker, uint256 depositAmount) internal view returns (uint256 expectedTaxIncome) {
uint256 tradeTax = isMaker? preMarktes.getMakerInfo(_maker).eachTradeTax : preMarktes.getOfferInfo(offerAddr).tradeTax;
assertGt(tradeTax, 0, "Trade tax should be greater than 0");
expectedTaxIncome = (depositAmount * tradeTax) / Constants.EACH_TRADE_TAX_DECIMAL_SCALER;
return expectedTaxIncome;
}
}

Impact

Likelihood: High, Impact: Medium, Severity: Medium

Note: Severity is medium due to the funds being at the discretion of the rescuable owner contract to be returned. Unlike in the non-native token case the funds are not locked in the contract.

Tools Used

  • Manual code review

  • Foundry testing framework

Recommendations

Change the TokenManager._transfer() function to use the tokenAddr instead of address(this) for the approval. An example of the correction can be seen below:

ICapitalPool(_capitalPoolAddr).approve(_token);
Updates

Lead Judging Commences

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

finding-TokenManager-approve-wrong-address-input

If we consider the correct permissioned implementation for the `approve()` function within `CapitalPool.sol`, this would be a critical severity issue, because the withdrawal of funds will be permanently blocked and must be rescued by the admin via the `Rescuable.sol` contract, given it will always revert [here](https://github.com/Cyfrin/2024-08-tadle/blob/04fd8634701697184a3f3a5558b41c109866e5f8/src/core/CapitalPool.sol#L36-L38) when attempting to call a non-existent function selector `approve` within the TokenManager contract. The argument up in the air is since the approval function `approve` was made permisionless, the `if` block within the internal `_transfer()` function will never be invoked if somebody beforehand calls approval for the TokenManager for the required token, so the transfer will infact not revert when a withdrawal is invoked. I will leave open for escalation discussions, but based on my first point, I believe high severity is appropriate.

Support

FAQs

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