Skip to content

Commit

Permalink
refactor:[SMR-2723] generate destination execute call (#1763)
Browse files Browse the repository at this point in the history
  • Loading branch information
Benjimmutable authored May 12, 2024
1 parent 4a0c7c2 commit dbb0c3f
Show file tree
Hide file tree
Showing 4 changed files with 136 additions and 50 deletions.
44 changes: 44 additions & 0 deletions packages/internal/bridge/sdk/src/lib/axelar.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { ethers } from 'ethers';
import { getWithdrawRootToken } from './axelarUtils';

const rootToken = '0x1f9090aaE28b8a3dCeaDf281B0F12828e676c326';
describe('Axelar', () => {
const mockERC20Contract = {
allowance: jest.fn(),
interface: {
encodeFunctionData: jest.fn(),
},
rootToken: jest.fn().mockImplementation(async () => rootToken),
};
describe('getWithdrawRootToken', () => {
beforeEach(() => {
jest.spyOn(ethers, 'Contract').mockReturnValue(mockERC20Contract as any);
});

it('should return the root token for a child token', async () => {
const childToken = '0x388c818ca8b9251b393131c08a736a67ccb19297';
const destinationChainId = '1';
const mockChildProvider = new ethers.providers.JsonRpcProvider('x');
const receivedRootToken = await getWithdrawRootToken(
childToken,
destinationChainId,
mockChildProvider,
);
expect(receivedRootToken).toEqual(rootToken);
});

it('should return the root IMX token withdrawing NATIVE', async () => {
const childToken = 'NATIVE';
const destinationChainId = '1';
const mockChildProvider = new ethers.providers.JsonRpcProvider('x');
const receivedRootToken = await getWithdrawRootToken(
childToken,
destinationChainId,
mockChildProvider,
);

const rootIMX = '0xf57e7e7c23978c3caec3c3548e3d615c346e79ff';
expect(receivedRootToken).toEqual(rootIMX);
});
});
});
66 changes: 66 additions & 0 deletions packages/internal/bridge/sdk/src/lib/axelarUtils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { Provider } from '@ethersproject/providers';
import { Address } from 'types';
import { WITHDRAW_SIG, NATIVE } from 'constants/bridges';
import { CHILD_ERC20 } from 'contracts/ABIs/ChildERC20';
import { withBridgeError, BridgeErrorType } from 'errors';
import { ethers } from 'ethers';
import { keccak256, defaultAbiCoder } from 'ethers/lib/utils';
import { isWrappedIMX, getRootIMX } from './utils';

/**
* We need the Axelar command ID to be unique, otherwise the simulation could fail.
* We don't necessarily care if the command is what would actually be used by the
* Axelar network.
* @param payload The Axelar GMP payload.
* @returns hash of payload and current time.
*/
export function genUniqueAxelarCommandId(payload: string) {
return keccak256(
defaultAbiCoder.encode(['bytes', 'uint256'], [payload, new Date().getTime()]),
);
}

/**
* Generates an Axelar GMP payload for a withdrawal.
* Note that this is not the payload *hash*. It can be any length of bytes.
*/
export function genAxelarWithdrawPayload(
rootToken: string,
sender: string,
recipient: string,
amount: string,
) {
return defaultAbiCoder.encode(
['bytes32', 'address', 'address', 'address', 'uint256'],
[WITHDRAW_SIG, rootToken, sender, recipient, amount],
);
}

export async function createChildErc20Contract(
token: string,
childProvider: Provider,
): Promise<ethers.Contract> {
return withBridgeError<ethers.Contract>(
async () => new ethers.Contract(token, CHILD_ERC20, childProvider),
BridgeErrorType.PROVIDER_ERROR,
);
}

/**
* Given a child chain token address (or NATIVE), returns its corresponding root token address.
* This is done by calling the `rootToken` function on the child token contract.
*/
export async function getWithdrawRootToken(
childToken: string,
destinationChainId: string,
childProvider: ethers.providers.Provider,
): Promise<string> {
if (childToken.toUpperCase() === NATIVE
|| isWrappedIMX(childToken, destinationChainId)) {
return getRootIMX(destinationChainId);
}
// Find root token
const erc20Contract: ethers.Contract = await createChildErc20Contract(childToken, childProvider);

return withBridgeError<Address>(() => erc20Contract.rootToken(), BridgeErrorType.PROVIDER_ERROR);
}
11 changes: 10 additions & 1 deletion packages/internal/bridge/sdk/src/lib/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
axelarGateways,
axelarAPIEndpoints,
tenderlyAPIEndpoints,
childWIMXs,
} from 'constants/bridges';
import { FungibleToken } from 'types';

