Skip to content
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

Crosschainswaps V3 #71

Merged
merged 14 commits into from
May 2, 2024
Merged
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
.idea
.idea/**
sdk/*.tgz
node_modules/
7 changes: 5 additions & 2 deletions sdk/package.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"version": "0.18.0",
"version": "0.19.0",
"name": "@rainbow-me/swaps",
"license": "GPL-3.0",
"main": "dist/index.js",
Expand All @@ -13,7 +13,7 @@
},
"scripts": {
"start": "tsdx watch",
"build": "tsdx build",
"build": "tsdx build --ignore-pattern tests/*",
"test": "tsdx test --passWithNoTests",
"lint": "eslint . --ext js,ts,jsx,tsx",
"prepare": "tsdx build",
Expand Down Expand Up @@ -83,5 +83,8 @@
"**/glob-parent": "5.1.2",
"**/ws": "7.4.6",
"**/ansi-regex": "5.0.1"
},
"jest": {
"testEnvironment": "node"
}
}
52 changes: 45 additions & 7 deletions sdk/src/quotes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,14 @@ import {
PERMIT_EXPIRATION_TS,
RAINBOW_ROUTER_CONTRACT_ADDRESS,
RAINBOW_ROUTER_CONTRACT_ADDRESS_ZORA,
SOCKET_GATEWAY_CONTRACT_ADDRESSESS,
WRAPPED_ASSET,
} from './utils/constants';
import { signPermit } from './utils/permit';
import { getReferrerCode } from './utils/referrer';
import {
extractDestinationAddress,
sanityCheckAddress,
} from './utils/sanity_check';

