Skip to content

Commit

Permalink
feat: Destination network liquidity check (#290)
Browse files Browse the repository at this point in the history
* destination liquidity check

* fix existing test

* update substrate balance checking

* add tests for balance check

* fix lint

* apply feedback

* clean up code

* add check to BuildTransferTx

* update tests

* Apply suggestions from code review

Co-authored-by: Matija Petrunić <[email protected]>

* throw error when calling `createFungibleTransfer`

* add test for not enough liquidity

* mark param as optional

* update param order

* fix lint

* Update packages/sdk/src/chains/BaseAssetTransfer.ts

Co-authored-by: Matija Petrunić <[email protected]>

* remove stale test

---------

Co-authored-by: Matija Petrunić <[email protected]>
  • Loading branch information
FSM1 and mpetrun5 authored Aug 8, 2023
1 parent fb3df09 commit ede1479
Show file tree
Hide file tree
Showing 14 changed files with 279 additions and 57 deletions.
2 changes: 1 addition & 1 deletion examples/evm-to-evm-fungible-transfer/src/transfer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export async function erc20Transfer(): Promise<void> {
const provider = new providers.JsonRpcProvider(
"https://rpc.goerli.eth.gateway.fm/"
);
const wallet = new Wallet(privateKey as string, provider);
const wallet = new Wallet(privateKey ?? "", provider);
const assetTransfer = new EVMAssetTransfer();
await assetTransfer.init(provider, Environment.TESTNET);

Expand Down
80 changes: 75 additions & 5 deletions packages/sdk/src/chains/BaseAssetTransfer.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,19 @@
import { Environment, Fungible, Transfer } from '../types';
import { constants } from 'ethers';
import { Config } from '../config';
import {
Environment,
EvmResource,
Fungible,
Network,
ResourceType,
SubstrateResource,
Transfer,
TransferType,
} from '../types';
import { LiquidityError } from '../errors/customErrors';
import { ParachainID } from './Substrate';
import { getEvmHandlerBalance } from './EVM/utils/getEvmHandlerBalance';
import { getSubstrateHandlerBalance } from './Substrate/utils/getSubstrateHandlerBalance';

export abstract class BaseAssetTransfer {
public config!: Config;
Expand All @@ -16,18 +29,20 @@ export abstract class BaseAssetTransfer {
* @param {string} destinationAddress - The address of the recipient on the destination chain
* @param {string} resourceId - The ID of the resource being transferred
* @param {string} amount - The amount of tokens to be transferred. The amount should be in the lowest denomination possible on the source chain. If the token on source chain is configured to use 12 decimals and the amount to be transferred is 1 ETH, then amount should be passed in as 1000000000000
* @param {string} parachainId - Optional parachain id if the substrate destination parachain differs from the target domain.
* @param {string} [parachainId] - Optional parachain id if the substrate destination parachain differs from the target domain.
* @param {string} [destinationProviderUrl] Destination Chain RPC URL - If passed in, this will perform a liquidity check on the destination chain handler.
* @returns {Transfer<Fungible>} - The populated transfer object
* @throws {Error} - Source domain not supported, Destination domain not supported, Resource not supported
* @throws {Error} - Source domain not supported, Destination domain not supported, Resource not supported, destination liquiditry too low
*/
public createFungibleTransfer(
public async createFungibleTransfer(
sourceAddress: string,
destinationChainId: number,
destinationAddress: string,
resourceId: string,
amount: string,
parachainId?: ParachainID,
): Transfer<Fungible> {
destinationProviderUrl?: string,
): Promise<Transfer<Fungible>> {
const { sourceDomain, destinationDomain, resource } = this.config.getBaseTransferParams(
destinationChainId,
resourceId,
Expand All @@ -45,6 +60,61 @@ export abstract class BaseAssetTransfer {
resource: resource,
};

if (destinationProviderUrl) {
const destinationHandlerBalance = await this.fetchDestinationHandlerBalance(
destinationProviderUrl,
transfer,
);
if (destinationHandlerBalance < BigInt(amount)) {
throw new LiquidityError(destinationHandlerBalance);
}
}

return transfer;
}

/**
* @param {Transfer} transfer Transfer to check
* @param {string} destinationProviderUrl URL of the destination chain provider
* @returns {Promise<bigint>} Handler balance on the destination chain
* @throws {Error} No Fungible handler configured on destination domain
*/
async fetchDestinationHandlerBalance(
destinationProviderUrl: string,
transfer: Transfer<TransferType>,
): Promise<bigint> {
const destinationDomain = this.config.getDomainConfig(transfer.to.id);
const handlerAddress = destinationDomain.handlers.find(
h => h.type === ResourceType.FUNGIBLE,
)?.address;

if (!handlerAddress) {
throw new Error('No Funglible handler configured on destination domain');
}

const destinationResource = destinationDomain.resources.find(
r => r.resourceId === transfer.resource.resourceId,
);

if (destinationResource?.burnable) {
return BigInt(constants.MaxUint256.toString());
}

switch (destinationDomain.type) {
case Network.EVM: {
return getEvmHandlerBalance(
destinationProviderUrl,
destinationResource as EvmResource,
handlerAddress,
);
}
case Network.SUBSTRATE: {
return getSubstrateHandlerBalance(
destinationProviderUrl,
destinationResource as SubstrateResource,
handlerAddress,
);
}
}
}
}
23 changes: 15 additions & 8 deletions packages/sdk/src/chains/EVM/assetTransfer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import {
ERC721MinterBurnerPauser__factory,
FeeHandlerRouter__factory,
} from '@buildwithsygma/sygma-contracts';

import {
Environment,
EthereumConfig,
Expand Down Expand Up @@ -91,7 +90,7 @@ export class EVMAssetTransfer extends BaseAssetTransfer {
* @returns fee that needs to paid
*/
public async getFee(transfer: Transfer<TransferType>): Promise<EvmFee> {
const domainConfig = this.config.getDomainConfig() as EthereumConfig;
const domainConfig = this.config.getSourceDomainConfig() as EthereumConfig;
const feeRouter = FeeHandlerRouter__factory.connect(domainConfig.feeRouter, this.provider);
const feeHandlerAddress = await feeRouter._domainResourceIDToFeeHandlerAddress(
transfer.to.id,
Expand Down Expand Up @@ -138,15 +137,18 @@ export class EVMAssetTransfer extends BaseAssetTransfer {
* Builds approval transactions that are required before executing
* deposit. Returns multiple approvals if fee is payed in ERC20 token.
*
* @param transfer requested transfer
* @param fee Fee calculated by 'getFee' function
* @param {Transfer} transfer Transfer
* @param {Fee} fee Fee calculated by 'getFee' function
* @returns array of unsigned approval transaction
*/
public async buildApprovals(
transfer: Transfer<TransferType>,
fee: EvmFee,
): Promise<Array<UnsignedTransaction>> {
const bridge = Bridge__factory.connect(this.config.getDomainConfig().bridge, this.provider);
const bridge = Bridge__factory.connect(
this.config.getSourceDomainConfig().bridge,
this.provider,
);
const handlerAddress = await bridge._resourceIDToHandlerAddress(transfer.resource.resourceId);

const approvals: Array<PopulatedTransaction> = [];
Expand Down Expand Up @@ -191,15 +193,20 @@ export class EVMAssetTransfer extends BaseAssetTransfer {
* Builds an unsigned transfer transaction.
* Should be executed after the approval transactions.
*
* @param transfer
* @param fee
* @param {Transfer} transfer requested transfer
* @param {Fee} fee Fee calculated by 'getFee' function
* @param {Boolean} ignoreInsufficientDestinationLiquidity Flag to disable destination chain balance check
* @returns unsigned transfer transaction
* @throws {Error} Insufficient destination chain liquidity to proceed with transfer
*/
public async buildTransferTransaction(
transfer: Transfer<TransferType>,
fee: EvmFee,
): Promise<PopulatedTransaction> {
const bridge = Bridge__factory.connect(this.config.getDomainConfig().bridge, this.provider);
const bridge = Bridge__factory.connect(
this.config.getSourceDomainConfig().bridge,
this.provider,
);
switch (transfer.resource.type) {
case ResourceType.FUNGIBLE: {
const fungibleTransfer = transfer as Transfer<Fungible>;
Expand Down
7 changes: 5 additions & 2 deletions packages/sdk/src/chains/EVM/genericMessage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ export class EVMGenericMessageTransfer {
* @returns fee that needs to paid
*/
public async getFee(transfer: Transfer<TransferType>): Promise<EvmFee> {
const domainConfig = this.config.getDomainConfig() as EthereumConfig;
const domainConfig = this.config.getSourceDomainConfig() as EthereumConfig;
const feeRouter = FeeHandlerRouter__factory.connect(domainConfig.feeRouter, this.provider);
const feeHandlerAddress = await feeRouter._domainResourceIDToFeeHandlerAddress(
transfer.to.id,
Expand Down Expand Up @@ -102,7 +102,10 @@ export class EVMGenericMessageTransfer {
transfer: Transfer<TransferType>,
fee: EvmFee,
): Promise<PopulatedTransaction> {
const bridge = Bridge__factory.connect(this.config.getDomainConfig().bridge, this.provider);
const bridge = Bridge__factory.connect(
this.config.getSourceDomainConfig().bridge,
this.provider,
);
switch (transfer.resource.type) {
case ResourceType.PERMISSIONLESS_GENERIC: {
const genericTransfer = transfer as Transfer<GenericMessage>;
Expand Down
18 changes: 18 additions & 0 deletions packages/sdk/src/chains/EVM/utils/getEvmHandlerBalance.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { ERC20__factory } from '@buildwithsygma/sygma-contracts';
import { JsonRpcProvider } from '@ethersproject/providers';
import { EvmResource } from 'types';

export const getEvmHandlerBalance = async (
destinationProviderUrl: string,
resource: EvmResource,
handlerAddress: string,
): Promise<bigint> => {
const provider = new JsonRpcProvider(destinationProviderUrl);
if (resource.native) {
return BigInt((await provider.getBalance(handlerAddress)).toString());
} else {
const tokenAddress = resource.address;
const erc20Contract = ERC20__factory.connect(tokenAddress, provider);
return BigInt((await erc20Contract.balanceOf(handlerAddress)).toString());
}
};
10 changes: 7 additions & 3 deletions packages/sdk/src/chains/Substrate/assetTransfer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,10 +77,14 @@ export class SubstrateAssetTransfer extends BaseAssetTransfer {
/**
* Builds an unsigned transfer transaction.
*
* @param transfer Instance of transfer
* @param fee The fee to be paid for the transfer
* @returns {SubmittableExtrinsic<'promise', SubmittableResult>} SubmittableExtrinsic which can be signed and sent
* @param {Transfer} transfer Instance of transfer
* @param {Fee} fee The fee to be paid for the transfer
* @param {Boolean} skipDestinationBalanceCheck Flag to disable destination chain balance check
* @param {String} destinationProviderUrl URL for destination chain provider
* @returns {Promise<SubmittableExtrinsic<'promise', SubmittableResult>>} SubmittableExtrinsic which can be signed and sent
* @throws {Error} Transfer amount too low
* @throws {Error} Destination Chain URL is required
* @throws {Error} Insufficient destination chain liquidity to proceed with transfer
*/
public buildTransferTransaction(
transfer: Transfer<TransferType>,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { ApiPromise } from '@polkadot/api';
import { WsProvider } from '@polkadot/rpc-provider';
import { SubstrateResource } from 'types';
import { getNativeTokenBalance } from './getNativeTokenBalance';
import { getAssetBalance } from './getAssetBalance';

export const getSubstrateHandlerBalance = async (
destinationProviderUrl: string,
resource: SubstrateResource,
handlerAddress: string,
): Promise<bigint> => {
const wsProvider = new WsProvider(destinationProviderUrl);
const apiPromise = new ApiPromise({ provider: wsProvider });
if (resource.native) {
const accountInfo = await getNativeTokenBalance(apiPromise, handlerAddress);
return BigInt(accountInfo.free.toString());
} else {
const assetBalance = await getAssetBalance(apiPromise, resource.assetId ?? 0, handlerAddress);
return BigInt(assetBalance.balance.toString());
}
};
13 changes: 11 additions & 2 deletions packages/sdk/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,20 +47,29 @@ export class Config {
}
}

public getDomainConfig(): EthereumConfig | SubstrateConfig {
public getSourceDomainConfig(): EthereumConfig | SubstrateConfig {
const domain = this.environment.domains.find(domain => domain.chainId === this.chainId);
if (!domain) {
throw new Error('Config for the provided domain is not setup');
}
return domain;
}

public getDomainConfig(domainId: number): EthereumConfig | SubstrateConfig {
const domain = this.environment.domains.find(d => d.id === domainId);
if (!domain) {
throw new Error('Domain not found');
}

return domain;
}

public getDomains(): Array<Domain> {
return this.environment.domains.map(({ id, chainId, name }) => ({ id, chainId, name }));
}

public getDomainResources(): Array<Resource> {
const domain = this.getDomainConfig();
const domain = this.getSourceDomainConfig();
return domain.resources;
}

Expand Down
17 changes: 17 additions & 0 deletions packages/sdk/src/errors/customErrors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
class SygmaSdkError extends Error {
constructor(message: string) {
super(message);
this.name = this.constructor.name;
}
}

export class LiquidityError extends SygmaSdkError {
availableLiquidity: bigint;

constructor(destinationLiquidity: bigint) {
super(
`Destination chain liquidity is too low to perform this transfer. Transfer is limited to ${destinationLiquidity.toString()}`,
);
this.availableLiquidity = destinationLiquidity;
}
}
4 changes: 2 additions & 2 deletions packages/sdk/src/types/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,11 @@ export type Handler = {
address: string;
};

export interface BaseConfig<Type> {
export interface BaseConfig<T> {
id: number;
chainId: number;
name: string;
type: Type;
type: T;
bridge: string;
nativeTokenSymbol: string;
nativeTokenName: string;
Expand Down
6 changes: 5 additions & 1 deletion packages/sdk/src/types/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ export type Resource = EvmResource | SubstrateResource;
interface BaseResource {
resourceId: string;
type: ResourceType;
native?: boolean;
burnable?: boolean;
symbol?: string;
decimals?: number;
}
Expand All @@ -37,7 +39,7 @@ export type EvmResource = BaseResource & {
};

export type SubstrateResource = BaseResource & {
assetId: number;
assetId?: number;
assetName: string;
xcmMultiAssetId: XcmMultiAssetIdType;
};
Expand Down Expand Up @@ -76,3 +78,5 @@ export type Transfer<TransferType> = {
resource: Resource;
sender: string;
};

export type LiquidityError = Error & { maximumTransferAmount: bigint };
Loading

0 comments on commit ede1479

Please sign in to comment.