Skip to content

feat: Solana relayFeeCalculator and gasPriceOracle #980

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

Open
wants to merge 14 commits into
base: epic/svm-client
Choose a base branch
from
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@
"@pinata/sdk": "^2.1.0",
"@solana/kit": "^2.1.0",
"@solana/web3.js": "^1.31.0",
"@solana-program/system": "^0.7.0",
"@types/mocha": "^10.0.1",
"@uma/sdk": "^0.34.10",
"arweave": "^1.14.4",
Expand Down
216 changes: 212 additions & 4 deletions src/arch/svm/SpokeUtils.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,34 @@
import { Rpc, SolanaRpcApi, Address } from "@solana/kit";

import {
SvmAddress,
getTokenInformationFromAddress,
BigNumber,
isDefined,
isUnsafeDepositId,
toAddressType,
toBN,
getMessageHash,
keccak256,
} from "../../utils";
import { SvmSpokeClient } from "@across-protocol/contracts";
import { getStatePda } from "./";
import { Deposit, FillStatus, FillWithBlock, RelayData } from "../../interfaces";
import { BigNumber, isUnsafeDepositId } from "../../utils";
import {
TOKEN_PROGRAM_ADDRESS,
ASSOCIATED_TOKEN_PROGRAM_ADDRESS,
getApproveCheckedInstruction,
} from "@solana-program/token";
import {
Address,
Rpc,
SolanaRpcApi,
some,
getProgramDerivedAddress,
type TransactionSigner,
address,
} from "@solana/kit";
import { fetchState } from "@across-protocol/contracts/dist/src/svm/clients/SvmSpoke";

type Provider = Rpc<SolanaRpcApi>;
export type Provider = Rpc<SolanaRpcApi>;

/**
* @param spokePool SpokePool Contract instance.
Expand Down Expand Up @@ -174,3 +198,187 @@ export function findFillEvent(
): Promise<FillWithBlock | undefined> {
throw new Error("fillStatusArray: not implemented");
}

/**
* @param spokePool Address (program ID) of the SvmSpoke.
* @param deposit V3Deopsit instance.
* @param relayer Address of the relayer filling the deposit.
* @param repaymentChainId Optional repaymentChainId (defaults to destinationChainId).
* @returns An Ethers UnsignedTransaction instance.
*/
export async function fillRelayInstruction(
spokePool: SvmAddress,
deposit: Omit<Deposit, "messageHash">,
relayer: TransactionSigner<string>,
recipientTokenAccount: Address<string>,
repaymentChainId = deposit.destinationChainId
) {
const programId = spokePool.toBase58();
const relayerAddress = SvmAddress.from(relayer.address);

// @todo we need to convert the deposit's relayData to svm-like since the interface assumes the data originates from an EVM Spoke pool.
// Once we migrate to `Address` types, this can be modified/removed.
const [depositor, recipient, exclusiveRelayer, inputToken, outputToken] = [
deposit.depositor,
deposit.recipient,
deposit.exclusiveRelayer,
deposit.inputToken,
deposit.outputToken,
].map((addr) => toAddressType(addr).forceSvmAddress());

const _relayDataHash = getRelayDataHash(deposit, deposit.destinationChainId);
const relayDataHash = new Uint8Array(Buffer.from(_relayDataHash.slice(2), "hex"));

// Create ATA for the relayer and recipient token accounts
const relayerTokenAccount = await getAssociatedTokenAddress(relayerAddress, outputToken);

const [statePda, fillStatusPda, eventAuthority] = await Promise.all([
getStatePda(spokePool.toV2Address()),
getFillStatusPda(_relayDataHash, spokePool.toV2Address()),
getEventAuthority(),
]);
const depositIdBuffer = Buffer.alloc(32);
const shortenedBuffer = Buffer.from(deposit.depositId.toHexString().slice(2), "hex");
shortenedBuffer.copy(depositIdBuffer, 32 - shortenedBuffer.length);

return SvmSpokeClient.getFillRelayInstruction({
signer: relayer,
state: statePda,
mint: outputToken.toV2Address(),
relayerTokenAccount: relayerTokenAccount,
recipientTokenAccount: recipientTokenAccount,
fillStatus: fillStatusPda,
eventAuthority,
program: address(programId),
relayHash: relayDataHash,
relayData: some({
depositor: depositor.toV2Address(),
recipient: recipient.toV2Address(),
exclusiveRelayer: exclusiveRelayer.toV2Address(),
inputToken: inputToken.toV2Address(),
outputToken: outputToken.toV2Address(),
inputAmount: deposit.inputAmount.toBigInt(),
outputAmount: deposit.outputAmount.toBigInt(),
originChainId: BigInt(deposit.originChainId),
fillDeadline: deposit.fillDeadline,
exclusivityDeadline: deposit.exclusivityDeadline,
depositId: new Uint8Array(depositIdBuffer),
message: new Uint8Array(Buffer.from(deposit.message.slice(2), "hex")),
}),
repaymentChainId: some(BigInt(repaymentChainId)),
repaymentAddress: some(relayerAddress.toV2Address()),
});
}