/**
* Function to get the rainbow router contract address based on the chainId
Expand Down Expand Up @@ -148,7 +151,7 @@ const buildRainbowCrosschainQuoteUrl = ({
swapType: SwapType.crossChain,
toChainId: String(toChainId),
});
return `${API_BASE_URL}/v1/quote?bridgeVersion=2&` + searchParams.toString();
return `${API_BASE_URL}/v1/quote?bridgeVersion=3&` + searchParams.toString();
};

/**
Expand Down Expand Up @@ -278,7 +281,9 @@ export const getQuote = async (
* @param {BigNumberish} params.sellAmount
* @param {number} params.slippage
* @param {boolean} params.refuel
* @returns {Promise<CrosschainQuote | null>}
* @returns {Promise<CrosschainQuote | QuoteError | null>} returns error in case the request failed or the
* destination address is not consistent with the SDK's
* stored destination address
*/
export const getCrosschainQuote = async (
params: QuoteParams
Expand Down Expand Up @@ -316,8 +321,24 @@ export const getCrosschainQuote = async (
}

const quoteWithRestrictedAllowanceTarget = quote as CrosschainQuote;
quoteWithRestrictedAllowanceTarget.allowanceTarget =
SOCKET_GATEWAY_CONTRACT_ADDRESSESS.get(chainId);
try {
const { expectedAddress, shouldOverride } = sanityCheckAddress(
quoteWithRestrictedAllowanceTarget.source,
quoteWithRestrictedAllowanceTarget.chainId,
quoteWithRestrictedAllowanceTarget.allowanceTarget
);
if (shouldOverride) {
quoteWithRestrictedAllowanceTarget.allowanceTarget = expectedAddress;
}
} catch (e) {
return {
error: true,
message:
e instanceof Error
? e.message
: `unexpected error happened while checking crosschain quote's address: ${quoteWithRestrictedAllowanceTarget.allowanceTarget}`,
} as QuoteError;
}

return quoteWithRestrictedAllowanceTarget;
};
Expand Down Expand Up @@ -490,7 +511,15 @@ export const fillCrosschainQuote = async (
): Promise<Transaction> => {
const { data, from, value } = quote;

const to = SOCKET_GATEWAY_CONTRACT_ADDRESSESS.get(quote.fromChainId);
let to = quote.to;
const { expectedAddress, shouldOverride } = sanityCheckAddress(
quote.source,
quote.fromChainId,
extractDestinationAddress(quote)
);
if (shouldOverride) {
to = expectedAddress;
}

let txData = data;
if (referrer) {
Expand Down Expand Up @@ -589,7 +618,16 @@ export const getCrosschainQuoteExecutionDetails = (
provider: StaticJsonRpcProvider
): CrosschainQuoteExecutionDetails => {
const { from, data, value } = quote;
const to = SOCKET_GATEWAY_CONTRACT_ADDRESSESS.get(quote.fromChainId);

let to = quote.to;
const { expectedAddress, shouldOverride } = sanityCheckAddress(
quote.source,
quote.fromChainId,
extractDestinationAddress(quote)
);
if (shouldOverride) {
to = expectedAddress;
}

return {
method: provider.estimateGas({
Expand Down
6 changes: 6 additions & 0 deletions sdk/src/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,13 @@ export enum ChainId {
export enum Source {
Aggregator0x = '0x',
Aggregator1inch = '1inch',
AggregatorRainbow = 'rainbow',
// DEPRECATED: Use Aggregator1inch instead
Aggregotor1inch = '1inch',

// Crosschain
CrosschainAggregatorSocket = 'socket',
CrosschainAggregatorRelay = 'relay',
}

export enum SwapType {
Expand Down Expand Up @@ -234,6 +239,7 @@ export interface CrosschainQuote extends Quote {
allowanceTarget?: string;
routes: SocketRoute[];
refuel: SocketRefuelData | null;
no_approval: boolean | undefined;
}

export interface TransactionOptions {
Expand Down
6 changes: 6 additions & 0 deletions sdk/src/utils/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,12 @@ export const SOCKET_GATEWAY_CONTRACT_ADDRESSESS = new Map([
[ChainId.blast, '0x3a23F943181408EAC424116Af7b7790c94Cb97a5'],
]);

// RELAY_LINK_BRIDGING_RELAYER_ADDRESS is the EOA used by relay link as relayer on all chains
export const RELAY_LINK_BRIDGING_RELAYER_ADDRESS =
'0xf70da97812CB96acDF810712Aa562db8dfA3dbEF';

export const ERC20_TRANSFER_SIGNATURE = `0xa9059cbb`;

export type MultiChainAsset = {
[key: string]: EthereumAddress;
};
Expand Down
167 changes: 167 additions & 0 deletions sdk/src/utils/sanity_check.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
import { ChainId, CrosschainQuote, Source } from '../types';
import {
ERC20_TRANSFER_SIGNATURE,
ETH_ADDRESS,
RELAY_LINK_BRIDGING_RELAYER_ADDRESS,
SOCKET_GATEWAY_CONTRACT_ADDRESSESS,
} from './constants';

/**
* Sanity checks the quote's returned address against the expected address stored in the SDK.
* This function ensures the integrity and correctness of the destination address provided by the quote source.
*
* @param quoteSource The aggregator used for the quote.
* @param chainID The origin network chain ID for the quote.
* @param assertedAddress The destination address provided by the quote.
* @returns {string, boolean} The destination address stored in the SDK for the provided (source, chainID) combination.
* And if it should be overridden in the quote.
* @throws {Error} Throws an error if any of the following conditions are met:
* - The quote's destination address is undefined.
* - No destination address is defined in the SDK for the provided (source, chainID) combination.
* - The provided quote's destination address does not case-insensitively match the SDK's stored destination address.
*/
export function sanityCheckAddress(
quoteSource: Source | undefined,
chainID: ChainId,
assertedAddress: string | undefined
): {
expectedAddress: string;
shouldOverride: boolean;
} {
if (assertedAddress === undefined || assertedAddress === '') {
throw new Error(
`quote's destination addresses must be defined (API Response)`
);
}
const { expectedAddress, shouldOverride } = getExpectedDestinationAddress(
quoteSource,
chainID
);
if (expectedAddress === undefined || expectedAddress === '') {
throw new Error(
`expected source ${quoteSource}'s destination address on chainID ${chainID} must be defined (Swap SDK)`
);
}
if (expectedAddress.toLowerCase() !== assertedAddress?.toLowerCase()) {
throw new Error(
`source ${quoteSource}'s destination address '${assertedAddress}' on chainID ${chainID} is not consistent, expected: '${expectedAddress}'`
);
}
return { expectedAddress, shouldOverride };
}

/**
* Retrieves the destination address from a cross-chain quote object, returning undefined
* when the quote source is not known or the quote does not contain a valid destination address.
*
* @param quote The cross-chain quote object returned by the API.
*
* @returns The destination address as a string if available.
* Returns undefined if the quote does not properly specify a destination.
*
* @example
* // Example for a quote from socket
* const quoteSocket = {
* to: '0x1234567890123456789012345678901234567890',
* data: '0x...',
* sellTokenAddress: '0x...'
* };
* console.log(getToAddressFromCrosschainQuote(Source.CrosschainAggregatorSocket, quoteSocket));
* // Output: '0x1234567890123456789012345678901234567890'
*
* // Example for a quote from CrosschainAggregatorRelay where the sell token is ETH
* const quoteRelayETH = {
* to: '0xabcdefabcdefabcdefabcdefabcdefabcdefabcdef',
* data: '0x...',
* sellTokenAddress: '0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE'
* };
* console.log(getToAddressFromCrosschainQuote(Source.CrosschainAggregatorRelay, quoteRelayETH));
* // Output: '0xabcdefabcdefabcdefabcdefabcdefabcdefabcdef'
*
* // Example for a quote from CrosschainAggregatorRelay where the sell token is not ETH
* const quoteRelayERC20 = {
* to: '0xabcdefabcdefabcdefabcdefabcdefabcdefabcdef',
* data: '0xa9059cbb000000000000000000000000f70da97812cb96acdf810712aa562db8dfa3dbef...',
* sellTokenAddress: '0xabcdefabcdefabcdefabcdefabcdefabcdefabcdef'
* };
* console.log(getToAddressFromCrosschainQuote(Source.CrosschainAggregatorRelay, quoteRelayERC20));
* // Output: '0xf70da97812cb96acdf810712aa562db8dfa3dbef' (assuming the call data was a ERC20 transfer)
*/
export function extractDestinationAddress(
quote: CrosschainQuote
): string | undefined {
const quoteSource = quote.source;
const validQuoteSource = quoteSource !== undefined;
if (validQuoteSource && quoteSource === Source.CrosschainAggregatorSocket) {
return quote.to;
}
if (validQuoteSource && quoteSource === Source.CrosschainAggregatorRelay) {
if (quote.sellTokenAddress?.toLowerCase() === ETH_ADDRESS.toLowerCase()) {
return quote.to;
}
return decodeERC20TransferToData(quote.data);
}
return undefined;
}

/**
* Decodes the ERC-20 token transfer data from a transaction's input data.
* This function expects the input data to start with the ERC-20 transfer method ID (`0xa9059cbb`),
* followed by the 64 hexadecimal characters for the destination address and 64 hexadecimal characters
* for the transfer amount. The function will check and parse the input data, extracting the recipient's address
*
* The method assumes the data is properly formatted and begins with the correct method ID.
* If the data does not conform to these expectations, the function will return an 'undefined' object.
*
* @param data The hex encoded input data string from an ERC-20 transfer transaction. This string
* should include the method ID followed by the encoded parameters (address and amount).
*
* @returns { string | undefined } The destination address. If any error happens.
* Returns 'undefined' if it could not decode the call data.
*/
export function decodeERC20TransferToData(
data: string | undefined
): string | undefined {
if (!data?.startsWith(ERC20_TRANSFER_SIGNATURE)) {
fringlesinthestreet marked this conversation as resolved.
Show resolved Hide resolved
return undefined;
}
const paramsData = data.slice(ERC20_TRANSFER_SIGNATURE.length);
if (paramsData.length < 64 * 2) {
return undefined;
}
return `0x${paramsData.slice(0, 64).replace(/^0+/, '')}`;
}

/**
* Retrieves the destination address stored in the SDK corresponding to the specified aggregator and chain ID.
*
* @param quoteSource The aggregator used for the quote.
* @param chainID The origin network chain ID for the quote.
* @returns {string | undefined, boolean} The destination address stored in the SDK for the provided (source, chainID)
* combination and if we need to overwrite it on the quote.
* Returns `undefined` if there is no address for the specified combination.
*/
export function getExpectedDestinationAddress(
quoteSource: Source | undefined,
chainID: ChainId
): {
expectedAddress: string | undefined;
shouldOverride: boolean;
} {
const validSource = quoteSource !== undefined;
if (validSource && quoteSource === Source.CrosschainAggregatorSocket) {
return {
expectedAddress: SOCKET_GATEWAY_CONTRACT_ADDRESSESS.get(chainID),
shouldOverride: true,
};
} else if (validSource && quoteSource === Source.CrosschainAggregatorRelay) {
return {
expectedAddress: RELAY_LINK_BRIDGING_RELAYER_ADDRESS,
shouldOverride: false,
};
}
return {
expectedAddress: undefined,
shouldOverride: false,
};
}