Skip to content
Draft
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
11 changes: 11 additions & 0 deletions chain-api/src/types/oracle.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,17 @@ describe("oracle.ts", () => {
mockAssertionDto.crossRate = new BigNumber("42");

expect(mockAssertionDto.validateCrossRate).toThrow();

await expect(
createValidSubmitDTO(OraclePriceCrossRateAssertionDto, {
oracle: mockOracle,
identity: mockOracleIdentity,
baseTokenCrossRate: tonUsdPriceAssertion,
quoteTokenCrossRate: galaUsdPriceAssertion,
externalCrossRateToken: usdDetails,
crossRate: new BigNumber("42")
})
).rejects.toThrow(/cross rate does not match/);
});

test("FetchOracleDefinitionsDto accepts optional name filter", async () => {
Expand Down
30 changes: 29 additions & 1 deletion chain-api/src/types/oracle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,10 @@ import {
Min,
Validate,
ValidateIf,
ValidateNested
ValidateNested,
ValidationArguments,
ValidatorConstraint,
ValidatorConstraintInterface
} from "class-validator";
import { JSONSchema } from "class-validator-jsonschema";

Expand Down Expand Up @@ -255,6 +258,24 @@ export class FetchOraclePriceAssertionsResponse extends ChainCallDTO {
bookmark?: string | undefined;
}

@ValidatorConstraint({ name: "crossRateMatchesQuote", async: false })
class CrossRateMatchesQuoteConstraint implements ValidatorConstraintInterface {
validate(_value: unknown, args: ValidationArguments): boolean {
const assertion = args.object as OraclePriceCrossRateAssertionDto;
try {
assertion.validateCrossRateTokenKeys();
assertion.validateCrossRate();
return true;
} catch {
return false;
}
}

defaultMessage(): string {
return "cross rate does not match the quoted token pair";
}
}

@JSONSchema({
description: "Cross Rate Exchange price assertion. E.g. compare $GALA to $ETH via price in $USD for each."
})
Expand Down Expand Up @@ -310,6 +331,7 @@ export class OraclePriceCrossRateAssertionDto extends SubmitCallDTO {
@JSONSchema({
description: "Cross rate for baseToken and quoteToken, calculated from the crossRateToken exchange rates."
})
@Validate(CrossRateMatchesQuoteConstraint)
@PositiveFiniteRate()
@BigNumberProperty()
crossRate: BigNumber;
Expand Down Expand Up @@ -483,6 +505,12 @@ export class OracleBridgeFeeAssertionDto extends SubmitCallDTO {
@Max(32)
public galaDecimals: number;

@JSONSchema({
description: "Destination chain this quote prices."
})
@IsInt()
public destinationChainId: number;

@JSONSchema({
description:
"The token requested to bridge. Token Class used to query the estimated " + "transaction fee units."
Expand Down
29 changes: 29 additions & 0 deletions chaincode/src/fees/feeGateImplementations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ import {
SetTokenInstanceMetadataDto,
TerminateTokenSwapDto,
TokenClassKey,
TokenInstance,
TokenInstanceKey,
TokenMintConfiguration,
TransferTokenDto,
Expand Down Expand Up @@ -155,6 +156,32 @@ export async function mintTokenWithAllowanceFeeGate(
});
}

export function assertOracleQuoteMatchesTransfer(
quote: OracleBridgeFeeAssertionDto,
transfer: { destinationChainId: number; tokenInstance: TokenInstanceKey }
): void {
const quoted = quote.bridgeToken;
const requested = transfer.tokenInstance;
const sameClass =
quoted.collection === requested.collection &&
quoted.category === requested.category &&
quoted.type === requested.type &&
quoted.additionalKey === requested.additionalKey;

if (!sameClass) {
throw new ValidationFailedError("Oracle quote is for a different token");
}

const requestedIsNonFungible = !TokenInstance.isFungible(requested.instance);
if (quote.bridgeTokenIsNonFungible !== requestedIsNonFungible) {
throw new ValidationFailedError("Oracle quote does not match whether the token is non-fungible");
}

if (quote.destinationChainId !== transfer.destinationChainId) {
throw new ValidationFailedError("Oracle quote is for a different destination chain");
}
}

export interface RequestTokenBridgeOutFeeGateParams {
destinationChainId: number;
tokenInstance: TokenInstanceKey;
Expand Down Expand Up @@ -242,6 +269,8 @@ export async function requestTokenBridgeOutFeeGate(
);
}

assertOracleQuoteMatchesTransfer(oracleAssertion, dto);

const gasFeeQuantity: BigNumber = oracleAssertion.estimatedTotalTxFeeInGala;

// standard GalaChain usage based fees optionally defined with FeeCodeDefinition(s)
Expand Down
65 changes: 65 additions & 0 deletions chaincode/src/fees/oracleQuoteBinding.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
/*
* Copyright (c) Gala Games Inc. All rights reserved.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { OracleBridgeFeeAssertionDto, TokenClassKey, TokenInstance, TokenInstanceKey } from "@gala-chain/api";
import BigNumber from "bignumber.js";

import { assertOracleQuoteMatchesTransfer } from "./feeGateImplementations";

const token = {
collection: "GALA",
category: "Unit",
type: "none",
additionalKey: "none",
instance: TokenInstance.FUNGIBLE_TOKEN_INSTANCE
} as TokenInstanceKey;

function quote(overrides: Partial<OracleBridgeFeeAssertionDto> = {}): OracleBridgeFeeAssertionDto {
return {
destinationChainId: 2,
bridgeToken: token,
bridgeTokenIsNonFungible: false,
...overrides
} as OracleBridgeFeeAssertionDto;
}

describe("oracle quote binding", () => {
it("accepts a quote for the same token, fungible flag, and destination", () => {
expect(() =>
assertOracleQuoteMatchesTransfer(quote(), { destinationChainId: 2, tokenInstance: token })
).not.toThrow();
});

it("rejects a quote for a different token, fungible flag, or destination", () => {
const nft = { ...token, instance: new BigNumber("1") } as TokenInstanceKey;

expect(() =>
assertOracleQuoteMatchesTransfer(quote({ destinationChainId: 1001 }), {
destinationChainId: 2,
tokenInstance: token
})
).toThrow(/destination/);

expect(() =>
assertOracleQuoteMatchesTransfer(quote(), { destinationChainId: 2, tokenInstance: nft })
).toThrow(/non-fungible/);

expect(() =>
assertOracleQuoteMatchesTransfer(
quote({ bridgeToken: { ...token, collection: "OTHER" } as TokenClassKey }),
{ destinationChainId: 2, tokenInstance: token }
)
).toThrow(/different token/);
});
});
Loading