/**
* @param mint Address of the token corresponding to the account being made.
* @param relayer Address of the relayer filling the deposit.
* @returns An instruction for creating a new token account.
*/
export function createTokenAccountsInstruction(
mint: SvmAddress,
relayer: TransactionSigner<string>
): SvmSpokeClient.CreateTokenAccountsInstruction {
return SvmSpokeClient.getCreateTokenAccountsInstruction({
signer: relayer,
mint: mint.toV2Address(),
});
}

/**
* @param mint Address of the token corresponding to the account being made.
* @param amount Amount of the token to approve.
* @param relayer Address of the relayer filling the deposit.
* @param spokePool Address (program ID) of the SvmSpoke.
* @returns A token approval instruction.
*/
export async function createApproveInstruction(
mint: SvmAddress,
amount: BigNumber,
relayer: SvmAddress,
spokePool: SvmAddress,
mintDecimals?: number
) {
const [relayerTokenAccount, statePda] = await Promise.all([
getAssociatedTokenAddress(relayer, mint, TOKEN_PROGRAM_ADDRESS),
getStatePda(spokePool.toV2Address()),
]);

// If no mint decimals were supplied, then assign it to whatever value we have in TOKEN_SYMBOLS_MAP.
// If this token is not in TOKEN_SYMBOLS_MAP, then throw an error.
mintDecimals ??= getTokenInformationFromAddress(mint.toBase58())?.decimals;
if (!isDefined(mintDecimals)) {
throw new Error(`No mint decimals found for token ${mint.toBase58()}`);
}

return getApproveCheckedInstruction({
source: relayerTokenAccount,
mint: mint.toV2Address(),
delegate: statePda,
owner: relayer.toV2Address(),
amount: amount.toBigInt(),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Neat - TIL!

decimals: mintDecimals,
});
}

export async function getAssociatedTokenAddress(
owner: SvmAddress,
mint: SvmAddress,
tokenProgramId: Address<string> = TOKEN_PROGRAM_ADDRESS
): Promise<Address<string>> {
const [associatedToken] = await getProgramDerivedAddress({
programAddress: ASSOCIATED_TOKEN_PROGRAM_ADDRESS,
seeds: [owner.toBuffer(), SvmAddress.from(tokenProgramId).toBuffer(), mint.toBuffer()],
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the user of Buffer here a hangover from the contracts implementation? Seeds in Kit have to conform to type Seed = ReadonlyUint8Array | string;, so it should be OK to pass strings in; I think this should work:

Suggested change
seeds: [owner.toBuffer(), SvmAddress.from(tokenProgramId).toBuffer(), mint.toBuffer()],
seeds: [owner.toAddress(), tokenProgramId, mint.toAddress()],

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm actually not too sure how the string encoding is supposed to work here, but each character is treated as a byte, so passing in a string here will have getProgramDerivedAddress interpret the input as 66 byte strings, not 32 bytes.

});
return associatedToken;
}

export async function getFillStatusPda(
relayDataHash: string,
spokePool: Address<string> = SvmSpokeClient.SVM_SPOKE_PROGRAM_ADDRESS
): Promise<Address<string>> {
const [fillStatusPda] = await getProgramDerivedAddress({
programAddress: spokePool,
seeds: [Buffer.from("fills"), Buffer.from(relayDataHash.slice(2), "hex")],
});
return fillStatusPda;
}

export async function getEventAuthority(
spokePool: Address<string> = SvmSpokeClient.SVM_SPOKE_PROGRAM_ADDRESS,
extraSeeds: string[] = []
): Promise<Address<string>> {
const [eventAuthority] = await getProgramDerivedAddress({
programAddress: spokePool,
seeds: [Buffer.from("__event_authority"), ...extraSeeds],
});
return eventAuthority;
}

