Skip to content
Open
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
1 change: 1 addition & 0 deletions relayer/spy_relayer/.env.sample
Original file line number Diff line number Diff line change
Expand Up @@ -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
6 changes: 3 additions & 3 deletions relayer/spy_relayer/src/__tests__/backends/swim/listener.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand All @@ -52,7 +52,7 @@ describe("validate", () => {
targetChainRecipient: convertAddressToUint8Array(targetChainRecipientStr, CHAIN_ID_SOLANA),
propellerEnabled: true,
gasKickstartEnabled: true,
maxSwimUSDFee: 1000n,
maxSwimUSDFee: 10001n,
swimTokenNumber: 1,
memoId: memoId
};
Expand Down Expand Up @@ -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");
});
});

Expand Down
2 changes: 2 additions & 0 deletions relayer/spy_relayer/src/__tests__/consts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -54,3 +55,4 @@ describe("consts should exist", () => {
).resolves.toBeTruthy();
});
});
*/
26 changes: 23 additions & 3 deletions relayer/spy_relayer/src/__tests__/testUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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;
Expand Down
23 changes: 21 additions & 2 deletions relayer/spy_relayer/src/backends/swim/listener.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ export class SwimListener implements Listener {
async verifyIsRateLimited(payload: ParsedTransferWithArbDataPayload<ParsedSwimData>): Promise<boolean> {
let env = getListenerEnvironment();
if (env.requestLimit < 0) {
return false;
return true;
}

const baseKey = payload.extraPayload.targetChainRecipient;
Expand All @@ -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<ParsedSwimData>): boolean {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

1/ Should we log anything if we reject an invalid maxSwimUSDFee?
2/ Can you add a TODO here that vaguely denotes all the conditions we wanna check (doesn't have to be super specific or anything). In an ideal world, we'll want to test this method for all conditions.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

  1. I can add a debug log
  2. Yeah i'll add a TODO

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
Expand Down Expand Up @@ -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;
Expand Down
2 changes: 1 addition & 1 deletion relayer/spy_relayer/src/configureEnv.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down
1 change: 1 addition & 0 deletions relayer/spy_relayer/src/relayer/evm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 3 additions & 3 deletions relayer/spy_relayer/src/utils/swim.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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};
};
};