diff --git a/.github/workflows/protocol-benchmark.yml b/.github/workflows/protocol-benchmark.yml new file mode 100644 index 0000000000..abcc12e4a8 --- /dev/null +++ b/.github/workflows/protocol-benchmark.yml @@ -0,0 +1,46 @@ +name: Protocol Benchmark +on: # yamllint disable-line rule:truthy + pull_request: + paths: + - 'protocol/**' + push: + branches: + - main + - 'release/protocol/v[0-9]+.[0-9]+.x' # e.g. release/protocol/v0.1.x + - 'release/protocol/v[0-9]+.x' # e.g. release/protocol/v1.x + paths: + - 'protocol/**' + +concurrency: + group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} + cancel-in-progress: true + +jobs: + benchmark: + runs-on: ubuntu-latest + defaults: + run: + working-directory: ./protocol + steps: + - name: Check out repository code + uses: actions/checkout@v3 + - name: Setup Golang + uses: actions/setup-go@v3 + with: + go-version: 1.22 + - name: Run Benchmarks + run: make benchmark | tee ./benchmark_output.txt + - name: Download previous benchmark data + uses: actions/cache@v4 + with: + path: ./cache + key: ${{ runner.os }}-benchmark + - name: Store benchmark result + uses: benchmark-action/github-action-benchmark@v1 + with: + tool: 'go' + output-file-path: ./protocol/benchmark_output.txt + external-data-json-path: ./cache/benchmark-data.json + fail-on-alert: true + alert-threshold: '150%' + save-data-file: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }} diff --git a/protocol/x/perpetuals/keeper/perpetual_test.go b/protocol/x/perpetuals/keeper/perpetual_test.go index 12df0776de..f24929021a 100644 --- a/protocol/x/perpetuals/keeper/perpetual_test.go +++ b/protocol/x/perpetuals/keeper/perpetual_test.go @@ -677,107 +677,107 @@ func TestModifyOpenInterest_Mixed(t *testing.T) { func TestGetMarginRequirements_Success(t *testing.T) { oneBip := math.Pow10(2) tests := map[string]struct { - price uint64 - exponent int32 - baseCurrencyAtomicResolution int32 - bigBaseQuantums *big.Int - initialMarginPpm uint32 - maintenanceFractionPpm uint32 - openInterest *big.Int - openInterestLowerCap uint64 - openInterestUpperCap uint64 - bigExpectedInitialMarginPpm *big.Int - bigExpectedMaintenanceMarginPpm *big.Int + price uint64 + exponent int32 + baseCurrencyAtomicResolution int32 + bigBaseQuantums *big.Int + initialMarginPpm uint32 + maintenanceFractionPpm uint32 + openInterest *big.Int + openInterestLowerCap uint64 + openInterestUpperCap uint64 + bigExpectedInitialMargin *big.Int + bigExpectedMaintenanceMargin *big.Int }{ "InitialMargin 2 BIPs, MaintenanceMargin 1 BIP, positive exponent, atomic resolution 8": { - price: 5_555, - exponent: 2, - baseCurrencyAtomicResolution: -8, - bigBaseQuantums: big.NewInt(7_000), - initialMarginPpm: uint32(oneBip * 2), - maintenanceFractionPpm: uint32(500_000), // 50% of IM - bigExpectedInitialMarginPpm: big.NewInt(7_777), - bigExpectedMaintenanceMarginPpm: big.NewInt(3_889), + price: 5_555, + exponent: 2, + baseCurrencyAtomicResolution: -8, + bigBaseQuantums: big.NewInt(7_000), + initialMarginPpm: uint32(oneBip * 2), + maintenanceFractionPpm: uint32(500_000), // 50% of IM + bigExpectedInitialMargin: big.NewInt(7_777), + bigExpectedMaintenanceMargin: big.NewInt(3_889), }, "InitialMargin 100 BIPs, MaintenanceMargin 50 BIPs, atomic resolution 4": { - price: 5_555, - exponent: 0, - baseCurrencyAtomicResolution: -4, - bigBaseQuantums: big.NewInt(7_000), - initialMarginPpm: uint32(oneBip * 100), - maintenanceFractionPpm: uint32(500_000), // 50% of IM - bigExpectedInitialMarginPpm: big.NewInt(38_885_000), - bigExpectedMaintenanceMarginPpm: big.NewInt(19_442_500), + price: 5_555, + exponent: 0, + baseCurrencyAtomicResolution: -4, + bigBaseQuantums: big.NewInt(7_000), + initialMarginPpm: uint32(oneBip * 100), + maintenanceFractionPpm: uint32(500_000), // 50% of IM + bigExpectedInitialMargin: big.NewInt(38_885_000), + bigExpectedMaintenanceMargin: big.NewInt(19_442_500), }, "InitialMargin 100 BIPs, MaintenanceMargin 50 BIPs, positive exponent, atomic resolution 0": { - price: 42, - exponent: 5, - baseCurrencyAtomicResolution: -0, - bigBaseQuantums: big.NewInt(88), - initialMarginPpm: uint32(oneBip * 100), - maintenanceFractionPpm: uint32(500_000), // 50% of IM - bigExpectedInitialMarginPpm: big.NewInt(3_696_000_000_000), - bigExpectedMaintenanceMarginPpm: big.NewInt(1_848_000_000_000), + price: 42, + exponent: 5, + baseCurrencyAtomicResolution: -0, + bigBaseQuantums: big.NewInt(88), + initialMarginPpm: uint32(oneBip * 100), + maintenanceFractionPpm: uint32(500_000), // 50% of IM + bigExpectedInitialMargin: big.NewInt(3_696_000_000_000), + bigExpectedMaintenanceMargin: big.NewInt(1_848_000_000_000), }, "InitialMargin 100 BIPs, MaintenanceMargin 50 BIPs, negative exponent, atomic resolution 6": { - price: 42_000_000, - exponent: -2, - baseCurrencyAtomicResolution: -6, - bigBaseQuantums: big.NewInt(-5_000), - initialMarginPpm: uint32(oneBip * 100), - maintenanceFractionPpm: uint32(500_000), // 50% of IM - bigExpectedInitialMarginPpm: big.NewInt(21_000_000), - bigExpectedMaintenanceMarginPpm: big.NewInt(10_500_000), + price: 42_000_000, + exponent: -2, + baseCurrencyAtomicResolution: -6, + bigBaseQuantums: big.NewInt(-5_000), + initialMarginPpm: uint32(oneBip * 100), + maintenanceFractionPpm: uint32(500_000), // 50% of IM + bigExpectedInitialMargin: big.NewInt(21_000_000), + bigExpectedMaintenanceMargin: big.NewInt(10_500_000), }, "InitialMargin 10_000 BIPs (max), MaintenanceMargin 10_000 BIPs (max), atomic resolution 6": { - price: 5_555, - exponent: 0, - baseCurrencyAtomicResolution: -6, - bigBaseQuantums: big.NewInt(7_000), - initialMarginPpm: uint32(oneBip * 10_000), - maintenanceFractionPpm: uint32(1_000_000), // 100% of IM - bigExpectedInitialMarginPpm: big.NewInt(38_885_000), - bigExpectedMaintenanceMarginPpm: big.NewInt(38_885_000), + price: 5_555, + exponent: 0, + baseCurrencyAtomicResolution: -6, + bigBaseQuantums: big.NewInt(7_000), + initialMarginPpm: uint32(oneBip * 10_000), + maintenanceFractionPpm: uint32(1_000_000), // 100% of IM + bigExpectedInitialMargin: big.NewInt(38_885_000), + bigExpectedMaintenanceMargin: big.NewInt(38_885_000), }, "InitialMargin 100 BIPs, MaintenanceMargin 100 BIPs, atomic resolution 6": { - price: 5_555, - exponent: 0, - baseCurrencyAtomicResolution: -6, - bigBaseQuantums: big.NewInt(7_000), - initialMarginPpm: uint32(oneBip * 100), - maintenanceFractionPpm: uint32(1_000_000), // 100% of IM - bigExpectedInitialMarginPpm: big.NewInt(388_850), - bigExpectedMaintenanceMarginPpm: big.NewInt(388_850), + price: 5_555, + exponent: 0, + baseCurrencyAtomicResolution: -6, + bigBaseQuantums: big.NewInt(7_000), + initialMarginPpm: uint32(oneBip * 100), + maintenanceFractionPpm: uint32(1_000_000), // 100% of IM + bigExpectedInitialMargin: big.NewInt(388_850), + bigExpectedMaintenanceMargin: big.NewInt(388_850), }, "InitialMargin 0.02 BIPs, MaintenanceMargin 0.01 BIPs, positive exponent, atomic resolution 6": { - price: 5_555, - exponent: 3, - baseCurrencyAtomicResolution: -6, - bigBaseQuantums: big.NewInt(-7_000), - initialMarginPpm: uint32(oneBip * 0.02), - maintenanceFractionPpm: uint32(500_000), // 50% of IM - bigExpectedInitialMarginPpm: big.NewInt(77_770), - bigExpectedMaintenanceMarginPpm: big.NewInt(38_885), + price: 5_555, + exponent: 3, + baseCurrencyAtomicResolution: -6, + bigBaseQuantums: big.NewInt(-7_000), + initialMarginPpm: uint32(oneBip * 0.02), + maintenanceFractionPpm: uint32(500_000), // 50% of IM + bigExpectedInitialMargin: big.NewInt(77_770), + bigExpectedMaintenanceMargin: big.NewInt(38_885), }, "InitialMargin 0 BIPs (min), MaintenanceMargin 0 BIPs (min), atomic resolution 6": { - price: 5_555, - exponent: 0, - baseCurrencyAtomicResolution: -6, - bigBaseQuantums: big.NewInt(7_000), - initialMarginPpm: uint32(oneBip * 0), - maintenanceFractionPpm: uint32(1_000_000), // 100% of IM, - bigExpectedInitialMarginPpm: big.NewInt(0), - bigExpectedMaintenanceMarginPpm: big.NewInt(0), + price: 5_555, + exponent: 0, + baseCurrencyAtomicResolution: -6, + bigBaseQuantums: big.NewInt(7_000), + initialMarginPpm: uint32(oneBip * 0), + maintenanceFractionPpm: uint32(1_000_000), // 100% of IM, + bigExpectedInitialMargin: big.NewInt(0), + bigExpectedMaintenanceMargin: big.NewInt(0), }, "Price is zero, atomic resolution 6": { - price: 0, - exponent: 1, - baseCurrencyAtomicResolution: -6, - bigBaseQuantums: big.NewInt(-7_000), - initialMarginPpm: uint32(oneBip * 1), - maintenanceFractionPpm: uint32(1_000_000), // 100% of IM, - bigExpectedInitialMarginPpm: big.NewInt(0), - bigExpectedMaintenanceMarginPpm: big.NewInt(0), + price: 0, + exponent: 1, + baseCurrencyAtomicResolution: -6, + bigBaseQuantums: big.NewInt(-7_000), + initialMarginPpm: uint32(oneBip * 1), + maintenanceFractionPpm: uint32(1_000_000), // 100% of IM, + bigExpectedInitialMargin: big.NewInt(0), + bigExpectedMaintenanceMargin: big.NewInt(0), }, "Price and quantums are max uints": { price: math.MaxUint64, @@ -786,10 +786,10 @@ func TestGetMarginRequirements_Success(t *testing.T) { bigBaseQuantums: new(big.Int).SetUint64(math.MaxUint64), initialMarginPpm: uint32(oneBip * 1), maintenanceFractionPpm: uint32(1_000_000), // 100% of IM, - bigExpectedInitialMarginPpm: big_testutil.MustFirst( + bigExpectedInitialMargin: big_testutil.MustFirst( new(big.Int).SetString("340282366920938463426481119284349109", 10), ), - bigExpectedMaintenanceMarginPpm: big_testutil.MustFirst( + bigExpectedMaintenanceMargin: big_testutil.MustFirst( new(big.Int).SetString("340282366920938463426481119284349109", 10), ), }, @@ -802,8 +802,8 @@ func TestGetMarginRequirements_Success(t *testing.T) { maintenanceFractionPpm: uint32(500_000), // 50% of IM // initialMarginPpmQuoteQuantums = initialMarginPpm * quoteQuantums / 1_000_000 // = 10_000 * 38_885_000 / 1_000_000 ~= 388_850. - bigExpectedInitialMarginPpm: big.NewInt(388_850), - bigExpectedMaintenanceMarginPpm: big.NewInt(388_850 / 2), + bigExpectedInitialMargin: big.NewInt(388_850), + bigExpectedMaintenanceMargin: big.NewInt(388_850 / 2), }, "InitialMargin 20%, MaintenanceMargin 10%, atomic resolution 6": { price: 36_750, @@ -815,8 +815,8 @@ func TestGetMarginRequirements_Success(t *testing.T) { // quoteQuantums = 36_750 * 12_000 = 441_000_000 // initialMarginPpmQuoteQuantums = initialMarginPpm * quoteQuantums / 1_000_000 // = 200_000 * 441_000_000 / 1_000_000 ~= 88_200_000 - bigExpectedInitialMarginPpm: big.NewInt(88_200_000), - bigExpectedMaintenanceMarginPpm: big.NewInt(88_200_000 / 2), + bigExpectedInitialMargin: big.NewInt(88_200_000), + bigExpectedMaintenanceMargin: big.NewInt(88_200_000 / 2), }, "InitialMargin 5%, MaintenanceMargin 3%, atomic resolution 6": { price: 123_456, @@ -828,8 +828,8 @@ func TestGetMarginRequirements_Success(t *testing.T) { // quoteQuantums = 123_456 * 74_523 = 9_200_311_488 // initialMarginPpmQuoteQuantums = initialMarginPpm * quoteQuantums / 1_000_000 // = 50_000 * 9_200_311_488 / 1_000_000 ~= 460_015_575 - bigExpectedInitialMarginPpm: big.NewInt(460_015_575), - bigExpectedMaintenanceMarginPpm: big.NewInt(276_009_345), + bigExpectedInitialMargin: big.NewInt(460_015_575), + bigExpectedMaintenanceMargin: big.NewInt(276_009_345), }, "InitialMargin 25%, MaintenanceMargin 15%, atomic resolution 6": { price: 123_456, @@ -839,8 +839,8 @@ func TestGetMarginRequirements_Success(t *testing.T) { initialMarginPpm: uint32(250_000), maintenanceFractionPpm: uint32(600_000), // 60% of IM // quoteQuantums = 123_456 * 74_523 = 9_200_311_488 - bigExpectedInitialMarginPpm: big.NewInt(2_300_077_872), - bigExpectedMaintenanceMarginPpm: big.NewInt(1_380_046_724), // Rounded up + bigExpectedInitialMargin: big.NewInt(2_300_077_872), + bigExpectedMaintenanceMargin: big.NewInt(1_380_046_724), // Rounded up }, "OIMF: IM 20%, scaled to 60%, MaintenanceMargin 10%, atomic resolution 6": { price: 36_750, @@ -852,11 +852,12 @@ func TestGetMarginRequirements_Success(t *testing.T) { openInterest: big.NewInt(408_163_265), // 408.163265 openInterestLowerCap: 10_000_000_000_000, openInterestUpperCap: 20_000_000_000_000, - // quoteQuantums = 36_750 * 12_000 = 441_000_000 - // initialMarginPpmQuoteQuantums = initialMarginPpm * quoteQuantums / 1_000_000 - // = 200_000 * 441_000_000 / 1_000_000 ~= 88_200_000 - bigExpectedInitialMarginPpm: big.NewInt(88_200_000 * 3), - bigExpectedMaintenanceMarginPpm: big.NewInt(88_200_000 / 2), + // openInterestNotional = 408_163_265 * 36_750 = 14_999_999_988_750 + // percentageOfCap = (openInterestNotional - lowerCap) / (upperCap - lowerCap) = 0.499999998875 + // adjustedIMF = (0.499999998875) * 0.8 + 0.2 = 0.5999999991 (rounded is 599_999 ppm) + // bigExpectedInitialMargin = bigBaseQuantums * price * adjustedIMF = 264_599_559 + bigExpectedInitialMargin: big.NewInt(264_599_559), + bigExpectedMaintenanceMargin: big.NewInt(88_200_000 / 2), }, "OIMF: IM 20%, scaled to 100%, MaintenanceMargin 10%, atomic resolution 6": { price: 36_750, @@ -871,8 +872,8 @@ func TestGetMarginRequirements_Success(t *testing.T) { // quoteQuantums = 36_750 * 12_000 = 441_000_000 // initialMarginPpmQuoteQuantums = initialMarginPpm * quoteQuantums / 1_000_000 // = 200_000 * 441_000_000 / 1_000_000 ~= 88_200_000 - bigExpectedInitialMarginPpm: big.NewInt(441_000_000), - bigExpectedMaintenanceMarginPpm: big.NewInt(88_200_000 / 2), + bigExpectedInitialMargin: big.NewInt(441_000_000), + bigExpectedMaintenanceMargin: big.NewInt(88_200_000 / 2), }, "OIMF: IM 20%, lower_cap < realistic open interest < upper_cap, MaintenanceMargin 10%, atomic resolution 6": { price: 36_750, @@ -884,12 +885,12 @@ func TestGetMarginRequirements_Success(t *testing.T) { openInterest: big.NewInt(1_123_456_789), // 1123.456 or ~$41mm notional openInterestLowerCap: 25_000_000_000_000, openInterestUpperCap: 50_000_000_000_000, - // quoteQuantums = 36_750 * 12_000 = 441_000_000 - // initialMarginPpmQuoteQuantums = initialMarginPpm * quoteQuantums / 1_000_000 - // = ((1123.456789 * 36750 - 25000000) / 25000000 * 0.8 + 0.2) * 441_000_000 - // ~= 318042667 - bigExpectedInitialMarginPpm: big.NewInt(318_042_667), - bigExpectedMaintenanceMarginPpm: big.NewInt(88_200_000 / 2), + // openInterestNotional = 1_123_456_789 * 36_750 = 41_287_036_995_750 + // percentageOfCap = (openInterestNotional - lowerCap) / (upperCap - lowerCap) = 0.65148147983 + // adjustedIMF = (0.65148147983) * 0.8 + 0.2 = 0.721185183864 (rounded is 721_185 ppm) + // bigExpectedInitialMargin = bigBaseQuantums * price * adjustedIMF = 318_042_585 + bigExpectedInitialMargin: big.NewInt(318_042_585), + bigExpectedMaintenanceMargin: big.NewInt(88_200_000 / 2), }, } @@ -972,21 +973,8 @@ func TestGetMarginRequirements_Success(t *testing.T) { ) require.NoError(t, err) - if tc.bigExpectedInitialMarginPpm.Cmp(bigInitialMargin) != 0 { - t.Fatalf( - "%s: expectedInitialMargin: %s, initialMargin: %s", - name, - tc.bigExpectedInitialMarginPpm.String(), - bigInitialMargin.String()) - } - - if tc.bigExpectedMaintenanceMarginPpm.Cmp(bigMaintenanceMargin) != 0 { - t.Fatalf( - "%s: expectedMaintenanceMargin: %s, maintenanceMargin: %s", - name, - tc.bigExpectedMaintenanceMarginPpm.String(), - bigMaintenanceMargin.String()) - } + require.Equal(t, tc.bigExpectedInitialMargin, bigInitialMargin, "Initial margin mismatch") + require.Equal(t, tc.bigExpectedMaintenanceMargin, bigMaintenanceMargin, "Maintenance margin mismatch") }) } } @@ -1173,14 +1161,7 @@ func TestGetNetNotional_Success(t *testing.T) { ) require.NoError(t, err) - if tc.bigExpectedNetNotionalQuoteQuantums.Cmp(bigNotionalQuoteQuantums) != 0 { - t.Fatalf( - "%s: expectedNetNotionalQuoteQuantums: %s, collateralQuoteQuantums: %s", - name, - tc.bigExpectedNetNotionalQuoteQuantums.String(), - bigNotionalQuoteQuantums.String(), - ) - } + require.Equal(t, tc.bigExpectedNetNotionalQuoteQuantums, bigNotionalQuoteQuantums, "Net notional mismatch") }) } } @@ -1335,14 +1316,7 @@ func TestGetNotionalInBaseQuantums_Success(t *testing.T) { ) require.NoError(t, err) - if tc.bigExpectedNetNotionalBaseQuantums.Cmp(bigNotionalBaseQuantums) != 0 { - t.Fatalf( - "%s: expectedNetNotionalBaseQuantums: %s, collateralBaseQuantums: %s", - name, - tc.bigExpectedNetNotionalBaseQuantums.String(), - bigNotionalBaseQuantums.String(), - ) - } + require.Equal(t, tc.bigExpectedNetNotionalBaseQuantums, bigNotionalBaseQuantums, "Net notional mismatch") }) } } @@ -1498,14 +1472,7 @@ func TestGetNetCollateral_Success(t *testing.T) { ) require.NoError(t, err) - if tc.bigExpectedNetCollateralQuoteQuantums.Cmp(bigCollateralQuoteQuantums) != 0 { - t.Fatalf( - "%s: expectedNetCollateralQuoteQuantums: %s, collateralQuoteQuantums: %s", - name, - tc.bigExpectedNetCollateralQuoteQuantums.String(), - bigCollateralQuoteQuantums.String(), - ) - } + require.Equal(t, tc.bigExpectedNetCollateralQuoteQuantums, bigCollateralQuoteQuantums, "Net collateral mismatch") }) } } diff --git a/protocol/x/perpetuals/types/liquidity_tier.go b/protocol/x/perpetuals/types/liquidity_tier.go index 7f4714d450..543cf494f2 100644 --- a/protocol/x/perpetuals/types/liquidity_tier.go +++ b/protocol/x/perpetuals/types/liquidity_tier.go @@ -73,9 +73,20 @@ func (liquidityTier LiquidityTier) GetMaxAbsFundingClampPpm(clampFactorPpm uint3 ) } -// GetInitialMarginQuoteQuantums returns initial margin requirement (IMR) in quote quantums. -// -// Now that OIMF is introduced, the calculation of IMR is as follows: +// GetInitialMarginQuoteQuantums returns the initial margin requirement (IMR) in quote quantums. +func (liquidityTier LiquidityTier) GetInitialMarginQuoteQuantums( + quoteQuantums *big.Int, + oiQuoteQuantums *big.Int, +) *big.Int { + totalImfPpm := liquidityTier.GetAdjustedInitialMarginPpm(oiQuoteQuantums) + return lib.BigMulPpm( + quoteQuantums, + totalImfPpm, + true, // Round up initial margin. + ) +} + +// GetAdjustedInitialMarginPpm returns the adjusted initial margin (in ppm) based on the current open interest. // // - Each market has a `Lower Cap` and `Upper Cap` denominated in USDC. // - Each market already has a `Base IMF`. @@ -88,93 +99,43 @@ func (liquidityTier LiquidityTier) GetMaxAbsFundingClampPpm(clampFactorPpm uint3 // // - I.e. the effective IMF is the base IMF while the OI < lower cap, and increases linearly until OI = Upper Cap, // at which point the IMF stays at 1.0 (requiring 1:1 collateral for trading). -// - initialMarginQuoteQuantums = scaledInitialMarginPpm * quoteQuantums / 1_000_000 -// -// note: -// - divisions are delayed for precision purposes. -func (liquidityTier LiquidityTier) GetInitialMarginQuoteQuantums( - bigQuoteQuantums *big.Int, - openInterestQuoteQuantums *big.Int, +func (liquidityTier LiquidityTier) GetAdjustedInitialMarginPpm( + oiQuoteQuantums *big.Int, ) *big.Int { - bigOpenInterestUpperCap := new(big.Int).SetUint64(liquidityTier.OpenInterestUpperCap) - - // If `open_interest` >= `open_interest_upper_cap` where `upper_cap` is non-zero, - // OIMF = 1.0 so return input quote quantums as the IMR. - if openInterestQuoteQuantums.Cmp( - bigOpenInterestUpperCap, - ) >= 0 && liquidityTier.OpenInterestUpperCap != 0 { - return bigQuoteQuantums - } + oiCapUpper := lib.BigU(liquidityTier.OpenInterestUpperCap) + oiCapLower := lib.BigU(liquidityTier.OpenInterestLowerCap) - ratQuoteQuantums := new(big.Rat).SetInt(bigQuoteQuantums) - - // If `open_interest_upper_cap` is 0, OIMF is disabled。 + // If `open_interest_upper_cap` is 0, OIMF is disabled. // Or if `current_interest` <= `open_interest_lower_cap`, IMF is not scaled. - // In both cases, use base IMF as OIMF. - bigOpenInterestLowerCap := new(big.Int).SetUint64(liquidityTier.OpenInterestLowerCap) - if liquidityTier.OpenInterestUpperCap == 0 || openInterestQuoteQuantums.Cmp( - bigOpenInterestLowerCap, - ) <= 0 { - // Calculate base IMR: multiply `bigQuoteQuantums` with `initialMarginPpm` and divide by 1 million. - ratBaseIMR := lib.BigRatMulPpm(ratQuoteQuantums, liquidityTier.InitialMarginPpm) - return lib.BigRatRound(ratBaseIMR, true) // Round up initial margin. + if oiCapUpper.Sign() == 0 || oiQuoteQuantums.Cmp(oiCapLower) <= 0 { + return lib.BigU(liquidityTier.InitialMarginPpm) } - // If `open_interest_lower_cap` < `open_interest` <= `open_interest_upper_cap`, calculate the scaled OIMF. - // `Scaling Factor = (Open Notional - Lower Cap) / (Upper Cap - Lower Cap)` - ratScalingFactor := new(big.Rat).SetFrac( - new(big.Int).Sub( - openInterestQuoteQuantums, // reuse pointer for memory efficiency - bigOpenInterestLowerCap, - ), - bigOpenInterestUpperCap.Sub( - bigOpenInterestUpperCap, // reuse pointer for memory efficiency - bigOpenInterestLowerCap, - ), - ) - - // `IMF Increase = Scaling Factor * (1 - Base IMF)` - ratIMFIncrease := lib.BigRatMulPpm( - ratScalingFactor, - lib.OneMillion-liquidityTier.InitialMarginPpm, // >= 0, since we check in `liquidityTier.Validate()` - ) - - // Calculate `Max(IMF Increase, 0)`. - if ratIMFIncrease.Sign() < 0 { - panic( - fmt.Sprintf( - "GetInitialMarginQuoteQuantums: IMF Increase is negative (%s), liquidityTier: %+v, openInterestQuoteQuantums: %s", - ratIMFIncrease.String(), - liquidityTier, - openInterestQuoteQuantums.String(), - ), - ) + // If `open_interest` >= `open_interest_upper_cap` where `upper_cap` is non-zero, OIMF is 1. + if oiCapUpper.Sign() > 0 && oiQuoteQuantums.Cmp(oiCapUpper) >= 0 { + return lib.BigU(lib.OneMillion) } - // First, calculate base IMF in big.Rat - ratBaseIMF := new(big.Rat).SetFrac64( - int64(liquidityTier.InitialMarginPpm), // safe, since `InitialMargin` is uint32 - int64(lib.OneMillion), - ) - - // `Effective IMF = Min(Base IMF + Max(IMF Increase, 0), 1.0)` - ratEffectiveIMF := ratBaseIMF.Add( - ratBaseIMF, // reuse pointer for memory efficiency - ratIMFIncrease, - ) - - // `Effective IMR = Effective IMF * Quote Quantums` - bigIMREffective := lib.BigRatRound( - ratEffectiveIMF.Mul( - ratEffectiveIMF, // reuse pointer for memory efficiency - ratQuoteQuantums, - ), - true, // Round up initial margin. - ) + // Get the ratio of where the current OI is between the lower and upper caps. + // The ratio should be between 0 and 1. + capNum := new(big.Int).Sub(oiQuoteQuantums, oiCapLower) + capDen := new(big.Int).Sub(oiCapUpper, oiCapLower) - // Return min(Effective IMR, Quote Quantums) - if bigIMREffective.Cmp(bigQuoteQuantums) >= 0 { - return bigQuoteQuantums + // Invariant checks. + if capNum.Sign() < 0 || capDen.Sign() <= 0 || capDen.Cmp(capNum) < 0 { + panic(fmt.Sprintf("invalid open interest values for liquidity tier %d", liquidityTier.Id)) } - return bigIMREffective + if oiCapLower.Cmp(oiCapUpper) > 0 { + panic(errorsmod.Wrap(ErrOpenInterestLowerCapLargerThanUpperCap, lib.UintToString(liquidityTier.Id))) + } + if liquidityTier.InitialMarginPpm > lib.OneMillion { + panic(errorsmod.Wrap(ErrInitialMarginPpmExceedsMax, lib.UintToString(liquidityTier.Id))) + } + + // Total IMF. + capScalePpm := lib.BigU(lib.OneMillion - liquidityTier.InitialMarginPpm) + result := new(big.Int).Mul(capScalePpm, capNum) + result.Div(result, capDen) + result.Add(result, lib.BigU(liquidityTier.InitialMarginPpm)) + return result } diff --git a/protocol/x/perpetuals/types/liquidity_tier_test.go b/protocol/x/perpetuals/types/liquidity_tier_test.go index 7afb3b9d87..ccc196b67f 100644 --- a/protocol/x/perpetuals/types/liquidity_tier_test.go +++ b/protocol/x/perpetuals/types/liquidity_tier_test.go @@ -219,6 +219,28 @@ func TestLiquidityTierGetMaxAbsFundingClampPpm(t *testing.T) { } } +func BenchmarkGetInitialMarginQuoteQuantums(b *testing.B) { + openInterestLowerCap := uint64(1_000_000) + openInterestUpperCap := uint64(2_000_000) + openInterestMiddle := (openInterestLowerCap + openInterestUpperCap) / 2 + liquidityTier := &types.LiquidityTier{ + OpenInterestLowerCap: openInterestLowerCap, + OpenInterestUpperCap: openInterestUpperCap, + InitialMarginPpm: 200_000, // 20% + } + bigQuoteQuantums := big.NewInt(500_000) + oiLower := new(big.Int).SetUint64(openInterestLowerCap) + oiUpper := new(big.Int).SetUint64(openInterestUpperCap) + oiMiddle := new(big.Int).SetUint64(openInterestMiddle) + + b.ResetTimer() + for i := 0; i < b.N; i++ { + _ = liquidityTier.GetInitialMarginQuoteQuantums(bigQuoteQuantums, oiLower) + _ = liquidityTier.GetInitialMarginQuoteQuantums(bigQuoteQuantums, oiUpper) + _ = liquidityTier.GetInitialMarginQuoteQuantums(bigQuoteQuantums, oiMiddle) + } +} + func TestGetInitialMarginQuoteQuantums(t *testing.T) { tests := map[string]struct { initialMarginPpm uint32 @@ -341,3 +363,77 @@ func TestGetInitialMarginQuoteQuantums(t *testing.T) { }) } } + +func TestGetAdjustedInitialMarginPpm(t *testing.T) { + tests := map[string]struct { + initialMarginPpm uint32 + openInterestLowerCap uint64 + openInterestUpperCap uint64 + openInterestNotional *big.Int + expectedPpm *big.Int + }{ + "Zero open interest": { + initialMarginPpm: uint32(200_000), + openInterestLowerCap: 1_000_000, + openInterestUpperCap: 2_000_000, + openInterestNotional: big.NewInt(0), + expectedPpm: big.NewInt(200_000), + }, + "Open interest within bounds": { + initialMarginPpm: uint32(200_000), + openInterestLowerCap: 1_000_000, + openInterestUpperCap: 2_000_000, + openInterestNotional: big.NewInt(1_500_000), + expectedPpm: big.NewInt(600_000), + }, + "Open interest within bounds, rounded": { + initialMarginPpm: uint32(200_000), + openInterestLowerCap: 1_000_000, + openInterestUpperCap: 2_000_000, + openInterestNotional: big.NewInt(1_234_567), + // Base_IMF + OI_IMF_Adjustment = + // 0.2 + (0.234_567 * 0.8) = + // 0.387_653_6 (rounded down to 0.387_653) + expectedPpm: big.NewInt(387_653), + }, + "Open interest at lower bound": { + initialMarginPpm: uint32(200_000), + openInterestLowerCap: 1_000_000, + openInterestUpperCap: 2_000_000, + openInterestNotional: big.NewInt(1_000_000), + expectedPpm: big.NewInt(200_000), + }, + "Open interest at upper bound": { + initialMarginPpm: uint32(200_000), + openInterestLowerCap: 1_000_000, + openInterestUpperCap: 2_000_000, + openInterestNotional: big.NewInt(2_000_000), + expectedPpm: big.NewInt(1_000_000), + }, + "Open interest above upper bound": { + initialMarginPpm: uint32(200_000), + openInterestLowerCap: 1_000_000, + openInterestUpperCap: 2_000_000, + openInterestNotional: big.NewInt(2_500_000), + expectedPpm: big.NewInt(1_000_000), + }, + "No upper bound, no increase": { + initialMarginPpm: uint32(200_000), + openInterestLowerCap: 0, + openInterestUpperCap: 0, + openInterestNotional: big.NewInt(1_500_000), + expectedPpm: big.NewInt(200_000), + }, + } + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + liquidityTier := &types.LiquidityTier{ + InitialMarginPpm: tc.initialMarginPpm, + OpenInterestLowerCap: tc.openInterestLowerCap, + OpenInterestUpperCap: tc.openInterestUpperCap, + } + adjustedIMQuoteQuantums := liquidityTier.GetAdjustedInitialMarginPpm(tc.openInterestNotional) + require.Equal(t, tc.expectedPpm, adjustedIMQuoteQuantums, "Adjusted initial margin ppm mismatch") + }) + } +} diff --git a/protocol/x/subaccounts/keeper/subaccount_test.go b/protocol/x/subaccounts/keeper/subaccount_test.go index 264e624c43..747ff8cfb9 100644 --- a/protocol/x/subaccounts/keeper/subaccount_test.go +++ b/protocol/x/subaccounts/keeper/subaccount_test.go @@ -2402,7 +2402,7 @@ func TestUpdateSubaccounts(t *testing.T) { }, msgSenderEnabled: true, }, - `Isolated subaccounts - empty subaccount has update to open position for isolated perpetual, + `Isolated subaccounts - empty subaccount has update to open position for isolated perpetual, errors out when collateral pool for cross perpetuals has no funds`: { assetPositions: testutil.CreateUsdcAssetPosition(big.NewInt(1_000_000_000_000)), expectedSuccess: false, @@ -2433,7 +2433,7 @@ func TestUpdateSubaccounts(t *testing.T) { expectedErr: sdkerrors.ErrInsufficientFunds, msgSenderEnabled: true, }, - `Isolated subaccounts - isolated subaccount has update to close position for isolated perpetual, + `Isolated subaccounts - isolated subaccount has update to close position for isolated perpetual, errors out when collateral pool for isolated perpetual has no funds`: { assetPositions: testutil.CreateUsdcAssetPosition(big.NewInt(1_000_000_000_000)), expectedSuccess: false, @@ -3250,7 +3250,7 @@ func TestUpdateSubaccounts_WithdrawalsBlocked(t *testing.T) { updateType: types.Withdrawal, }, - `withdrawals are not blocked if negative TNC subaccount was seen within + `withdrawals are not blocked if negative TNC subaccount was seen within WITHDRAWAL_AND_TRANSFERS_BLOCKED_AFTER_NEGATIVE_TNC_SUBACCOUNT_SEEN_BLOCKS for a different collateral pool`: { expectedQuoteBalance: big.NewInt(-100), @@ -3297,7 +3297,7 @@ func TestUpdateSubaccounts_WithdrawalsBlocked(t *testing.T) { updateType: types.Withdrawal, }, - `withdrawals are blocked if negative TNC subaccount was seen within + `withdrawals are blocked if negative TNC subaccount was seen within WITHDRAWAL_AND_TRANSFERS_BLOCKED_AFTER_NEGATIVE_TNC_SUBACCOUNT_SEEN_BLOCKS for an isolated perpetual collateral pool`: { expectedQuoteBalance: big.NewInt(-100), @@ -3332,7 +3332,7 @@ func TestUpdateSubaccounts_WithdrawalsBlocked(t *testing.T) { updateType: types.Withdrawal, }, - `withdrawals are blocked if negative TNC subaccount was seen within + `withdrawals are blocked if negative TNC subaccount was seen within WITHDRAWAL_AND_TRANSFERS_BLOCKED_AFTER_NEGATIVE_TNC_SUBACCOUNT_SEEN_BLOCKS for one isolated perpetual collateral pool and negative TNC subaccount was never seen for the cross-perpetual collateral pool, both of which are associated with subaccounts being updated`: { @@ -3377,7 +3377,7 @@ func TestUpdateSubaccounts_WithdrawalsBlocked(t *testing.T) { updateType: types.Withdrawal, }, - `withdrawals are blocked if negative TNC subaccount was seen within + `withdrawals are blocked if negative TNC subaccount was seen within WITHDRAWAL_AND_TRANSFERS_BLOCKED_AFTER_NEGATIVE_TNC_SUBACCOUNT_SEEN_BLOCKS for one isolated perpetual collateral pool and negative TNC subaccount was seen for the cross-perpetual collateral pool after WITHDRAWAL_AND_TRANSFERS_BLOCKED_AFTER_NEGATIVE_TNC_SUBACCOUNT_SEEN_BLOCKS, @@ -3424,7 +3424,7 @@ func TestUpdateSubaccounts_WithdrawalsBlocked(t *testing.T) { updateType: types.Withdrawal, }, - `withdrawals are blocked if negative TNC subaccount was seen within + `withdrawals are blocked if negative TNC subaccount was seen within WITHDRAWAL_AND_TRANSFERS_BLOCKED_AFTER_NEGATIVE_TNC_SUBACCOUNT_SEEN_BLOCKS for one isolated perpetual collateral pool and negative TNC subaccount was never seen for another isolated collateral pool, both of which are associated with subaccounts being updated`: { @@ -3472,7 +3472,7 @@ func TestUpdateSubaccounts_WithdrawalsBlocked(t *testing.T) { updateType: types.Withdrawal, }, - `withdrawals are blocked if negative TNC subaccount was seen within + `withdrawals are blocked if negative TNC subaccount was seen within WITHDRAWAL_AND_TRANSFERS_BLOCKED_AFTER_NEGATIVE_TNC_SUBACCOUNT_SEEN_BLOCKS for one isolated perpetual collateral pool and negative TNC subaccount was seen for another isolated perpetual collateral pool after WITHDRAWAL_AND_TRANSFERS_BLOCKED_AFTER_NEGATIVE_TNC_SUBACCOUNT_SEEN_BLOCKS, @@ -3991,7 +3991,7 @@ func TestUpdateSubaccounts_WithdrawalsBlocked(t *testing.T) { updateType: types.Transfer, }, - `transfers are not blocked if negative TNC subaccount was seen within + `transfers are not blocked if negative TNC subaccount was seen within WITHDRAWAL_AND_TRANSFERS_BLOCKED_AFTER_NEGATIVE_TNC_SUBACCOUNT_SEEN_BLOCKS for a different collateral pool from the ones associated with the subaccounts being updated`: { expectedQuoteBalance: big.NewInt(-100), @@ -4035,7 +4035,7 @@ func TestUpdateSubaccounts_WithdrawalsBlocked(t *testing.T) { updateType: types.Transfer, }, - `transfers are blocked if negative TNC subaccount was seen within + `transfers are blocked if negative TNC subaccount was seen within WITHDRAWAL_AND_TRANSFERS_BLOCKED_AFTER_NEGATIVE_TNC_SUBACCOUNT_SEEN_BLOCKS for one isolated perpetual collateral pool and negative TNC subaccount was never seen for the cross-perpetual collateral pool, both of which are associated with subaccounts being updated`: { @@ -4081,7 +4081,7 @@ func TestUpdateSubaccounts_WithdrawalsBlocked(t *testing.T) { updateType: types.Transfer, }, - `transferss are blocked if negative TNC subaccount was seen within + `transferss are blocked if negative TNC subaccount was seen within WITHDRAWAL_AND_TRANSFERS_BLOCKED_AFTER_NEGATIVE_TNC_SUBACCOUNT_SEEN_BLOCKS for one isolated perpetual collateral pool and negative TNC subaccount was seen for the cross-perpetual collateral pool after WITHDRAWAL_AND_TRANSFERS_BLOCKED_AFTER_NEGATIVE_TNC_SUBACCOUNT_SEEN_BLOCKS, @@ -4129,7 +4129,7 @@ func TestUpdateSubaccounts_WithdrawalsBlocked(t *testing.T) { updateType: types.Transfer, }, - `transfers are blocked if negative TNC subaccount was seen within + `transfers are blocked if negative TNC subaccount was seen within WITHDRAWAL_AND_TRANSFERS_BLOCKED_AFTER_NEGATIVE_TNC_SUBACCOUNT_SEEN_BLOCKS for one isolated perpetual collateral pool and negative TNC subaccount was never seen for another isolated perpetual collateral pool, both of which are associated with subaccounts being updated`: { @@ -4177,7 +4177,7 @@ func TestUpdateSubaccounts_WithdrawalsBlocked(t *testing.T) { updateType: types.Transfer, }, - `transferss are blocked if negative TNC subaccount was seen within + `transferss are blocked if negative TNC subaccount was seen within WITHDRAWAL_AND_TRANSFERS_BLOCKED_AFTER_NEGATIVE_TNC_SUBACCOUNT_SEEN_BLOCKS for one isolated perpetual collateral pool and negative TNC subaccount was seen for another the cross-perpetual collateral pool after WITHDRAWAL_AND_TRANSFERS_BLOCKED_AFTER_NEGATIVE_TNC_SUBACCOUNT_SEEN_BLOCKS, @@ -4406,7 +4406,7 @@ func TestCanUpdateSubaccounts(t *testing.T) { assetPositions: []*types.AssetPosition{ { AssetId: uint32(0), - // 900_000 USDC (just enough to colalteralize 90 BTC at $50_000 and 20% IMF) + // 900_000 USDC (just enough to collateralize 90 BTC at $50_000 and 20% IMF) Quantums: dtypes.NewInt(900_000_000_000), }, }, @@ -4416,7 +4416,7 @@ func TestCanUpdateSubaccounts(t *testing.T) { AssetPositions: []*types.AssetPosition{ { AssetId: uint32(0), - // 900_000 USDC (just enough to colalteralize 90 BTC at $50_000 and 20% IMF) + // 900_000 USDC (just enough to collateralize 90 BTC at $50_000 and 20% IMF) Quantums: dtypes.NewInt(900_000_000_000), }, }, @@ -4467,7 +4467,7 @@ func TestCanUpdateSubaccounts(t *testing.T) { assetPositions: []*types.AssetPosition{ { AssetId: uint32(0), - // 900_000 USDC (just enough to colalteralize 90 BTC at $50_000 and 20% IMF) + // 900_000 USDC (just enough to collateralize 90 BTC at $50_000 and 20% IMF) Quantums: dtypes.NewInt(900_000_000_000), }, }, @@ -4477,7 +4477,7 @@ func TestCanUpdateSubaccounts(t *testing.T) { AssetPositions: []*types.AssetPosition{ { AssetId: uint32(0), - // 900_000 USDC (just enough to colalteralize 90 BTC at $50_000 and 20% IMF) + // 900_000 USDC (just enough to collateralize 90 BTC at $50_000 and 20% IMF) Quantums: dtypes.NewInt(900_000_000_000), }, }, @@ -4535,7 +4535,7 @@ func TestCanUpdateSubaccounts(t *testing.T) { assetPositions: []*types.AssetPosition{ { AssetId: uint32(0), - // 900_000 USDC (just enough to colalteralize 90 BTC at $50_000 and 20% IMF) + // 900_000 USDC (just enough to collateralize 90 BTC at $50_000 and 20% IMF) Quantums: dtypes.NewInt(900_000_000_000), }, }, @@ -4545,7 +4545,7 @@ func TestCanUpdateSubaccounts(t *testing.T) { AssetPositions: []*types.AssetPosition{ { AssetId: uint32(0), - // 900_000 USDC (just enough to colalteralize 90 BTC at $50_000 and 20% IMF) + // 900_000 USDC (just enough to collateralize 90 BTC at $50_000 and 20% IMF) Quantums: dtypes.NewInt(900_000_000_000), }, }, @@ -4554,7 +4554,7 @@ func TestCanUpdateSubaccounts(t *testing.T) { openInterests: []perptypes.OpenInterestDelta{ { PerpetualId: uint32(0), - // (Only difference from prevoius test case) + // (Only difference from previous test case) // 410 BTC. At $50,000, this is $20,500,000 of OI. // OI would be $25,000,000 after the Match updates, so OIMF is still at base IMF. BaseQuantums: big.NewInt(41_000_000_000), @@ -4605,7 +4605,7 @@ func TestCanUpdateSubaccounts(t *testing.T) { assetPositions: []*types.AssetPosition{ { AssetId: uint32(0), - // 900_000 USDC (just enough to colalteralize 90 BTC at $50_000 and 20% IMF) + // 900_000 USDC (just enough to collateralize 90 BTC at $50_000 and 20% IMF) Quantums: dtypes.NewInt(900_000_000_000), }, }, @@ -4615,7 +4615,7 @@ func TestCanUpdateSubaccounts(t *testing.T) { AssetPositions: []*types.AssetPosition{ { AssetId: uint32(0), - // 900_000 USDC (just enough to colalteralize 90 BTC at $50_000 and 20% IMF) + // 900_000 USDC (just enough to collateralize 90 BTC at $50_000 and 20% IMF) Quantums: dtypes.NewInt(900_000_000_000), }, }, @@ -4624,10 +4624,10 @@ func TestCanUpdateSubaccounts(t *testing.T) { openInterests: []perptypes.OpenInterestDelta{ { PerpetualId: uint32(0), - // (Only difference from prevoius test case) - // 410 BTC + 1 base quantum. At $50,000, this is > $20,500,000 of OI. - // OI would be just past $25,000,000 after the Match updates, so OIMF > IMF = 20% - BaseQuantums: big.NewInt(41_000_000_001), + // (Only difference from previous test case) + // 410.001 BTC. At $50,000, this is $20,500,050 of OI. + // OI would be $25,000,050 after the Match updates, so OIMF > (IMF = 20%) + BaseQuantums: big.NewInt(41_000_100_000), }, }, updates: []types.Update{