export function getRelayDataHash(relayData: RelayData, destinationChainId: number): string {
const toBuffer = (hex: string, byteLength: number, littleEndian: boolean = true) => {
const buffer = Buffer.from(hex.slice(2), "hex");
if (buffer.length < byteLength) {
const zeroPad = Buffer.alloc(byteLength);
buffer.copy(zeroPad, byteLength - buffer.length);
return littleEndian ? zeroPad.reverse() : zeroPad;
}
return littleEndian ? buffer.slice(0, byteLength).reverse() : buffer.slice(0, byteLength);
};
const contentToHash = Buffer.concat([
toBuffer(relayData.depositor, 32, false),
toBuffer(relayData.recipient, 32, false),
toBuffer(relayData.exclusiveRelayer, 32, false),
toBuffer(relayData.inputToken, 32, false),
toBuffer(relayData.outputToken, 32, false),
toBuffer(relayData.inputAmount.toHexString(), 8),
toBuffer(relayData.outputAmount.toHexString(), 8),
toBuffer(toBN(relayData.originChainId).toHexString(), 8),
toBuffer(relayData.depositId.toHexString(), 32, false),
toBuffer(toBN(relayData.fillDeadline).toHexString(), 4),
toBuffer(toBN(relayData.exclusivityDeadline).toHexString(), 4),
toBuffer(getMessageHash(relayData.message), 32, false),
toBuffer(toBN(destinationChainId).toHexString(), 8),
]);
return keccak256(contentToHash);
}
23 changes: 22 additions & 1 deletion src/arch/svm/utils.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,28 @@
import { BN, BorshEventCoder, Idl } from "@coral-xyz/anchor";
import web3, { address, getProgramDerivedAddress, getU64Encoder, Address, RpcTransport } from "@solana/kit";
import web3, {
address,
getProgramDerivedAddress,
getU64Encoder,
Address,
RpcTransport,
type TransactionSigner,
} from "@solana/kit";
import { EventName, SVMEventNames } from "./types";

/**
* Basic void TransactionSigner type
*/
export const SolanaVoidSigner: (simulationAddress: string) => TransactionSigner<string> = (
simulationAddress: string
) => {
return {
address: address(simulationAddress),
signAndSendTransactions: async () => {
return await Promise.resolve([]);
},
};
};

/**
* Helper to determine if the current RPC network is devnet.
*/
Expand Down
7 changes: 5 additions & 2 deletions src/gasPriceOracle/adapters/arbitrum.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { providers } from "ethers";
import { bnOne } from "../../utils";
import { GasPriceEstimate } from "../types";
import { EvmGasPriceEstimate } from "../types";
import * as ethereum from "./ethereum";
import { GasPriceEstimateOptions } from "../oracle";

