Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Fix] Add maxSettlementFee implementation #481

Merged
merged 1 commit into from
Oct 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ interface IKeeperFactory is IOracleProviderFactory, IFactory {
error KeeperFactoryVersionOutsideRangeError();

function initialize(IOracleFactory oracleFactory) external;
function oracleFactory() external view returns (IOracleFactory);
function factoryType() external view returns (string memory);
function commitmentGasOracle() external view returns (IGasOracle);
function settlementGasOracle() external view returns (IGasOracle);
Expand Down
6 changes: 6 additions & 0 deletions packages/perennial-oracle/contracts/keeper/KeeperOracle.sol
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { OracleVersion } from "@perennial/core/contracts/types/OracleVersion.sol
import { OracleReceipt } from "@perennial/core/contracts/types/OracleReceipt.sol";
import { IKeeperFactory } from "../interfaces/IKeeperFactory.sol";
import { IKeeperOracle } from "../interfaces/IKeeperOracle.sol";
import { OracleParameter } from "../types/OracleParameter.sol";
import { PriceResponse, PriceResponseStorage, PriceResponseLib } from "./types/PriceResponse.sol";
import { KeeperOracleParameter } from "./types/KeeperOracleParameter.sol";
import { IOracle } from "../interfaces/IOracle.sol";
Expand Down Expand Up @@ -192,6 +193,7 @@ contract KeeperOracle is IKeeperOracle, Instance {
) private returns (PriceResponse memory priceResponse) {
IKeeperFactory factory = IKeeperFactory(address(factory()));
KeeperOracleParameter memory keeperOracleParameter = factory.parameter();
OracleParameter memory oracleParameter = factory.oracleFactory().parameter();

if (block.timestamp <= (next() + timeout)) {
if (!oracleVersion.valid) revert KeeperOracleInvalidPriceError();
Expand All @@ -206,6 +208,10 @@ contract KeeperOracle is IKeeperOracle, Instance {
priceResponse.syncFee = UFixed6Lib.from(factory.commitmentGasOracle().cost(value), true);
priceResponse.asyncFee = UFixed6Lib.from(factory.settlementGasOracle().cost(0), true);
priceResponse.oracleFee = keeperOracleParameter.oracleFee;
priceResponse.applyFeeMaximum(
oracleParameter.maxSettlementFee,
_localCallbacks[oracleVersion.timestamp].length()
);

_responses[oracleVersion.timestamp].store(priceResponse);
_global.latestIndex++;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,26 @@ library PriceResponseLib {
/// @param callbacks The number of settlement callbacks to be made
/// @return The corresponding oracle receipt
function toOracleReceipt(PriceResponse memory self, uint256 callbacks) internal pure returns (OracleReceipt memory) {
return OracleReceipt(self.syncFee.add(self.asyncFee.mul(UFixed6Lib.from(callbacks))), self.oracleFee);
return OracleReceipt(settlementFee(self, callbacks), self.oracleFee);
}

/// @notice Returns the total settlement fee for the price response
/// @param self The price response object
/// @param callbacks The number of settlement callbacks to be made
/// @return The total settlement fee
function settlementFee(PriceResponse memory self, uint256 callbacks) internal pure returns (UFixed6) {
return self.syncFee.add(self.asyncFee.mul(UFixed6Lib.from(callbacks)));
}

/// @notice Scales down sync and async fees if they exceed the maximum settlement fee
/// @param self The price response object
/// @param maxSettlementFee The maximum settlement fee
function applyFeeMaximum(PriceResponse memory self, UFixed6 maxSettlementFee, uint256 callbacks) internal pure {
UFixed6 totalSettlementFee = settlementFee(self, callbacks);
if (totalSettlementFee.gt(maxSettlementFee)) {
self.syncFee = self.syncFee.muldiv(maxSettlementFee, totalSettlementFee);
self.asyncFee = self.asyncFee.muldiv(maxSettlementFee, totalSettlementFee);
}
}
}

Expand Down
11 changes: 11 additions & 0 deletions packages/perennial-oracle/contracts/test/PriceResponseTester.sol
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// SPDX-License-Identifier: Apache-2.0
pragma solidity ^0.8.13;

import { UFixed6 } from "@equilibria/root/number/types/UFixed6.sol";
import { PriceResponse, PriceResponseLib, PriceResponseStorage } from "../keeper/types/PriceResponse.sol";
import { OracleVersion } from "@perennial/core/contracts/types/OracleVersion.sol";
import { OracleReceipt } from "@perennial/core/contracts/types/OracleReceipt.sol";
Expand All @@ -27,4 +28,14 @@ contract PriceResponseTester {
function toOracleReceipt(PriceResponse memory self, uint256 callbacks) external pure returns (OracleReceipt memory) {
return PriceResponseLib.toOracleReceipt(self, callbacks);
}

function settlementFee(PriceResponse memory self, uint256 callbacks) external pure returns (UFixed6) {
return PriceResponseLib.settlementFee(self, callbacks);
}

function applyFeeMaximum(UFixed6 maxSettlementFee, uint256 callbacks) external {
PriceResponse memory newPriceResponse = read();
newPriceResponse.applyFeeMaximum(maxSettlementFee, callbacks);
store(newPriceResponse);
}
}
172 changes: 172 additions & 0 deletions packages/perennial-oracle/test/unit/types/PriceResponse.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -218,4 +218,176 @@ describe('PriceResponse', () => {
expect(value.oracleFee).to.equal(parse6decimal('0.01'))
})
})

describe('#settlementFee', () => {
it('calculates correct fee w/ non-zero sync, zero async', async () => {
const value = await priceResponse.settlementFee(
{
...DEFAULT_PRICE_RESPONSE,
syncFee: parse6decimal('1'),
asyncFee: parse6decimal('0.1'),
},
0,
)

expect(value).to.equal(parse6decimal('1.0'))
})

it('calculates correct fee w/ non-zero sync, single async', async () => {
const value = await priceResponse.settlementFee(
{
...DEFAULT_PRICE_RESPONSE,
syncFee: parse6decimal('1'),
asyncFee: parse6decimal('0.1'),
},
1,
)

expect(value).to.equal(parse6decimal('1.1'))
})

it('calculates correct fee w/ non-zero sync, multiple async', async () => {
const value = await priceResponse.settlementFee(
{
...DEFAULT_PRICE_RESPONSE,
syncFee: parse6decimal('1'),
asyncFee: parse6decimal('0.1'),
},
5,
)

expect(value).to.equal(parse6decimal('1.5'))
})

it('calculates correct fee w/ zero sync, zero async', async () => {
const value = await priceResponse.settlementFee(
{
...DEFAULT_PRICE_RESPONSE,
syncFee: parse6decimal('0'),
asyncFee: parse6decimal('0.1'),
},
0,
)

expect(value).to.equal(parse6decimal('0.0'))
})

it('calculates correct fee w/ zero sync, single async', async () => {
const value = await priceResponse.settlementFee(
{
...DEFAULT_PRICE_RESPONSE,
syncFee: parse6decimal('0'),
asyncFee: parse6decimal('0.1'),
},
1,
)

expect(value).to.equal(parse6decimal('0.1'))
})

it('calculates correct fee w/ zero sync, multiple async', async () => {
const value = await priceResponse.settlementFee(
{
...DEFAULT_PRICE_RESPONSE,
syncFee: parse6decimal('0'),
asyncFee: parse6decimal('0.1'),
},
5,
)

expect(value).to.equal(parse6decimal('0.5'))
})
})

describe('#applyFeeMaximum', () => {
it('calculates correct fee w/ non-zero sync, zero async', async () => {
await priceResponse.store({
...DEFAULT_PRICE_RESPONSE,
syncFee: parse6decimal('1'),
asyncFee: parse6decimal('0.2'),
})

await priceResponse.applyFeeMaximum(parse6decimal('0.5'), 0)

const value = await priceResponse.read()

expect(value.syncFee).to.equal(parse6decimal('0.5'))
expect(value.asyncFee).to.equal(parse6decimal('0.1'))
})

it('calculates correct fee w/ non-zero sync, single async', async () => {
await priceResponse.store({
...DEFAULT_PRICE_RESPONSE,
syncFee: parse6decimal('1'),
asyncFee: parse6decimal('0.2'),
})

await priceResponse.applyFeeMaximum(parse6decimal('0.6'), 1)

const value = await priceResponse.read()

expect(value.syncFee).to.equal(parse6decimal('0.5'))
expect(value.asyncFee).to.equal(parse6decimal('0.1'))
})

it('calculates correct fee w/ non-zero sync, multiple async', async () => {
await priceResponse.store({
...DEFAULT_PRICE_RESPONSE,
syncFee: parse6decimal('1'),
asyncFee: parse6decimal('0.2'),
})

await priceResponse.applyFeeMaximum(parse6decimal('1.0'), 5)

const value = await priceResponse.read()

expect(value.syncFee).to.equal(parse6decimal('0.5'))
expect(value.asyncFee).to.equal(parse6decimal('0.1'))
})

it('calculates correct fee w/ zero sync, zero async', async () => {
await priceResponse.store({
...DEFAULT_PRICE_RESPONSE,
syncFee: parse6decimal('0'),
asyncFee: parse6decimal('0.2'),
})

await priceResponse.applyFeeMaximum(parse6decimal('0.0'), 0)

const value = await priceResponse.read()

expect(value.syncFee).to.equal(parse6decimal('0'))
expect(value.asyncFee).to.equal(parse6decimal('0.2'))
})

it('calculates correct fee w/ zero sync, single async', async () => {
await priceResponse.store({
...DEFAULT_PRICE_RESPONSE,
syncFee: parse6decimal('0'),
asyncFee: parse6decimal('0.2'),
})

await priceResponse.applyFeeMaximum(parse6decimal('0.1'), 1)

const value = await priceResponse.read()

expect(value.syncFee).to.equal(parse6decimal('0.0'))
expect(value.asyncFee).to.equal(parse6decimal('0.1'))
})

it('calculates correct fee w/ zero sync, multiple async', async () => {
await priceResponse.store({
...DEFAULT_PRICE_RESPONSE,
syncFee: parse6decimal('0'),
asyncFee: parse6decimal('0.2'),
})

await priceResponse.applyFeeMaximum(parse6decimal('0.5'), 5)

const value = await priceResponse.read()

expect(value.syncFee).to.equal(parse6decimal('0.0'))
expect(value.asyncFee).to.equal(parse6decimal('0.1'))
})
})
})
Loading