Expand All @@ -23,7 +24,7 @@ function getAddresses(source:string, addresses:Record<string, string>) {
return address;
}

export function getChildETH(source: string) {
function getChildETH(source: string) {
return getAddresses(source, childETHs);
}

Expand Down Expand Up @@ -63,6 +64,14 @@ export function getTenderlyEndpoint(source:string) {
return getAddresses(source, tenderlyAPIEndpoints);
}

function getWrappedIMX(source: string) {
return getAddresses(source, childWIMXs);
}

export function isWrappedIMX(token: FungibleToken, source: string) {
return token.toUpperCase() === getWrappedIMX(source).toUpperCase();
}

export const exportedForTesting = {
getAddresses,
};
65 changes: 16 additions & 49 deletions packages/internal/bridge/sdk/src/tokenBridge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
/* eslint-disable class-methods-use-this */
import axios, { AxiosResponse } from 'axios';
import { ethers } from 'ethers';
import { CHILD_ERC20 } from 'contracts/ABIs/ChildERC20';
import {
concat, defaultAbiCoder, hexlify, keccak256, zeroPad,
} from 'ethers/lib/utils';
Expand All @@ -13,22 +12,20 @@ import {
validateGetFee,
} from 'lib/validation';
import {
getAxelarEndpoint, getAxelarGateway, getChildAdaptor, getChildchain, getRootAdaptor, getRootIMX, getTenderlyEndpoint,
getAxelarEndpoint, getAxelarGateway, getChildAdaptor, getChildchain, getRootAdaptor, getTenderlyEndpoint,
isWrappedIMX,
} from 'lib/utils';
import { TenderlySimulation } from 'types/tenderly';
import { calculateGasFee } from 'lib/gas';
import { getWithdrawRootToken, genAxelarWithdrawPayload, genUniqueAxelarCommandId } from 'lib/axelarUtils';
import {
NATIVE,
ETHEREUM_NATIVE_TOKEN_ADDRESS,
ETH_MAINNET_TO_ZKEVM_MAINNET,
ETH_SEPOLIA_TO_ZKEVM_TESTNET,
ZKEVM_DEVNET_CHAIN_ID,
ZKEVM_MAINNET_CHAIN_ID,
ZKEVM_TESTNET_CHAIN_ID,
axelarChains,
bridgeMethods,
childWIMXs,
WITHDRAW_SIG,
SLOT_PREFIX_CONTRACT_CALL_APPROVED,
SLOT_POS_CONTRACT_CALL_APPROVED,
} from './constants/bridges';
Expand Down Expand Up @@ -67,9 +64,10 @@ import {
BridgeBundledTxRequest,
BridgeBundledTxResponse,
DynamicGasEstimatesResponse,
Address,
} from './types';
import { GMPStatus, GMPStatusResponse, GasPaidStatus } from './types/axelar';
import {
GMPStatus, GMPStatusResponse, GasPaidStatus,
} from './types/axelar';
import { queryTransactionStatus } from './lib/gmpRecovery';

/**
Expand Down Expand Up @@ -178,7 +176,7 @@ export class TokenBridge {
if (req.sourceChainId === this.config.bridgeInstance.rootChainID) {
approvalFee = calculateGasFee(feeData, BridgeMethodsGasLimit.APPROVE_TOKEN);
} else if (req.sourceChainId === this.config.bridgeInstance.childChainID
&& this.isWrappedIMX(req.token, this.config.bridgeInstance.childChainID)) {
&& isWrappedIMX(req.token, this.config.bridgeInstance.childChainID)) {
// On child chain, only WIMX requires approval.
approvalFee = calculateGasFee(feeData, BridgeMethodsGasLimit.APPROVE_TOKEN);
}
Expand Down Expand Up @@ -681,7 +679,7 @@ export class TokenBridge {
const imtblFee: ethers.BigNumber = ethers.BigNumber.from(0);

// Approval required only for WIMX tokens with insufficient allowance.
if (this.isWrappedIMX(token, this.config.bridgeInstance.childChainID) && allowance.lt(amount)) {
if (isWrappedIMX(token, this.config.bridgeInstance.childChainID) && allowance.lt(amount)) {
contractToApprove = token;
const erc20Contract: ethers.Contract = await withBridgeError<ethers.Contract>(
async () => new ethers.Contract(token, ERC20, this.config.childProvider),
Expand Down Expand Up @@ -814,27 +812,14 @@ export class TokenBridge {
token: FungibleToken,
amount: ethers.BigNumber,
): Promise<number> {
let rootToken: string;
if (token.toUpperCase() === NATIVE
|| this.isWrappedIMX(token, destinationChainId)) {
rootToken = getRootIMX(destinationChainId);
} else {
// Find root token
const erc20Contract: ethers.Contract = await withBridgeError<ethers.Contract>(
async () => new ethers.Contract(token, CHILD_ERC20, this.config.childProvider),
BridgeErrorType.PROVIDER_ERROR,
);
rootToken = await withBridgeError<Address>(() => erc20Contract.rootToken(), BridgeErrorType.PROVIDER_ERROR);
}
// Encode payload
const payload = defaultAbiCoder.encode(
['bytes32', 'address', 'address', 'address', 'uint256'],
[WITHDRAW_SIG, rootToken, sender, recipient, amount],
);
// Generate unique command ID based on payload and current time.
const commandId = keccak256(
defaultAbiCoder.encode(['bytes', 'uint256'], [payload, new Date().getTime()]),
const rootToken = await getWithdrawRootToken(token, destinationChainId, this.config.childProvider);
const payload = genAxelarWithdrawPayload(
rootToken,
sender,
recipient,
amount.toString(),
);
const commandId = genUniqueAxelarCommandId(payload);
const sourceChain = getChildchain(destinationChainId);
const sourceAddress = ethers.utils.getAddress(getChildAdaptor(destinationChainId)).toString();
const destinationAddress = getRootAdaptor(destinationChainId);
Expand Down Expand Up @@ -947,7 +932,7 @@ export class TokenBridge {
return ethers.BigNumber.from(0);
}
if (sourceChainId === this.config.bridgeInstance.childChainID
&& !this.isWrappedIMX(token, sourceChainId)
&& !isWrappedIMX(token, sourceChainId)
) {
// Return immediately for non wrapped IMX on child chain.
return ethers.BigNumber.from(0);
Expand All @@ -974,24 +959,6 @@ export class TokenBridge {
), BridgeErrorType.PROVIDER_ERROR);
}

private getWrappedIMX(source: string) {
let wIMX:string;
if (source === ETH_MAINNET_TO_ZKEVM_MAINNET.rootChainID
|| source === ETH_MAINNET_TO_ZKEVM_MAINNET.childChainID) {
wIMX = childWIMXs.mainnet;
} else if (source === ETH_SEPOLIA_TO_ZKEVM_TESTNET.rootChainID
|| source === ETH_SEPOLIA_TO_ZKEVM_TESTNET.childChainID) {
wIMX = childWIMXs.testnet;
} else {
wIMX = childWIMXs.devnet;
}
return wIMX;
}

private isWrappedIMX(token: FungibleToken, source: string) {
return token.toUpperCase() === this.getWrappedIMX(source).toUpperCase();
}

/**
* Query the axelar fee for a transaction using axelarjs-sdk.
* @param {*} sourceChainId - The source chainId.
Expand Down

0 comments on commit dbb0c3f

Please sign in to comment.