Expand All @@ -16,7 +16,10 @@ import { GasPriceEstimateOptions } from "../oracle";
* function.
* @returns GasPriceEstimate
*/
export async function eip1559(provider: providers.Provider, opts: GasPriceEstimateOptions): Promise<GasPriceEstimate> {
export async function eip1559(
provider: providers.Provider,
opts: GasPriceEstimateOptions
): Promise<EvmGasPriceEstimate> {
const { maxFeePerGas: _maxFeePerGas, maxPriorityFeePerGas } = await ethereum.eip1559(provider, opts);

// eip1559() sets maxFeePerGas = lastBaseFeePerGas + maxPriorityFeePerGas, so back out priority fee.
Expand Down
11 changes: 7 additions & 4 deletions src/gasPriceOracle/adapters/ethereum.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import assert from "assert";
import { providers } from "ethers";
import { BigNumber, bnZero, fixedPointAdjustment, getNetworkName, parseUnits } from "../../utils";
import { GasPriceEstimate } from "../types";
import { EvmGasPriceEstimate } from "../types";
import { gasPriceError } from "../util";
import { GasPriceEstimateOptions } from "../oracle";

Expand All @@ -13,7 +13,7 @@ import { GasPriceEstimateOptions } from "../oracle";
* @param priorityFeeMultiplier Amount to multiply priority fee or unused for legacy gas pricing.
* @returns Promise of gas price estimate object.
*/
export function eip1559(provider: providers.Provider, opts: GasPriceEstimateOptions): Promise<GasPriceEstimate> {
export function eip1559(provider: providers.Provider, opts: GasPriceEstimateOptions): Promise<EvmGasPriceEstimate> {
return eip1559Raw(provider, opts.chainId, opts.baseFeeMultiplier, opts.priorityFeeMultiplier);
}

Expand All @@ -29,7 +29,7 @@ export async function eip1559Raw(
chainId: number,
baseFeeMultiplier: BigNumber,
priorityFeeMultiplier: BigNumber
): Promise<GasPriceEstimate> {
): Promise<EvmGasPriceEstimate> {
const [{ baseFeePerGas }, _maxPriorityFeePerGas] = await Promise.all([
provider.getBlock("pending"),
(provider as providers.JsonRpcProvider).send("eth_maxPriorityFeePerGas", []),
Expand All @@ -54,7 +54,10 @@ export async function eip1559Raw(
* @dev Its recommended to use the eip1559Raw method over this one where possible as it will be more accurate.
* @returns GasPriceEstimate
*/
export async function legacy(provider: providers.Provider, opts: GasPriceEstimateOptions): Promise<GasPriceEstimate> {
export async function legacy(
provider: providers.Provider,
opts: GasPriceEstimateOptions
): Promise<EvmGasPriceEstimate> {
const { chainId, baseFeeMultiplier } = opts;
const gasPrice = await provider.getGasPrice();

Expand Down
4 changes: 3 additions & 1 deletion src/gasPriceOracle/adapters/linea-viem.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { PopulatedTransaction } from "ethers";
import { Address, Hex, PublicClient } from "viem";
import { estimateGas } from "viem/linea";
import { DEFAULT_SIMULATED_RELAYER_ADDRESS as account } from "../../constants";
Expand Down Expand Up @@ -26,7 +27,8 @@ export async function eip1559(
provider: PublicClient,
opts: GasPriceEstimateOptions
): Promise<InternalGasPriceEstimate> {
const { unsignedTx, priorityFeeMultiplier } = opts;
const { unsignedTx: _unsignedTx, priorityFeeMultiplier } = opts;
const unsignedTx = _unsignedTx as PopulatedTransaction; // Cast the opaque unsignedTx type to an ethers PopulatedTransaction.
const { baseFeePerGas, priorityFeePerGas: _priorityFeePerGas } = await estimateGas(provider, {
account: (unsignedTx?.from as Address) ?? account,
to: (unsignedTx?.to as Address) ?? account,
Expand Down
12 changes: 6 additions & 6 deletions src/gasPriceOracle/adapters/polygon.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { providers } from "ethers";
import { BaseHTTPAdapter, BaseHTTPAdapterArgs } from "../../priceClient/adapters/baseAdapter";
import { BigNumber, bnZero, fixedPointAdjustment, isDefined, parseUnits } from "../../utils";
import { CHAIN_IDs } from "../../constants";
import { GasPriceEstimate } from "../types";
import { EvmGasPriceEstimate } from "../types";
import { gasPriceError } from "../util";
import { eip1559 } from "./ethereum";
import { GasPriceEstimateOptions } from "../oracle";
Expand Down Expand Up @@ -38,7 +38,7 @@ export class PolygonGasStation extends BaseHTTPAdapter {
this.chainId = chainId;
}

async getFeeData(strategy: "safeLow" | "standard" | "fast" = "fast"): Promise<GasPriceEstimate> {
async getFeeData(strategy: "safeLow" | "standard" | "fast" = "fast"): Promise<EvmGasPriceEstimate> {
const gas = await this.query("v2", {});

const gasPrice = (gas as GasStationV2Response)?.[strategy];
Expand Down Expand Up @@ -69,7 +69,7 @@ export class PolygonGasStation extends BaseHTTPAdapter {
}

class MockRevertingPolygonGasStation extends PolygonGasStation {
getFeeData(): Promise<GasPriceEstimate> {
getFeeData(): Promise<EvmGasPriceEstimate> {
throw new Error();
}
}
Expand All @@ -78,7 +78,7 @@ export const MockPolygonGasStationBaseFee = () => parseUnits("12", 9);
export const MockPolygonGasStationPriorityFee = () => parseUnits("1", 9);

class MockPolygonGasStation extends PolygonGasStation {
getFeeData(): Promise<GasPriceEstimate> {
getFeeData(): Promise<EvmGasPriceEstimate> {
return Promise.resolve({
maxPriorityFeePerGas: MockPolygonGasStationPriorityFee(),
maxFeePerGas: MockPolygonGasStationBaseFee().add(MockPolygonGasStationPriorityFee()),
Expand All @@ -90,12 +90,12 @@ class MockPolygonGasStation extends PolygonGasStation {
* @notice Returns the gas price suggested by the Polygon GasStation API or reconstructs it using
* the eip1559() method as a fallback.
* @param provider Ethers Provider.
* @returns GasPriceEstimate
* @returns EvmGasPriceEstimate
*/
export async function gasStation(
provider: providers.Provider,
opts: GasPriceEstimateOptions
): Promise<GasPriceEstimate> {
): Promise<EvmGasPriceEstimate> {
const { chainId, baseFeeMultiplier, priorityFeeMultiplier } = opts;
let gasStation: PolygonGasStation;
if (process.env.TEST_POLYGON_GAS_STATION === "true") {
Expand Down
Loading