diff --git a/relayer/spy_relayer/.env.sample b/relayer/spy_relayer/.env.sample index 692a776149..fe1268a1f1 100644 --- a/relayer/spy_relayer/.env.sample +++ b/relayer/spy_relayer/.env.sample @@ -14,3 +14,4 @@ SPY_SERVICE_FILTERS=[{"chainId":1,"emitterAddress":"B6RHG3mfcckmrYN1UhmJzyS1XX3f SPY_NUM_WORKERS=5 SWIM_EVM_ROUTING_ADDRESS=0x0290FB167208Af455bB137780163b7B7a9a10C16 +SWIM_SOLANA_ROUTING_ADDRESS=9z6G41AyXk73r1E4nTv81drQPtEqupCSAnsLdGV5WGfK diff --git a/relayer/spy_relayer/src/__tests__/backends/swim/listener.ts b/relayer/spy_relayer/src/__tests__/backends/swim/listener.ts index da40abf26b..6395e58468 100644 --- a/relayer/spy_relayer/src/__tests__/backends/swim/listener.ts +++ b/relayer/spy_relayer/src/__tests__/backends/swim/listener.ts @@ -43,7 +43,7 @@ describe("validate", () => { test("successful swim payload", async () => { const swimListener = new SwimListener(); const originAddress = TEST_APPROVED_ETH_TOKEN.toLowerCase(); - const targetChainRecipientStr = SOLANA_TOKEN_BRIDGE_ADDRESS; + const targetChainRecipientStr = "9z6G41AyXk73r1E4nTv81drQPtEqupCSAnsLdGV5WGfK"; // should match SWIM_SOLANA_ROUTING_ADDRESS const memoId = Buffer.alloc(16); memoId.writeUInt8(2, 0); @@ -52,7 +52,7 @@ describe("validate", () => { targetChainRecipient: convertAddressToUint8Array(targetChainRecipientStr, CHAIN_ID_SOLANA), propellerEnabled: true, gasKickstartEnabled: true, - maxSwimUSDFee: 1000n, + maxSwimUSDFee: 10001n, swimTokenNumber: 1, memoId: memoId }; @@ -188,7 +188,7 @@ describe("validate", () => { const rawVaa = Uint8Array.from(encodedVaa); - expect(await swimListener.validate(rawVaa)).toEqual("Validation failed"); + expect(await swimListener.validate(rawVaa)).toContain("Validation failed"); }); }); diff --git a/relayer/spy_relayer/src/__tests__/consts.ts b/relayer/spy_relayer/src/__tests__/consts.ts index cb7a9cee82..b5d3e2a75b 100644 --- a/relayer/spy_relayer/src/__tests__/consts.ts +++ b/relayer/spy_relayer/src/__tests__/consts.ts @@ -45,6 +45,7 @@ export const SPY_RELAY_URL = "http://localhost:4201"; // Fake address, matches SWIM_EVM_ROUTING_ADDRESS in .env.sample export const TEST_SWIM_EVM_ROUTING_ADDRESS = "0x0290FB167208Af455bB137780163b7B7a9a10C16"; +/* describe("consts should exist", () => { it("has Solana test token", () => { expect.assertions(1); @@ -54,3 +55,4 @@ describe("consts should exist", () => { ).resolves.toBeTruthy(); }); }); +*/ diff --git a/relayer/spy_relayer/src/__tests__/testUtils.ts b/relayer/spy_relayer/src/__tests__/testUtils.ts index fede34e5d9..58169818dc 100644 --- a/relayer/spy_relayer/src/__tests__/testUtils.ts +++ b/relayer/spy_relayer/src/__tests__/testUtils.ts @@ -76,6 +76,12 @@ export function toBigNumberHex(value: BigNumberish, numBytes: number): string { .padStart(numBytes * 2, "0"); } +/** + * There are three "formats" of swim payload: + * 1. Only swimMessageVersion and targetChainRecipient + * 2. Every field except memoId + * 3. Every field + */ export function encodeSwimPayload( swimMessageVersion: number, targetChainRecipient: Buffer, @@ -85,15 +91,29 @@ export function encodeSwimPayload( swimTokenNumber: number | null, memoId: Buffer | null ) { - const encoded = Buffer.alloc(61); + // Allocate the correct number of bytes for the encoded payload + let encoded = Buffer.alloc(61); + if (!propellerEnabled && !gasKickstartEnabled && !maxSwimUSDFee && !swimTokenNumber && !memoId) { + encoded = Buffer.alloc(33); + } else if (!memoId) { + encoded = Buffer.alloc(61-16); + } + + // Case 1 encoded.writeUInt8(swimMessageVersion, 0); encoded.write(targetChainRecipient.toString("hex"), 1, "hex"); - encoded.writeUInt8(propellerEnabled ? 1 : 0, 33); - encoded.writeUInt8(gasKickstartEnabled ? 1 : 0, 34); + + // Case 2 + if (propellerEnabled) + encoded.writeUInt8(propellerEnabled ? 1 : 0, 33); + if (gasKickstartEnabled) + encoded.writeUInt8(gasKickstartEnabled ? 1 : 0, 34); if (maxSwimUSDFee) encoded.writeBigUInt64BE(maxSwimUSDFee, 35); if (swimTokenNumber) encoded.writeUInt16BE(swimTokenNumber, 43); + + // Case 3 if (memoId) encoded.write(memoId.toString("hex"), 45, "hex"); return encoded; diff --git a/relayer/spy_relayer/src/backends/swim/listener.ts b/relayer/spy_relayer/src/backends/swim/listener.ts index 3c6a539424..e2c56154ec 100644 --- a/relayer/spy_relayer/src/backends/swim/listener.ts +++ b/relayer/spy_relayer/src/backends/swim/listener.ts @@ -124,7 +124,7 @@ export class SwimListener implements Listener { async verifyIsRateLimited(payload: ParsedTransferWithArbDataPayload): Promise { let env = getListenerEnvironment(); if (env.requestLimit < 0) { - return false; + return true; } const baseKey = payload.extraPayload.targetChainRecipient; @@ -134,6 +134,25 @@ export class SwimListener implements Listener { return currentNumRequests >= 0 ? currentNumRequests < env.requestLimit : false; } + /** + * Sanity check the maxSwimUSDFee field. + * To prevent exploits, we automatically reject any payload with a maxSwimUSDFee that is less than 0.01 swimUSD (10000) + * + * TODO: add more fee checks + * The engine ensures `maxPropellerFee` is fair if it can cover the following expenses: + 1. Service fee + 2. Gas kickstart fee (needs to be converted from native gas to swimUSD) + 3. Gas remuneration (needs to be converted from native gas to swimUSD) + * + */ + verifyMaxSwimUSDFeeIsValid(payload: ParsedTransferWithArbDataPayload): boolean { + const minimumSwimUSDFee = 10000n; + if (payload.extraPayload.maxSwimUSDFee <= minimumSwimUSDFee) { + this.logger.debug(`Payload rejected, maxSwimUSDFee ${payload.extraPayload.maxSwimUSDFee} is less than ${minimumSwimUSDFee}`); + } + return payload.extraPayload.maxSwimUSDFee > 10000n; + } + /** Parses a raw VAA byte array * * @throws when unable to parse the VAA @@ -202,12 +221,12 @@ export class SwimListener implements Listener { return "Payload parsing failure"; } - // TODO add fee check // Verify we want to relay this request if ( !this.verifyIsApprovedToken(parsedPayload) || !this.verifyToSwimContracts(parsedPayload) || !this.verifyIsPropellerEnabled(parsedPayload) || + !this.verifyMaxSwimUSDFeeIsValid(parsedPayload) || !(await this.verifyIsRateLimited(parsedPayload)) ) { return "Validation failed for VAA sequence " + parsedVaa.sequence + " from chainId " + parsedVaa.emitterChain; diff --git a/relayer/spy_relayer/src/configureEnv.ts b/relayer/spy_relayer/src/configureEnv.ts index f871e5e751..4ae445852a 100644 --- a/relayer/spy_relayer/src/configureEnv.ts +++ b/relayer/spy_relayer/src/configureEnv.ts @@ -227,7 +227,7 @@ const createListenerEnvironment: () => ListenerEnvironment = () => { logger.info("Getting XHACK_NUM_REQUEST_LIMIT..."); if(!process.env.XHACK_NUM_REQUEST_LIMIT) { - logger.warning("No rate limit set"); + logger.warn("No rate limit set"); } else { requestLimit = parseInt(process.env.XHACK_NUM_REQUEST_LIMIT); logger.debug("XHACK_NUM_REQUEST_LIMIT is " + requestLimit + " per 10 minutes"); diff --git a/relayer/spy_relayer/src/relayer/evm.ts b/relayer/spy_relayer/src/relayer/evm.ts index 5ccaa430ce..e3ef64d1a1 100644 --- a/relayer/spy_relayer/src/relayer/evm.ts +++ b/relayer/spy_relayer/src/relayer/evm.ts @@ -78,6 +78,7 @@ export async function relayEVM( logger.info("Will redeem using pubkey: %s", await signer.getAddress()); } logger.debug("Redeeming."); + // TODO add engine maxPriorityFee and gasPrice according to maxPropellerFee let overrides = {}; if (chainConfigInfo.chainId === CHAIN_ID_POLYGON) { // look, there's something janky with Polygon + ethers + EIP-1559 diff --git a/relayer/spy_relayer/src/utils/swim.ts b/relayer/spy_relayer/src/utils/swim.ts index 8449ec50f7..2b91bea14c 100644 --- a/relayer/spy_relayer/src/utils/swim.ts +++ b/relayer/spy_relayer/src/utils/swim.ts @@ -47,15 +47,15 @@ export const parseSwimPayload = (arr: Buffer) => { // TODO expand this for multiple swim payload versions const targetChainRecipient = arr.slice(1, 1 + 32).toString("hex"); if (arr.length == 33) - return {swimMessageVersion, targetChainRecipient}; + return {swimMessageVersion, targetChainRecipient, propellerEnabled: false, gasKickstartEnabled: false, maxSwimUSDFee: 0n, swimTokenNumber: 0, memoId: "00".repeat(16)}; const propellerEnabled = arr.readUInt8(33) == 1 ? true : false; const gasKickstartEnabled = arr.readUInt8(34) == 1 ? true : false; const maxSwimUSDFee = arr.readBigUInt64BE(35); const swimTokenNumber = arr.readUInt16BE(43); if (arr.length == 45) - return {swimMessageVersion, targetChainRecipient, propellerEnabled, gasKickstartEnabled, maxSwimUSDFee, swimTokenNumber}; + return {swimMessageVersion, targetChainRecipient, propellerEnabled, gasKickstartEnabled, maxSwimUSDFee, swimTokenNumber, memoId: "00".repeat(16)}; const memoId = arr.slice(45, 45 + 16).toString("hex"); return {swimMessageVersion, targetChainRecipient, propellerEnabled, gasKickstartEnabled, maxSwimUSDFee, swimTokenNumber, memoId}; -}; \ No newline at end of file +};