Skip to content

Commit

Permalink
finalize Oracle Cross-rate price assertions (#421)
Browse files Browse the repository at this point in the history
support bridge out, destination chain fee transaction
calculations in tokens that aren't directly priced in
GALA, using a common cross-rate exchange rate denominated
in a widely used token.
  • Loading branch information
sentientforest authored Nov 4, 2024
1 parent a2db7da commit a4f229c
Show file tree
Hide file tree
Showing 6 changed files with 159 additions and 50 deletions.
18 changes: 14 additions & 4 deletions chain-api/src/types/OracleBridgeFeeAssertion.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,14 @@
*/
import BigNumber from "bignumber.js";
import { Type } from "class-transformer";
import { IsBoolean, IsNotEmpty, IsNumber, Max, Min, ValidateNested } from "class-validator";
import { IsBoolean, IsNotEmpty, IsNumber, Max, Min, ValidateIf, ValidateNested } from "class-validator";
import { JSONSchema } from "class-validator-jsonschema";

import { ChainKey } from "../utils";
import { BigNumberProperty } from "../validators";
import { ChainObject } from "./ChainObject";
import { OraclePriceAssertion } from "./OraclePriceAssertion";
import { OraclePriceCrossRateAssertion } from "./OraclePriceCrossRateAssertion";
import { TokenClassKey } from "./TokenClass";

@JSONSchema({
Expand Down Expand Up @@ -49,9 +50,18 @@ export class OracleBridgeFeeAssertion extends ChainObject {
@JSONSchema({
description: "Exchange Rate Price Assertion used to calculate Gas Fee"
})
@ValidateIf((assertion) => !!assertion.galaExchangeCrossRate)
@ValidateNested()
@Type(() => OraclePriceAssertion)
public galaExchangeRate: OraclePriceAssertion;
public galaExchangeRate?: OraclePriceAssertion;

@JSONSchema({
description: "Cross-Rate Exchange Rate used to calculate Gas Fee"
})
@ValidateIf((assertion) => !!assertion.galaExchangeRate)
@ValidateNested()
@Type(() => OraclePriceCrossRateAssertion)
public galaExchangeCrossRate?: OraclePriceCrossRateAssertion;

@JSONSchema({
description:
Expand All @@ -64,7 +74,7 @@ export class OracleBridgeFeeAssertion extends ChainObject {

@JSONSchema({
description:
"The token requested to bridge. Token Class used to query the estimated " + "transaction fee units."
"The token requested to bridge. Token Class used to query the estimated transaction fee units."
})
@ValidateNested()
@Type(() => TokenClassKey)
Expand All @@ -85,7 +95,7 @@ export class OracleBridgeFeeAssertion extends ChainObject {
public estimatedTxFeeUnitsTotal: BigNumber;

@JSONSchema({
description: "Estimated price per unit of gas, as retrieved approximately " + "at the time of assertion."
description: "Estimated price per unit of gas, as retrieved approximately at the time of assertion."
})
@BigNumberProperty()
public estimatedPricePerTxFeeUnit: BigNumber;
Expand Down
21 changes: 17 additions & 4 deletions chain-api/src/types/OraclePriceAssertion.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import { TokenInstanceKey } from "./TokenInstance";
description:
"External token/currency definition. Use to specify an external currency that does not have a TokenClass defined on GalaChain."
})
export class ExternalToken {
export class ExternalToken extends ChainObject {
@JSONSchema({
description: `Name of the external currency, e.g. "Ethereum"`
})
Expand Down Expand Up @@ -62,13 +62,25 @@ export class OraclePriceAssertion extends ChainObject {
@JSONSchema({
description: "First currency in the described currency pair. Unit of exchange."
})
@ValidateIf((assertion) => !!assertion.externalBaseToken)
@ValidateNested()
@Type(() => TokenInstanceKey)
baseToken: TokenInstanceKey;
baseToken?: TokenInstanceKey;

@JSONSchema({
description:
"Second token/currency in the pair. Token/Currency in which the baseToken is quoted. Optional, but required if externalQuoteToken is not provided."
"External token representing the first currency in the described currency pair. " +
"Unit of exchange. Optional, but required if baseToken is not provided."
})
@ValidateIf((assertion) => !!assertion.baseToken)
@ValidateNested()
@Type(() => ExternalToken)
externalBaseToken?: ExternalToken;

@JSONSchema({
description:
"Second token/currency in the pair. Token/Currency in which the baseToken is quoted. " +
"Optional, but required if externalQuoteToken is not provided."
})
@ValidateIf((o) => !o.externalQuoteToken)
@ValidateNested()
Expand All @@ -77,7 +89,8 @@ export class OraclePriceAssertion extends ChainObject {

@JSONSchema({
description:
"Second token/currency in the pair. Token/Currency in which the baseToken is quoted. Optional, but required if quoteToken is not provided."
"Second token/currency in the pair. Token/Currency in which the baseToken is quoted. " +
"Optional, but required if quoteToken is not provided."
})
@ValidateIf((o) => !o.quoteToken)
@ValidateNested()
Expand Down
14 changes: 9 additions & 5 deletions chain-api/src/types/OraclePriceCrossRateAssertion.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,13 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { IsNotEmpty } from "class-validator";
import { Type } from "class-transformer";
import { IsNotEmpty, ValidateNested } from "class-validator";
import { JSONSchema } from "class-validator-jsonschema";

import { ChainKey } from "../utils";
import { ChainObject } from "./ChainObject";
import { OraclePriceAssertion } from "./OraclePriceAssertion";

export class OraclePriceCrossRateAssertion extends ChainObject {
public static INDEX_KEY = "GCOC"; // GalaChain Oracle Cross-rate
Expand All @@ -42,12 +44,14 @@ export class OraclePriceCrossRateAssertion extends ChainObject {
@JSONSchema({
description: "Chain key referencing the saved baseToken price assertion"
})
@IsNotEmpty()
public baseTokenPriceAssertionKey: string;
@ValidateNested()
@Type(() => OraclePriceAssertion)
public baseTokenCrossRate: OraclePriceAssertion;

@JSONSchema({
description: "Chain key referencing the saved quote token price assertion"
})
@IsNotEmpty()
public quoteTokenPriceAssertionKey: string;
@ValidateNested()
@Type(() => OraclePriceAssertion)
public quoteTokenCrossRate: OraclePriceAssertion;
}
120 changes: 88 additions & 32 deletions chain-api/src/types/oracle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,15 +137,29 @@ export class OraclePriceAssertionDto extends ChainCallDTO {
public identity: string;

@JSONSchema({
description: "First currency in the described currency pair. Unit of exchange."
description:
"First currency in the described currency pair. Unit of exchange. " +
"Optional, but required if externalBaseToken is not provided."
})
@ValidateIf((assertion) => !!assertion.externalBaseToken)
@ValidateNested()
@Type(() => TokenInstanceKey)
baseToken: TokenInstanceKey;
baseToken?: TokenInstanceKey;

@JSONSchema({
description:
"Second token/currency in the pair. Token/Currency in which the baseToken is quoted. Optional, but required if externalQuoteToken is not provided."
"External token representing the first currency in the described currency pair. " +
"Unit of exchange. Optional, but required if baseToken is not provided."
})
@ValidateIf((assertion) => !!assertion.baseToken)
@ValidateNested()
@Type(() => ExternalToken)
externalBaseToken?: ExternalToken;

@JSONSchema({
description:
"Second token/currency in the pair. Token/Currency in which the baseToken is quoted. " +
"Optional, but required if externalQuoteToken is not provided."
})
@ValidateIf((o) => !!o.externalQuoteToken)
@ValidateNested()
Expand Down Expand Up @@ -235,40 +249,72 @@ export class OraclePriceCrossRateAssertionDto extends ChainCallDTO {
@Type(() => OraclePriceAssertionDto)
quoteTokenCrossRate: OraclePriceAssertionDto;

// todo: consider supporting external tokens here as well
@JSONSchema({
description:
"Comparative token used to price both the base and quote tokens in order to calculate an exchange cross-rate."
"Comparative token used to price both the base and quote tokens in order to " +
"calculate an exchange cross-rate. Optional, but required if externalCrossRateToken is " +
"not provided."
})
@ValidateIf((assertion) => !!assertion.externalCrossRateToken)
@ValidateNested()
@Type(() => TokenInstanceKey)
crossRateToken: TokenInstanceKey;
crossRateToken?: TokenInstanceKey;

@JSONSchema({
description:
"Comparative token used to price both the base and quote tokens in order to " +
"calculate an exchange cross-rate. Optional, but required if crossRateToken is not provided."
})
@ValidateIf((assertion) => !!assertion.crossRateToken)
@ValidateNested()
@Type(() => ExternalToken)
externalCrossRateToken?: ExternalToken;

// todo: maybe we don't require this to be set, we just always calculate it
// arguably this is duplication, or maybe unnecessary validation steps
// maybe calculate it and write it in the chain data structure, but don't specify it in the DTO
// @JSONSchema({
// description: "Cross rate for baseToken and quoteToken, calculated from the crossRateToken exchange rates."
// })
// @BigNumberProperty()
// crossRate: BigNumber;
@JSONSchema({
description: "Cross rate for baseToken and quoteToken, calculated from the crossRateToken exchange rates."
})
@BigNumberProperty()
crossRate: BigNumber;

@Exclude()
public validateCrossRateTokenKeys() {
const crossRateToken: TokenInstanceKey | undefined = this.crossRateToken;
const baseTokenCrossRateToken: TokenInstanceKey | undefined = this.baseTokenCrossRate.quoteToken;
const quoteTokenCrossRateToken: TokenInstanceKey | undefined = this.quoteTokenCrossRate.quoteToken;

if (baseTokenCrossRateToken === undefined || quoteTokenCrossRateToken === undefined) {
throw new NotImplementedError(
`External token support is not yet implemented for cross rate exchange assertions`
);
}
const externalCrossRateToken: ExternalToken | undefined = this.externalCrossRateToken;
const baseTokenExternalCrossRateToken: ExternalToken | undefined =
this.baseTokenCrossRate.externalQuoteToken;
const quoteTokenExternalCrossRateToken: ExternalToken | undefined =
this.quoteTokenCrossRate.externalQuoteToken;

if (baseTokenCrossRateToken.toStringKey() !== quoteTokenCrossRateToken.toStringKey()) {
if (crossRateToken === undefined && externalCrossRateToken === undefined) {
throw new ValidationFailedError(
`Neither crossRateToken nor externalCrossRateToken defined on OraclePriceCrossRateAssertionDto, both undefined`
);
} else if (
crossRateToken !== undefined &&
(baseTokenCrossRateToken === undefined ||
quoteTokenCrossRateToken === undefined ||
crossRateToken.toStringKey() !== baseTokenCrossRateToken?.toStringKey() ||
crossRateToken.toStringKey() !== quoteTokenCrossRateToken?.toStringKey())
) {
throw new ValidationFailedError(
`Cross rate validation failed: ` +
`baseToken cross-quoted in ${baseTokenCrossRateToken?.toStringKey()} but ` +
`quoteToken cross-quoted in ${quoteTokenCrossRateToken?.toStringKey()}`
);
} else if (
externalCrossRateToken !== undefined &&
(baseTokenExternalCrossRateToken === undefined ||
quoteTokenExternalCrossRateToken === undefined ||
externalCrossRateToken.symbol !== baseTokenExternalCrossRateToken.symbol ||
externalCrossRateToken.symbol !== quoteTokenExternalCrossRateToken.symbol)
) {
throw new ValidationFailedError(
`Cross rate validation failed: ` +
`baseToken cross-quoted in ${baseTokenCrossRateToken.toStringKey()} but ` +
`quoteToken cross-quoted in ${quoteTokenCrossRateToken.toStringKey()}`
`baseToken cross-quoted in ${baseTokenExternalCrossRateToken?.symbol} but ` +
`quoteToken cross-quoted in ${quoteTokenExternalCrossRateToken?.symbol}`
);
}
}
Expand All @@ -284,16 +330,17 @@ export class OraclePriceCrossRateAssertionDto extends ChainCallDTO {

return calculatedCrossRate;
}
// @Exclude()
// public validateCrossRate() {
// const calculatedCrossRate = this.calculateCrossRate();

// if (!this.crossRate.isEqualTo(calculatedCrossRate)) {
// throw new ValidationFailedError(
// `Asserted cross rate (${this.crossRate} is not equal to calculated cross rate)`
// )
// }
// }

@Exclude()
public validateCrossRate() {
const calculatedCrossRate = this.calculateCrossRate();

if (!this.crossRate.isEqualTo(calculatedCrossRate)) {
throw new ValidationFailedError(
`Asserted cross rate (${this.crossRate} is not equal to calculated cross rate)`
);
}
}
}

export class FetchOraclePriceCrossRateAssertionsResponse extends ChainCallDTO {
Expand Down Expand Up @@ -327,10 +374,19 @@ export class OracleBridgeFeeAssertionDto extends ChainCallDTO {
@JSONSchema({
description: "Exchange Rate Price Assertion used to calculate Gas Fee"
})
@ValidateIf((assertion) => !!assertion.galaExchangeCrossRate)
@ValidateNested()
@Type(() => OraclePriceAssertionDto)
public galaExchangeRate: OraclePriceAssertionDto;

@JSONSchema({
description: "Cross-Rate Exchange Rate used to calculate Gas Fee"
})
@ValidateIf((assertion) => !!assertion.galaExchangeRate)
@ValidateNested()
@Type(() => OraclePriceCrossRateAssertion)
public galaExchangeCrossRate?: OraclePriceCrossRateAssertion;

@JSONSchema({
description:
"Rounding decimals used for estimatedTotalTxFeeInGala. " +
Expand Down
20 changes: 16 additions & 4 deletions chaincode/src/fees/feeGateImplementations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import {
OracleBridgeFeeAssertionDto,
OracleDefinition,
OraclePriceAssertion,
OraclePriceCrossRateAssertion,
PaymentRequiredError,
RequestTokenBridgeOutDto,
TerminateTokenSwapDto,
Expand Down Expand Up @@ -333,6 +334,7 @@ export async function requestTokenBridgeOutFeeGate(ctx: GalaChainContext, dto: R

const {
galaExchangeRate,
galaExchangeCrossRate,
galaDecimals,
bridgeToken,
bridgeTokenIsNonFungible,
Expand Down Expand Up @@ -360,10 +362,20 @@ export async function requestTokenBridgeOutFeeGate(ctx: GalaChainContext, dto: R
timestamp
});

bridgeFeeAssertionRecord.galaExchangeRate = await createValidChainObject(OraclePriceAssertion, {
...galaExchangeRate,
txid
});
if (galaExchangeRate !== undefined) {
bridgeFeeAssertionRecord.galaExchangeRate = await createValidChainObject(OraclePriceAssertion, {
...galaExchangeRate,
txid
});
} else if (galaExchangeCrossRate !== undefined) {
bridgeFeeAssertionRecord.galaExchangeCrossRate = await createValidChainObject(
OraclePriceCrossRateAssertion,
{
...galaExchangeCrossRate,
txid
}
);
}

await putChainObject(ctx, bridgeFeeAssertionRecord);
}
Expand Down
16 changes: 15 additions & 1 deletion chaincode/src/oracle/saveOraclePriceAssertion.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,14 +54,28 @@ export async function saveOraclePriceAssertion(ctx: GalaChainContext, dto: Oracl

await ensureIsAuthorizedBy(ctx, dto, identity);

const { oracle, baseToken, quoteToken, exchangeRate, source, sourceUrl, timestamp } = dto;
const {
oracle,
baseToken,
externalBaseToken,
quoteToken,
externalQuoteToken,
exchangeRate,
source,
sourceUrl,
timestamp
} = dto;

const txid = ctx.stub.getTxID();

const priceAssertion = plainToInstance(OraclePriceAssertion, {
oracle,
identity,
txid,
baseToken,
externalBaseToken,
quoteToken,
externalQuoteToken,
exchangeRate,
source,
sourceUrl,
Expand Down

0 comments on commit a4f229c

Please sign in to comment.