Zaros charges long traders higher order fees than short traders irrespective of whether they are taker or maker orders.
Zaro is a perp market that allows traders to go long or short on an asset. Users create market orders and the keeper executes them. All orders are charged order fees and settlement fees. Order fees are based on trade bias i.e. if the order is taker or maker order. Takers are orders that push the market skew farther from zero, while maker pushes it closer to zero.
Like most exchanges, takers are charged higher fees. And being a taker/maker on Zaros is not about whether you are going long or short an asset, but the relationship between the market skew and the direction of your trade. Trades that push the skew further from zero are charged takerFees and trades that push the skew closer to zero are charged makerFees. Logically, the first trade in any market, or when the skew is zero, is charged takerFees.
The issue is that Zaros charges long traders higher fees compared to short traders irrespective of whether they are maker or taker orders.
The PoC below shows the cost of long and short transactions under the same market conditions.
Note that the mock price feed makes no distinction between the ask and bid price when encoding the verifiedPriceData, and the protocol does not adjust the prices when it is decoded, further proving that the imbalance fee charges are coming from the protocol's logic.
<details>
pragma solidity 0.8.25;
import { Base_Test } from "test/Base.t.sol";
import { Errors } from "@zaros/utils/Errors.sol";
import { ERC20, IERC20 } from "@openzeppelin/token/ERC20/ERC20.sol";
import {console2} from "forge-std/console2.sol";
import { MockERC20 } from "test/mocks/MockERC20.sol";
import { MockPriceFeed } from "test/mocks/MockPriceFeed.sol";
import { Users, User, MockPriceAdapters } from "test/utils/Types.sol";
import { UpgradeBranch } from "@zaros/tree-proxy/branches/UpgradeBranch.sol";
import { LookupBranch } from "@zaros/tree-proxy/branches/LookupBranch.sol";
import { GlobalConfigurationBranch } from "@zaros/perpetuals/branches/GlobalConfigurationBranch.sol";
import { LiquidationBranch } from "@zaros/perpetuals/branches/LiquidationBranch.sol";
import { OrderBranch } from "@zaros/perpetuals/branches/OrderBranch.sol";
import { PerpMarketBranch } from "@zaros/perpetuals/branches/PerpMarketBranch.sol";
import { TradingAccountBranch } from "@zaros/perpetuals/branches/TradingAccountBranch.sol";
import { SettlementBranch } from "@zaros/perpetuals/branches/SettlementBranch.sol";
import { MarginCollateralConfiguration } from "@zaros/perpetuals/leaves/MarginCollateralConfiguration.sol";
import { Position } from "@zaros/perpetuals/leaves/Position.sol";
import { MarketOrder } from "@zaros/perpetuals/leaves/MarketOrder.sol";
import { SettlementConfiguration } from "@zaros/perpetuals/leaves/SettlementConfiguration.sol";
import { TradingAccountBranchTestnet } from "testnet/branches/TradingAccountBranchTestnet.sol";
import { GlobalConfigurationHarness } from "test/harnesses/perpetuals/leaves/GlobalConfigurationHarness.sol";
import { MarginCollateralConfigurationHarness } from "test/harnesses/perpetuals/leaves/MarginCollateralConfigurationHarness.sol";
import { MarketConfigurationHarness } from "test/harnesses/perpetuals/leaves/MarketConfigurationHarness.sol";
import { MarketOrderHarness } from "test/harnesses/perpetuals/leaves/MarketOrderHarness.sol";
import { PerpMarketHarness } from "test/harnesses/perpetuals/leaves/PerpMarketHarness.sol";
import { PositionHarness } from "test/harnesses/perpetuals/leaves/PositionHarness.sol";
import { SettlementConfigurationHarness } from "test/harnesses/perpetuals/leaves/SettlementConfigurationHarness.sol";
import { TradingAccountHarness } from "test/harnesses/perpetuals/leaves/TradingAccountHarness.sol";
import {LiquidationKeeper} from "@zaros/external/chainlink/keepers/liquidation/LiquidationKeeper.sol";
import {MarketOrderKeeper} from "@zaros/external/chainlink/keepers/market-order/MarketOrderKeeper.sol";
import { UD60x18, ud60x18, ZERO as UD60x18_ZERO, convert as ud60x18Convert } from "@prb-math/UD60x18.sol";
import {SD59x18,sd59x18, unary, UNIT as SD_UNIT, ZERO as SD59x18_ZERO, convert as sd59x18Convert} from "@prb-math/SD59x18.sol";
contract BugPoC is Base_Test {
address[] internal USERS;
MockERC20[] internal EXTERNAL_TOKENS;
address internal BOB;
address internal ALICE;
address internal defaultUser;
address USDC;
address wBTC;
address weETH;
uint256 internal constant INITIAL_USDC_BALANCE = 1_000_000;
uint256 internal constant INITIAL_WEETH_BALANCE = 100_000;
uint256 internal constant INITIAL_WBTC_BALANCE = 10_000;
MockPriceFeed btcPriceFeed;
MockPriceFeed ethPriceFeed;
function setUp() public override {
Base_Test.setUp();
changePrank({ msgSender: users.owner.account });
configureSystemParameters();
createPerpMarkets();
EXTERNAL_TOKENS = new MockERC20[](3);
EXTERNAL_TOKENS[0] = usdc;
EXTERNAL_TOKENS[1] = weEth;
EXTERNAL_TOKENS[2] = wBtc;
USDC = address(usdc);
wBTC = address(wBtc);
weETH = address(weEth);
btcPriceFeed = MockPriceFeed(marketsConfig[1].priceAdapter);
ethPriceFeed = MockPriceFeed(marketsConfig[2].priceAdapter);
BOB = users.naruto.account;
ALICE = users.sasuke.account;
USERS = new address[](2);
USERS[0] = BOB;
USERS[1] = ALICE;
_topUpUsers();
vm.stopPrank();
}
function _topUpUsers() internal {
address user;
for (uint8 i = 0; i < USERS.length; i++) {
user = USERS[i];
EXTERNAL_TOKENS[0].mint(user, INITIAL_USDC_BALANCE * (10 ** EXTERNAL_TOKENS[0].decimals()));
EXTERNAL_TOKENS[1].mint(user, INITIAL_WEETH_BALANCE * (10 ** EXTERNAL_TOKENS[0].decimals()));
EXTERNAL_TOKENS[2].mint(user, INITIAL_WBTC_BALANCE * (10 ** EXTERNAL_TOKENS[0].decimals()));
}
}
modifier updateMockPriceFeedBtcEth(uint256 _btcNewPrice, uint256 _ethNewPrice) {
updateMockPriceFeed(1, _btcNewPrice);
updateMockPriceFeed(2, _ethNewPrice);
_;
}
function getPriceUint(MockPriceFeed priceFeed) internal view returns (uint256) {
(, int256 answer,,,) = priceFeed.latestRoundData();
return uint256(answer);
}
function performLiquidation(uint128 nextAccountId) internal {
LiquidationKeeper liquidationKeeper_ = LiquidationKeeper(liquidationKeeper);
uint256 checkLowerBound = 0;
uint256 checkUpperBound = uint256(nextAccountId);
uint256 performLowerBound = 0;
uint256 performUpperBound = checkUpperBound + 1;
if (checkUpperBound < 1) return;
bytes memory data = abi.encode(checkLowerBound, checkUpperBound, performLowerBound, performUpperBound);
(bool upkeepNeeded, bytes memory performData) = liquidationKeeper_.checkUpkeep(data);
if (upkeepNeeded) {
vm.startPrank(users.keepersForwarder.account);
liquidationKeeper_.performUpkeep(performData);
vm.stopPrank();
}
}
struct TestHelper {
uint256 oldBtcPrice;
uint256 oldEthPrice;
uint256 btcNewPrice;
uint256 ethNewPrice;
uint256 price;
bytes32 streamId;
uint256 priceBtcChange;
uint256 priceEthChange;
OrderBranch.CreateMarketOrderParams params;
}
function _performOrderExecution(uint256 marketId, uint128 tradingAccountId, bool isIncrease) internal {
TestHelper memory helper;
helper.oldBtcPrice = getPriceUint(btcPriceFeed);
helper.oldEthPrice = getPriceUint(ethPriceFeed);
helper.btcNewPrice;
helper.ethNewPrice;
helper.price;
helper.streamId = marketsConfig[marketId].streamId;
helper.priceBtcChange = helper.oldBtcPrice % 100e18;
helper.priceEthChange = helper.oldEthPrice % 10e18;
if(isIncrease) {
helper.btcNewPrice = helper.oldBtcPrice + helper.priceBtcChange;
helper.ethNewPrice = helper.oldEthPrice + helper.priceEthChange;
} else {
helper.btcNewPrice = helper.oldBtcPrice - helper.priceBtcChange;
helper.ethNewPrice = helper.oldEthPrice - helper.priceEthChange;
}
btcPriceFeed.updateMockPrice(helper.btcNewPrice);
ethPriceFeed.updateMockPrice(helper.ethNewPrice);
address marketOrderKeeper_ = marketOrderKeepers[marketId];
MarketOrderKeeper marketOrderKeeper = MarketOrderKeeper(marketOrderKeeper_);
if (marketId == 1) {
helper.price = getPriceUint(btcPriceFeed);
} else {
helper.price = getPriceUint(ethPriceFeed);
}
bytes memory mockSignedReport = getMockedSignedReport(helper.streamId, helper.price);
vm.startPrank(users.owner.account);
marketOrderKeeper.setForwarder(users.keepersForwarder.account);
vm.stopPrank();
bytes memory performData = abi.encode(mockSignedReport, abi.encode(tradingAccountId));
vm.startPrank(users.keepersForwarder.account);
marketOrderKeeper.performUpkeep(performData);
vm.stopPrank();
}
function tradingAccountBranch_createTradingAccount(
uint256 _btcNewPrice,
uint256 _ethNewPrice
) public updateMockPriceFeedBtcEth(_btcNewPrice, _ethNewPrice) returns(uint128 _tradingAccountId){
return perpsEngine.createTradingAccount(bytes(""), false) ;
}
function tradingAccountBranch_depositMargin(
uint128 _tradingAccountId,
address collateralType,
uint256 _amount,
uint256 _btcNewPrice,
uint256 _ethNewPrice
) public updateMockPriceFeedBtcEth(_btcNewPrice, _ethNewPrice) {
perpsEngine.depositMargin(_tradingAccountId, collateralType, _amount);
}
function tradingAccountBranch_withdrawMargin(
uint128 _tradingAccountId,
address collateralType,
uint256 _amount,
uint256 _btcNewPrice,
uint256 _ethNewPrice
) public updateMockPriceFeedBtcEth(_btcNewPrice, _ethNewPrice) {
perpsEngine.withdrawMargin(_tradingAccountId, collateralType, _amount);
}
function orderBranch_createMarketOrder(
uint128 _tradingAccountId,
bool _buyBtc,
int128 sizeDelta,
bool isIncrease,
uint256 _btcNewPrice,
uint256 _ethNewPrice
) public updateMockPriceFeedBtcEth(_btcNewPrice, _ethNewPrice) {
TestHelper memory helper;
helper.params.tradingAccountId = _tradingAccountId;
helper.params.marketId = _buyBtc ? 1 : 2;
helper.params.sizeDelta = sizeDelta;
perpsEngine.createMarketOrder(helper.params);
_performOrderExecution(helper.params.marketId, helper.params.tradingAccountId, isIncrease);
}
function testMarginBalanceAfterLongTrade() public {
vm.warp(1 hours);
vm.startPrank(BOB);
tradingAccountBranch_createTradingAccount(30_000e18, 2_000e18);
vm.startPrank(BOB);
tradingAccountBranch_depositMargin(1, USDC, 10000e6, 100_000e18, 10_000e18);
(SD59x18 before_marginBalanceUsdX18, UD60x18 before_initialMarginUsdX18, UD60x18 before_maintenanceMarginUsdX18,) = perpsEngine.getAccountMarginBreakdown(1);
vm.warp(1 hours);
vm.startPrank(BOB);
orderBranch_createMarketOrder(1, true, 1e18, true, 100_000e18, 10_000e18);
(SD59x18 after_marginBalanceUsdX18, UD60x18 after_initialMarginUsdX18, UD60x18 after_maintenanceMarginUsdX18,) = perpsEngine.getAccountMarginBreakdown(1);
console2.log("before_marginBalanceUsdX18: ", (before_marginBalanceUsdX18.intoUD60x18()).intoUint256());
console2.log("after_marginBalanceUsdX18: ", (after_marginBalanceUsdX18.intoUD60x18()).intoUint256());
console2.log("diff_marginBalanceUsdX18: ", ((before_marginBalanceUsdX18 - after_marginBalanceUsdX18).intoUD60x18()).intoUint256());
console2.log("before_initialMarginUsdX18: ", before_initialMarginUsdX18.intoUint256());
console2.log("after_initialMarginUsdX18: ", after_initialMarginUsdX18.intoUint256());
}
function testMarginBalanceAfterShortTrade() public {
vm.warp(1 hours);
vm.startPrank(BOB);
tradingAccountBranch_createTradingAccount(30_000e18, 2_000e18);
vm.startPrank(BOB);
tradingAccountBranch_depositMargin(1, USDC, 10000e6, 100_000e18, 10_000e18);
(SD59x18 before_marginBalanceUsdX18, UD60x18 before_initialMarginUsdX18, UD60x18 before_maintenanceMarginUsdX18,) = perpsEngine.getAccountMarginBreakdown(1);
vm.warp(1 hours);
vm.startPrank(BOB);
orderBranch_createMarketOrder(1, true, -1e18, true, 100_000e18, 10_000e18);
(SD59x18 after_marginBalanceUsdX18, UD60x18 after_initialMarginUsdX18, UD60x18 after_maintenanceMarginUsdX18,) = perpsEngine.getAccountMarginBreakdown(1);
console2.log("before_marginBalanceUsdX18: ", (before_marginBalanceUsdX18.intoUD60x18()).intoUint256());
console2.log("after_marginBalanceUsdX18: ", (after_marginBalanceUsdX18.intoUD60x18()).intoUint256());
console2.log("diff_marginBalanceUsdX18: ", ((before_marginBalanceUsdX18 - after_marginBalanceUsdX18).intoUD60x18()).intoUint256());
console2.log("before_initialMarginUsdX18: ", before_initialMarginUsdX18.intoUint256());
console2.log("after_initialMarginUsdX18: ", after_initialMarginUsdX18.intoUint256());
}
}
</details>
Review the fee calculation logic. The ratio of fee/reward for opposing but equally important actions such as long and short taker/maker orders should be thesame.