diff --git a/packages/contract-helpers/src/v3-pool-contract-bundle/index.ts b/packages/contract-helpers/src/v3-pool-contract-bundle/index.ts index 767957aa..ff4dae44 100644 --- a/packages/contract-helpers/src/v3-pool-contract-bundle/index.ts +++ b/packages/contract-helpers/src/v3-pool-contract-bundle/index.ts @@ -37,7 +37,7 @@ import { L2Pool, L2PoolInterface } from '../v3-pool-rollups'; import { WETHGatewayInterface, WETHGatewayService, -} from '../wethgateway-contract'; +} from '../v3-wethgateway-contract'; export type SupplyTxBuilder = { generateTxData: ({ @@ -316,7 +316,6 @@ export class PoolBundle user, amount, debtTokenAddress, - interestRateMode, referralCode: referralCodeParam, }); } else { @@ -375,7 +374,6 @@ export class PoolBundle lendingPool: this.poolAddress, user, amount, - interestRateMode, onBehalfOf: onBehalfOfParam, }); } diff --git a/packages/contract-helpers/src/v3-pool-contract-bundle/pool-bundle.test.ts b/packages/contract-helpers/src/v3-pool-contract-bundle/pool-bundle.test.ts index 2224aae3..fed24e17 100644 --- a/packages/contract-helpers/src/v3-pool-contract-bundle/pool-bundle.test.ts +++ b/packages/contract-helpers/src/v3-pool-contract-bundle/pool-bundle.test.ts @@ -475,16 +475,6 @@ describe('PoolBundle', () => { debtTokenAddress: API_ETH_MOCK_ADDRESS.toLowerCase(), }); - const resultStable = instance.borrowTxBuilder.generateTxData({ - user: USER, - reserve: API_ETH_MOCK_ADDRESS.toLowerCase(), - amount: '1', - onBehalfOf: USER, - interestRateMode: InterestRate.Stable, - referralCode: '0', - debtTokenAddress: API_ETH_MOCK_ADDRESS.toLowerCase(), - }); - const differentParamsSameResult = instance.borrowTxBuilder.generateTxData( { user: USER, @@ -495,9 +485,7 @@ describe('PoolBundle', () => { }, ); const variableTxData = - '0x66514c970000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000'; - const stableTxData = - '0x66514c970000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000'; + '0xe74f7b85000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000'; expect(result.to).toEqual(WETH_GATEWAY); expect(result.from).toEqual(USER); expect(result.value).toEqual(undefined); @@ -505,10 +493,6 @@ describe('PoolBundle', () => { expect(differentParamsSameResult.to).toEqual(WETH_GATEWAY); expect(differentParamsSameResult.from).toEqual(USER); expect(differentParamsSameResult.data).toEqual(variableTxData); - expect(resultStable.to).toEqual(WETH_GATEWAY); - expect(resultStable.from).toEqual(USER); - expect(resultStable.value).toEqual(undefined); - expect(resultStable.data).toEqual(stableTxData); }); it('generates borrow tx data with generateTxData and L2POOL', () => { @@ -711,12 +695,12 @@ describe('PoolBundle', () => { expect(result.from).toEqual(USER); expect(result.value).toEqual(BigNumber.from('1')); expect(result.data).toEqual( - '0x02c5fcf80000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000003', + '0xbcc3c255000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000003', ); expect(differentParamsSameResult.to).toEqual(WETH_GATEWAY); expect(differentParamsSameResult.from).toEqual(USER); expect(differentParamsSameResult.data).toEqual( - '0x02c5fcf80000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000003', + '0xbcc3c255000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000003', ); }); diff --git a/packages/contract-helpers/src/v3-pool-contract/index.ts b/packages/contract-helpers/src/v3-pool-contract/index.ts index 4e4e025b..cdc252e6 100644 --- a/packages/contract-helpers/src/v3-pool-contract/index.ts +++ b/packages/contract-helpers/src/v3-pool-contract/index.ts @@ -53,7 +53,7 @@ import { L2Pool, L2PoolInterface } from '../v3-pool-rollups'; import { WETHGatewayInterface, WETHGatewayService, -} from '../wethgateway-contract'; +} from '../v3-wethgateway-contract'; import { LPBorrowParamsType, LPSupplyParamsType, @@ -681,7 +681,6 @@ export class Pool extends BaseService implements PoolInterface { user, amount, debtTokenAddress, - interestRateMode, referralCode, }); } @@ -750,7 +749,6 @@ export class Pool extends BaseService implements PoolInterface { lendingPool: this.poolAddress, user, amount, - interestRateMode, onBehalfOf, }); } diff --git a/packages/contract-helpers/src/v3-wethgateway-contract/index.ts b/packages/contract-helpers/src/v3-wethgateway-contract/index.ts new file mode 100644 index 00000000..3c2fd6da --- /dev/null +++ b/packages/contract-helpers/src/v3-wethgateway-contract/index.ts @@ -0,0 +1,390 @@ +import { BigNumber, constants, PopulatedTransaction, providers } from 'ethers'; +import { + BaseDebtToken, + BaseDebtTokenInterface, +} from '../baseDebtToken-contract'; +import BaseService from '../commons/BaseService'; +import { + eEthereumTxType, + EthereumTransactionTypeExtended, + ProtocolAction, + tEthereumAddress, + transactionType, +} from '../commons/types'; +import { gasLimitRecommendations, valueToWei } from '../commons/utils'; +import { WETHValidator } from '../commons/validators/methodValidators'; +import { + is0OrPositiveAmount, + isEthAddress, + isPositiveAmount, + isPositiveOrMinusOneAmount, +} from '../commons/validators/paramValidators'; +import { IERC20ServiceInterface } from '../erc20-contract'; +import { + WrappedTokenGatewayV3, + WrappedTokenGatewayV3Interface, +} from './typechain/WrappedTokenGatewayV3'; +import { WrappedTokenGatewayV3__factory } from './typechain/WrappedTokenGatewayV3__factory'; + +export type WETHDepositParamsType = { + lendingPool: tEthereumAddress; + user: tEthereumAddress; + amount: string; // normal + onBehalfOf?: tEthereumAddress; + referralCode?: string; +}; + +export type WETHWithdrawParamsType = { + lendingPool: tEthereumAddress; + user: tEthereumAddress; + amount: string; + aTokenAddress: tEthereumAddress; + onBehalfOf?: tEthereumAddress; +}; + +export type WETHRepayParamsType = { + lendingPool: tEthereumAddress; + user: tEthereumAddress; + amount: string; + onBehalfOf?: tEthereumAddress; +}; + +export type WETHBorrowParamsType = { + lendingPool: tEthereumAddress; + user: tEthereumAddress; + amount: string; + debtTokenAddress?: tEthereumAddress; + referralCode?: string; +}; + +export interface WETHGatewayInterface { + generateDepositEthTxData: ( + args: WETHDepositParamsType, + ) => PopulatedTransaction; + generateBorrowEthTxData: (args: WETHBorrowParamsType) => PopulatedTransaction; + generateRepayEthTxData: (args: WETHRepayParamsType) => PopulatedTransaction; + depositETH: ( + args: WETHDepositParamsType, + ) => EthereumTransactionTypeExtended[]; + withdrawETH: ( + args: WETHWithdrawParamsType, + ) => Promise; + repayETH: (args: WETHRepayParamsType) => EthereumTransactionTypeExtended[]; + borrowETH: ( + args: WETHBorrowParamsType, + ) => Promise; +} + +export class WETHGatewayService + extends BaseService + implements WETHGatewayInterface +{ + readonly wethGatewayAddress: string; + + readonly baseDebtTokenService: BaseDebtTokenInterface; + + readonly erc20Service: IERC20ServiceInterface; + + readonly wethGatewayInstance: WrappedTokenGatewayV3Interface; + + generateDepositEthTxData: ( + args: WETHDepositParamsType, + ) => PopulatedTransaction; + + generateBorrowEthTxData: (args: WETHBorrowParamsType) => PopulatedTransaction; + + generateRepayEthTxData: (args: WETHRepayParamsType) => PopulatedTransaction; + + constructor( + provider: providers.Provider, + erc20Service: IERC20ServiceInterface, + wethGatewayAddress?: string, + ) { + super(provider, WrappedTokenGatewayV3__factory); + this.erc20Service = erc20Service; + + this.baseDebtTokenService = new BaseDebtToken( + this.provider, + this.erc20Service, + ); + + this.wethGatewayAddress = wethGatewayAddress ?? ''; + + this.depositETH = this.depositETH.bind(this); + this.withdrawETH = this.withdrawETH.bind(this); + this.repayETH = this.repayETH.bind(this); + this.borrowETH = this.borrowETH.bind(this); + this.wethGatewayInstance = WrappedTokenGatewayV3__factory.createInterface(); + this.generateDepositEthTxData = ( + args: WETHDepositParamsType, + ): PopulatedTransaction => { + const txData = this.wethGatewayInstance.encodeFunctionData('depositETH', [ + args.lendingPool, + args.onBehalfOf ?? args.user, + args.referralCode ?? '0', + ]); + const actionTx: PopulatedTransaction = { + data: txData, + to: this.wethGatewayAddress, + from: args.user, + value: BigNumber.from(args.amount), + gasLimit: BigNumber.from( + gasLimitRecommendations[ProtocolAction.deposit].limit, + ), + }; + return actionTx; + }; + + this.generateBorrowEthTxData = ( + args: WETHBorrowParamsType, + ): PopulatedTransaction => { + const txData = this.wethGatewayInstance.encodeFunctionData('borrowETH', [ + args.lendingPool, + args.amount, + args.referralCode ?? '0', + ]); + const actionTx: PopulatedTransaction = { + data: txData, + to: this.wethGatewayAddress, + from: args.user, + gasLimit: BigNumber.from( + gasLimitRecommendations[ProtocolAction.borrowETH].limit, + ), + }; + return actionTx; + }; + + this.generateRepayEthTxData = ({ + lendingPool, + amount, + user, + onBehalfOf, + }) => { + const txData = this.wethGatewayInstance.encodeFunctionData('repayETH', [ + lendingPool, + amount, + onBehalfOf ?? user, + ]); + const actionTx: PopulatedTransaction = { + data: txData, + to: this.wethGatewayAddress, + from: user, + value: BigNumber.from(amount), + gasLimit: BigNumber.from( + gasLimitRecommendations[ProtocolAction.repayETH].limit, + ), + }; + return actionTx; + }; + } + + @WETHValidator + public depositETH( + @isEthAddress('lendingPool') + @isEthAddress('user') + @isEthAddress('onBehalfOf') + @isPositiveAmount('amount') + @is0OrPositiveAmount('referralCode') + { + lendingPool, + user, + amount, + onBehalfOf, + referralCode, + }: WETHDepositParamsType, + ): EthereumTransactionTypeExtended[] { + const convertedAmount: string = valueToWei(amount, 18); + + const wethGatewayContract: WrappedTokenGatewayV3 = this.getContractInstance( + this.wethGatewayAddress, + ); + const txCallback: () => Promise = this.generateTxCallback({ + rawTxMethod: async () => + wethGatewayContract.populateTransaction.depositETH( + lendingPool, + onBehalfOf ?? user, + referralCode ?? '0', + ), + from: user, + value: convertedAmount, + }); + + return [ + { + tx: txCallback, + txType: eEthereumTxType.DLP_ACTION, + gas: this.generateTxPriceEstimation([], txCallback), + }, + ]; + } + + @WETHValidator + public async borrowETH( + @isEthAddress('lendingPool') + @isEthAddress('user') + @isPositiveAmount('amount') + @isEthAddress('debtTokenAddress') + @is0OrPositiveAmount('referralCode') + { + lendingPool, + user, + amount, + debtTokenAddress, + referralCode, + }: WETHBorrowParamsType, + ): Promise { + const txs: EthereumTransactionTypeExtended[] = []; + const convertedAmount: string = valueToWei(amount, 18); + if (!debtTokenAddress) { + throw new Error( + `To borrow ETH you need to pass the variable WETH debt Token Address`, + ); + } + + const delegationApproved: boolean = + await this.baseDebtTokenService.isDelegationApproved({ + debtTokenAddress, + allowanceGiver: user, + allowanceReceiver: this.wethGatewayAddress, + amount, + }); + + if (!delegationApproved) { + const approveDelegationTx: EthereumTransactionTypeExtended = + this.baseDebtTokenService.approveDelegation({ + user, + delegatee: this.wethGatewayAddress, + debtTokenAddress, + amount: constants.MaxUint256.toString(), + }); + + txs.push(approveDelegationTx); + } + + const wethGatewayContract: WrappedTokenGatewayV3 = this.getContractInstance( + this.wethGatewayAddress, + ); + + const txCallback: () => Promise = this.generateTxCallback({ + rawTxMethod: async () => + wethGatewayContract.populateTransaction.borrowETH( + lendingPool, + convertedAmount, + referralCode ?? '0', + ), + from: user, + }); + + txs.push({ + tx: txCallback, + txType: eEthereumTxType.DLP_ACTION, + gas: this.generateTxPriceEstimation( + txs, + txCallback, + ProtocolAction.borrowETH, + ), + }); + + return txs; + } + + @WETHValidator + public async withdrawETH( + @isEthAddress('lendingPool') + @isEthAddress('user') + @isEthAddress('onBehalfOf') + @isPositiveOrMinusOneAmount('amount') + @isEthAddress('aTokenAddress') + { + lendingPool, + user, + amount, + onBehalfOf, + aTokenAddress, + }: WETHWithdrawParamsType, + ): Promise { + const txs: EthereumTransactionTypeExtended[] = []; + const { isApproved, approve }: IERC20ServiceInterface = this.erc20Service; + const convertedAmount: string = + amount === '-1' + ? constants.MaxUint256.toString() + : valueToWei(amount, 18); + + const approved: boolean = await isApproved({ + token: aTokenAddress, + user, + spender: this.wethGatewayAddress, + amount, + }); + + if (!approved) { + const approveTx: EthereumTransactionTypeExtended = approve({ + user, + token: aTokenAddress, + spender: this.wethGatewayAddress, + amount: constants.MaxUint256.toString(), + }); + txs.push(approveTx); + } + + const wethGatewayContract: WrappedTokenGatewayV3 = this.getContractInstance( + this.wethGatewayAddress, + ); + + const txCallback: () => Promise = this.generateTxCallback({ + rawTxMethod: async () => + wethGatewayContract.populateTransaction.withdrawETH( + lendingPool, + convertedAmount, + onBehalfOf ?? user, + ), + from: user, + }); + + txs.push({ + tx: txCallback, + txType: eEthereumTxType.DLP_ACTION, + gas: this.generateTxPriceEstimation( + txs, + txCallback, + ProtocolAction.withdrawETH, + ), + }); + + return txs; + } + + @WETHValidator + public repayETH( + @isEthAddress('lendingPool') + @isEthAddress('user') + @isEthAddress('onBehalfOf') + @isPositiveAmount('amount') + { lendingPool, user, amount, onBehalfOf }: WETHRepayParamsType, + ): EthereumTransactionTypeExtended[] { + const convertedAmount: string = valueToWei(amount, 18); + const wethGatewayContract: WrappedTokenGatewayV3 = this.getContractInstance( + this.wethGatewayAddress, + ); + + const txCallback: () => Promise = this.generateTxCallback({ + rawTxMethod: async () => + wethGatewayContract.populateTransaction.repayETH( + lendingPool, + convertedAmount, + onBehalfOf ?? user, + ), + gasSurplus: 30, + from: user, + value: convertedAmount, + }); + + return [ + { + tx: txCallback, + txType: eEthereumTxType.DLP_ACTION, + gas: this.generateTxPriceEstimation([], txCallback), + }, + ]; + } +} diff --git a/packages/contract-helpers/src/v3-wethgateway-contract/typechain/WrappedTokenGatewayV3.ts b/packages/contract-helpers/src/v3-wethgateway-contract/typechain/WrappedTokenGatewayV3.ts new file mode 100644 index 00000000..0bc6d83a --- /dev/null +++ b/packages/contract-helpers/src/v3-wethgateway-contract/typechain/WrappedTokenGatewayV3.ts @@ -0,0 +1,528 @@ +/* Autogenerated file. Do not edit manually. */ +/* eslint-disable */ +import type { + BaseContract, + BigNumber, + BigNumberish, + BytesLike, + CallOverrides, + ContractTransaction, + Overrides, + PayableOverrides, + PopulatedTransaction, + Signer, + utils, +} from 'ethers'; +import type { + FunctionFragment, + Result, + EventFragment, +} from '@ethersproject/abi'; +import type { Listener, Provider } from '@ethersproject/providers'; +import type { + TypedEventFilter, + TypedEvent, + TypedListener, + OnEvent, +} from './common'; + +export interface WrappedTokenGatewayV3Interface extends utils.Interface { + functions: { + 'borrowETH(address,uint256,uint16)': FunctionFragment; + 'depositETH(address,address,uint16)': FunctionFragment; + 'emergencyEtherTransfer(address,uint256)': FunctionFragment; + 'emergencyTokenTransfer(address,address,uint256)': FunctionFragment; + 'getWETHAddress()': FunctionFragment; + 'owner()': FunctionFragment; + 'renounceOwnership()': FunctionFragment; + 'repayETH(address,uint256,address)': FunctionFragment; + 'transferOwnership(address)': FunctionFragment; + 'withdrawETH(address,uint256,address)': FunctionFragment; + 'withdrawETHWithPermit(address,uint256,address,uint256,uint8,bytes32,bytes32)': FunctionFragment; + }; + + getFunction( + nameOrSignatureOrTopic: + | 'borrowETH' + | 'depositETH' + | 'emergencyEtherTransfer' + | 'emergencyTokenTransfer' + | 'getWETHAddress' + | 'owner' + | 'renounceOwnership' + | 'repayETH' + | 'transferOwnership' + | 'withdrawETH' + | 'withdrawETHWithPermit', + ): FunctionFragment; + + encodeFunctionData( + functionFragment: 'borrowETH', + values: [string, BigNumberish, BigNumberish], + ): string; + encodeFunctionData( + functionFragment: 'depositETH', + values: [string, string, BigNumberish], + ): string; + encodeFunctionData( + functionFragment: 'emergencyEtherTransfer', + values: [string, BigNumberish], + ): string; + encodeFunctionData( + functionFragment: 'emergencyTokenTransfer', + values: [string, string, BigNumberish], + ): string; + encodeFunctionData( + functionFragment: 'getWETHAddress', + values?: undefined, + ): string; + encodeFunctionData(functionFragment: 'owner', values?: undefined): string; + encodeFunctionData( + functionFragment: 'renounceOwnership', + values?: undefined, + ): string; + encodeFunctionData( + functionFragment: 'repayETH', + values: [string, BigNumberish, string], + ): string; + encodeFunctionData( + functionFragment: 'transferOwnership', + values: [string], + ): string; + encodeFunctionData( + functionFragment: 'withdrawETH', + values: [string, BigNumberish, string], + ): string; + encodeFunctionData( + functionFragment: 'withdrawETHWithPermit', + values: [ + string, + BigNumberish, + string, + BigNumberish, + BigNumberish, + BytesLike, + BytesLike, + ], + ): string; + + decodeFunctionResult(functionFragment: 'borrowETH', data: BytesLike): Result; + decodeFunctionResult(functionFragment: 'depositETH', data: BytesLike): Result; + decodeFunctionResult( + functionFragment: 'emergencyEtherTransfer', + data: BytesLike, + ): Result; + decodeFunctionResult( + functionFragment: 'emergencyTokenTransfer', + data: BytesLike, + ): Result; + decodeFunctionResult( + functionFragment: 'getWETHAddress', + data: BytesLike, + ): Result; + decodeFunctionResult(functionFragment: 'owner', data: BytesLike): Result; + decodeFunctionResult( + functionFragment: 'renounceOwnership', + data: BytesLike, + ): Result; + decodeFunctionResult(functionFragment: 'repayETH', data: BytesLike): Result; + decodeFunctionResult( + functionFragment: 'transferOwnership', + data: BytesLike, + ): Result; + decodeFunctionResult( + functionFragment: 'withdrawETH', + data: BytesLike, + ): Result; + decodeFunctionResult( + functionFragment: 'withdrawETHWithPermit', + data: BytesLike, + ): Result; + + events: { + 'OwnershipTransferred(address,address)': EventFragment; + }; + + getEvent(nameOrSignatureOrTopic: 'OwnershipTransferred'): EventFragment; +} + +export interface OwnershipTransferredEventObject { + previousOwner: string; + newOwner: string; +} +export type OwnershipTransferredEvent = TypedEvent< + [string, string], + OwnershipTransferredEventObject +>; + +export type OwnershipTransferredEventFilter = + TypedEventFilter; + +export interface WrappedTokenGatewayV3 extends BaseContract { + connect(signerOrProvider: Signer | Provider | string): this; + attach(addressOrName: string): this; + deployed(): Promise; + + interface: WrappedTokenGatewayV3Interface; + + queryFilter( + event: TypedEventFilter, + fromBlockOrBlockhash?: string | number | undefined, + toBlock?: string | number | undefined, + ): Promise>; + + listeners( + eventFilter?: TypedEventFilter, + ): Array>; + listeners(eventName?: string): Array; + removeAllListeners( + eventFilter: TypedEventFilter, + ): this; + removeAllListeners(eventName?: string): this; + off: OnEvent; + on: OnEvent; + once: OnEvent; + removeListener: OnEvent; + + functions: { + borrowETH( + arg0: string, + amount: BigNumberish, + referralCode: BigNumberish, + overrides?: Overrides & { from?: string }, + ): Promise; + + depositETH( + arg0: string, + onBehalfOf: string, + referralCode: BigNumberish, + overrides?: PayableOverrides & { from?: string }, + ): Promise; + + emergencyEtherTransfer( + to: string, + amount: BigNumberish, + overrides?: Overrides & { from?: string }, + ): Promise; + + emergencyTokenTransfer( + token: string, + to: string, + amount: BigNumberish, + overrides?: Overrides & { from?: string }, + ): Promise; + + getWETHAddress(overrides?: CallOverrides): Promise<[string]>; + + owner(overrides?: CallOverrides): Promise<[string]>; + + renounceOwnership( + overrides?: Overrides & { from?: string }, + ): Promise; + + repayETH( + arg0: string, + amount: BigNumberish, + onBehalfOf: string, + overrides?: PayableOverrides & { from?: string }, + ): Promise; + + transferOwnership( + newOwner: string, + overrides?: Overrides & { from?: string }, + ): Promise; + + withdrawETH( + arg0: string, + amount: BigNumberish, + to: string, + overrides?: Overrides & { from?: string }, + ): Promise; + + withdrawETHWithPermit( + arg0: string, + amount: BigNumberish, + to: string, + deadline: BigNumberish, + permitV: BigNumberish, + permitR: BytesLike, + permitS: BytesLike, + overrides?: Overrides & { from?: string }, + ): Promise; + }; + + borrowETH( + arg0: string, + amount: BigNumberish, + referralCode: BigNumberish, + overrides?: Overrides & { from?: string }, + ): Promise; + + depositETH( + arg0: string, + onBehalfOf: string, + referralCode: BigNumberish, + overrides?: PayableOverrides & { from?: string }, + ): Promise; + + emergencyEtherTransfer( + to: string, + amount: BigNumberish, + overrides?: Overrides & { from?: string }, + ): Promise; + + emergencyTokenTransfer( + token: string, + to: string, + amount: BigNumberish, + overrides?: Overrides & { from?: string }, + ): Promise; + + getWETHAddress(overrides?: CallOverrides): Promise; + + owner(overrides?: CallOverrides): Promise; + + renounceOwnership( + overrides?: Overrides & { from?: string }, + ): Promise; + + repayETH( + arg0: string, + amount: BigNumberish, + onBehalfOf: string, + overrides?: PayableOverrides & { from?: string }, + ): Promise; + + transferOwnership( + newOwner: string, + overrides?: Overrides & { from?: string }, + ): Promise; + + withdrawETH( + arg0: string, + amount: BigNumberish, + to: string, + overrides?: Overrides & { from?: string }, + ): Promise; + + withdrawETHWithPermit( + arg0: string, + amount: BigNumberish, + to: string, + deadline: BigNumberish, + permitV: BigNumberish, + permitR: BytesLike, + permitS: BytesLike, + overrides?: Overrides & { from?: string }, + ): Promise; + + callStatic: { + borrowETH( + arg0: string, + amount: BigNumberish, + referralCode: BigNumberish, + overrides?: CallOverrides, + ): Promise; + + depositETH( + arg0: string, + onBehalfOf: string, + referralCode: BigNumberish, + overrides?: CallOverrides, + ): Promise; + + emergencyEtherTransfer( + to: string, + amount: BigNumberish, + overrides?: CallOverrides, + ): Promise; + + emergencyTokenTransfer( + token: string, + to: string, + amount: BigNumberish, + overrides?: CallOverrides, + ): Promise; + + getWETHAddress(overrides?: CallOverrides): Promise; + + owner(overrides?: CallOverrides): Promise; + + renounceOwnership(overrides?: CallOverrides): Promise; + + repayETH( + arg0: string, + amount: BigNumberish, + onBehalfOf: string, + overrides?: CallOverrides, + ): Promise; + + transferOwnership( + newOwner: string, + overrides?: CallOverrides, + ): Promise; + + withdrawETH( + arg0: string, + amount: BigNumberish, + to: string, + overrides?: CallOverrides, + ): Promise; + + withdrawETHWithPermit( + arg0: string, + amount: BigNumberish, + to: string, + deadline: BigNumberish, + permitV: BigNumberish, + permitR: BytesLike, + permitS: BytesLike, + overrides?: CallOverrides, + ): Promise; + }; + + filters: { + 'OwnershipTransferred(address,address)'( + previousOwner?: string | null, + newOwner?: string | null, + ): OwnershipTransferredEventFilter; + OwnershipTransferred( + previousOwner?: string | null, + newOwner?: string | null, + ): OwnershipTransferredEventFilter; + }; + + estimateGas: { + borrowETH( + arg0: string, + amount: BigNumberish, + referralCode: BigNumberish, + overrides?: Overrides & { from?: string }, + ): Promise; + + depositETH( + arg0: string, + onBehalfOf: string, + referralCode: BigNumberish, + overrides?: PayableOverrides & { from?: string }, + ): Promise; + + emergencyEtherTransfer( + to: string, + amount: BigNumberish, + overrides?: Overrides & { from?: string }, + ): Promise; + + emergencyTokenTransfer( + token: string, + to: string, + amount: BigNumberish, + overrides?: Overrides & { from?: string }, + ): Promise; + + getWETHAddress(overrides?: CallOverrides): Promise; + + owner(overrides?: CallOverrides): Promise; + + renounceOwnership( + overrides?: Overrides & { from?: string }, + ): Promise; + + repayETH( + arg0: string, + amount: BigNumberish, + onBehalfOf: string, + overrides?: PayableOverrides & { from?: string }, + ): Promise; + + transferOwnership( + newOwner: string, + overrides?: Overrides & { from?: string }, + ): Promise; + + withdrawETH( + arg0: string, + amount: BigNumberish, + to: string, + overrides?: Overrides & { from?: string }, + ): Promise; + + withdrawETHWithPermit( + arg0: string, + amount: BigNumberish, + to: string, + deadline: BigNumberish, + permitV: BigNumberish, + permitR: BytesLike, + permitS: BytesLike, + overrides?: Overrides & { from?: string }, + ): Promise; + }; + + populateTransaction: { + borrowETH( + arg0: string, + amount: BigNumberish, + referralCode: BigNumberish, + overrides?: Overrides & { from?: string }, + ): Promise; + + depositETH( + arg0: string, + onBehalfOf: string, + referralCode: BigNumberish, + overrides?: PayableOverrides & { from?: string }, + ): Promise; + + emergencyEtherTransfer( + to: string, + amount: BigNumberish, + overrides?: Overrides & { from?: string }, + ): Promise; + + emergencyTokenTransfer( + token: string, + to: string, + amount: BigNumberish, + overrides?: Overrides & { from?: string }, + ): Promise; + + getWETHAddress(overrides?: CallOverrides): Promise; + + owner(overrides?: CallOverrides): Promise; + + renounceOwnership( + overrides?: Overrides & { from?: string }, + ): Promise; + + repayETH( + arg0: string, + amount: BigNumberish, + onBehalfOf: string, + overrides?: PayableOverrides & { from?: string }, + ): Promise; + + transferOwnership( + newOwner: string, + overrides?: Overrides & { from?: string }, + ): Promise; + + withdrawETH( + arg0: string, + amount: BigNumberish, + to: string, + overrides?: Overrides & { from?: string }, + ): Promise; + + withdrawETHWithPermit( + arg0: string, + amount: BigNumberish, + to: string, + deadline: BigNumberish, + permitV: BigNumberish, + permitR: BytesLike, + permitS: BytesLike, + overrides?: Overrides & { from?: string }, + ): Promise; + }; +} diff --git a/packages/contract-helpers/src/v3-wethgateway-contract/typechain/WrappedTokenGatewayV3__factory.ts b/packages/contract-helpers/src/v3-wethgateway-contract/typechain/WrappedTokenGatewayV3__factory.ts new file mode 100644 index 00000000..7dec9754 --- /dev/null +++ b/packages/contract-helpers/src/v3-wethgateway-contract/typechain/WrappedTokenGatewayV3__factory.ts @@ -0,0 +1,299 @@ +/* Autogenerated file. Do not edit manually. */ +/* eslint-disable */ + +import { Contract, Signer, utils } from 'ethers'; +import type { Provider } from '@ethersproject/providers'; +import type { + WrappedTokenGatewayV3, + WrappedTokenGatewayV3Interface, +} from './WrappedTokenGatewayV3'; + +const _abi = [ + { + inputs: [ + { + internalType: 'address', + name: 'weth', + type: 'address', + }, + { + internalType: 'address', + name: 'owner', + type: 'address', + }, + { + internalType: 'contract IPool', + name: 'pool', + type: 'address', + }, + ], + stateMutability: 'nonpayable', + type: 'constructor', + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: 'address', + name: 'previousOwner', + type: 'address', + }, + { + indexed: true, + internalType: 'address', + name: 'newOwner', + type: 'address', + }, + ], + name: 'OwnershipTransferred', + type: 'event', + }, + { + stateMutability: 'payable', + type: 'fallback', + }, + { + inputs: [ + { + internalType: 'address', + name: '', + type: 'address', + }, + { + internalType: 'uint256', + name: 'amount', + type: 'uint256', + }, + { + internalType: 'uint16', + name: 'referralCode', + type: 'uint16', + }, + ], + name: 'borrowETH', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [ + { + internalType: 'address', + name: '', + type: 'address', + }, + { + internalType: 'address', + name: 'onBehalfOf', + type: 'address', + }, + { + internalType: 'uint16', + name: 'referralCode', + type: 'uint16', + }, + ], + name: 'depositETH', + outputs: [], + stateMutability: 'payable', + type: 'function', + }, + { + inputs: [ + { + internalType: 'address', + name: 'to', + type: 'address', + }, + { + internalType: 'uint256', + name: 'amount', + type: 'uint256', + }, + ], + name: 'emergencyEtherTransfer', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [ + { + internalType: 'address', + name: 'token', + type: 'address', + }, + { + internalType: 'address', + name: 'to', + type: 'address', + }, + { + internalType: 'uint256', + name: 'amount', + type: 'uint256', + }, + ], + name: 'emergencyTokenTransfer', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [], + name: 'getWETHAddress', + outputs: [ + { + internalType: 'address', + name: '', + type: 'address', + }, + ], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'owner', + outputs: [ + { + internalType: 'address', + name: '', + type: 'address', + }, + ], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'renounceOwnership', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [ + { + internalType: 'address', + name: '', + type: 'address', + }, + { + internalType: 'uint256', + name: 'amount', + type: 'uint256', + }, + { + internalType: 'address', + name: 'onBehalfOf', + type: 'address', + }, + ], + name: 'repayETH', + outputs: [], + stateMutability: 'payable', + type: 'function', + }, + { + inputs: [ + { + internalType: 'address', + name: 'newOwner', + type: 'address', + }, + ], + name: 'transferOwnership', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [ + { + internalType: 'address', + name: '', + type: 'address', + }, + { + internalType: 'uint256', + name: 'amount', + type: 'uint256', + }, + { + internalType: 'address', + name: 'to', + type: 'address', + }, + ], + name: 'withdrawETH', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [ + { + internalType: 'address', + name: '', + type: 'address', + }, + { + internalType: 'uint256', + name: 'amount', + type: 'uint256', + }, + { + internalType: 'address', + name: 'to', + type: 'address', + }, + { + internalType: 'uint256', + name: 'deadline', + type: 'uint256', + }, + { + internalType: 'uint8', + name: 'permitV', + type: 'uint8', + }, + { + internalType: 'bytes32', + name: 'permitR', + type: 'bytes32', + }, + { + internalType: 'bytes32', + name: 'permitS', + type: 'bytes32', + }, + ], + name: 'withdrawETHWithPermit', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + stateMutability: 'payable', + type: 'receive', + }, +] as const; + +export class WrappedTokenGatewayV3__factory { + static readonly abi = _abi; + static createInterface(): WrappedTokenGatewayV3Interface { + return new utils.Interface(_abi) as WrappedTokenGatewayV3Interface; + } + static connect( + address: string, + signerOrProvider: Signer | Provider, + ): WrappedTokenGatewayV3 { + return new Contract( + address, + _abi, + signerOrProvider, + ) as WrappedTokenGatewayV3; + } +} diff --git a/packages/contract-helpers/src/v3-wethgateway-contract/typechain/common.ts b/packages/contract-helpers/src/v3-wethgateway-contract/typechain/common.ts new file mode 100644 index 00000000..0fb82cd3 --- /dev/null +++ b/packages/contract-helpers/src/v3-wethgateway-contract/typechain/common.ts @@ -0,0 +1,43 @@ +/* Autogenerated file. Do not edit manually. */ +/* eslint-disable */ +import type { Listener } from '@ethersproject/providers'; +import type { Event, EventFilter } from 'ethers'; + +export interface TypedEvent< + TArgsArray extends Array = any, + TArgsObject = any, +> extends Event { + args: TArgsArray & TArgsObject; +} + +export interface TypedEventFilter<_TEvent extends TypedEvent> + extends EventFilter {} + +export interface TypedListener { + (...listenerArg: [...__TypechainArgsArray, TEvent]): void; +} + +type __TypechainArgsArray = T extends TypedEvent ? U : never; + +export interface OnEvent { + ( + eventFilter: TypedEventFilter, + listener: TypedListener, + ): TRes; + (eventName: string, listener: Listener): TRes; +} + +export type MinEthersFactory = { + deploy(...a: ARGS[]): Promise; +}; + +export type GetContractTypeFromFactory = F extends MinEthersFactory< + infer C, + any +> + ? C + : never; + +export type GetARGsTypeFromFactory = F extends MinEthersFactory + ? Parameters + : never; diff --git a/packages/contract-helpers/src/v3-wethgateway-contract/wethGateway.test.ts b/packages/contract-helpers/src/v3-wethgateway-contract/wethGateway.test.ts new file mode 100644 index 00000000..29886cb9 --- /dev/null +++ b/packages/contract-helpers/src/v3-wethgateway-contract/wethGateway.test.ts @@ -0,0 +1,1113 @@ +import { BigNumber, constants, providers, utils } from 'ethers'; +import { eEthereumTxType, GasType, transactionType } from '../commons/types'; +import { valueToWei } from '../commons/utils'; +import { ERC20Service } from '../erc20-contract'; +import { WETHGatewayService } from './index'; + +jest.mock('../commons/gasStation', () => { + return { + __esModule: true, + estimateGasByNetwork: jest + .fn() + .mockImplementation(async () => Promise.resolve(BigNumber.from(1))), + estimateGas: jest.fn(async () => Promise.resolve(BigNumber.from(1))), + }; +}); + +describe('WethGatewayService', () => { + const wethGatewayAddress = '0x0000000000000000000000000000000000000001'; + const lendingPool = '0x0000000000000000000000000000000000000002'; + describe('Initialization', () => { + const provider: providers.Provider = new providers.JsonRpcProvider(); + const erc20Service = new ERC20Service(provider); + it('Expects to be initialized', () => { + expect( + () => + new WETHGatewayService(provider, erc20Service, wethGatewayAddress), + ).not.toThrow(); + }); + it('Expects to initialize without wethgateway address', () => { + expect( + () => new WETHGatewayService(provider, erc20Service), + ).not.toThrow(); + }); + }); + describe('generateDepositEthTxData', () => { + it('generates depositETH tx data', () => { + const provider: providers.Provider = new providers.JsonRpcProvider(); + const erc20Service = new ERC20Service(provider); + const weth = new WETHGatewayService( + provider, + erc20Service, + wethGatewayAddress, + ); + const user = '0x0000000000000000000000000000000000000003'; + const txData = weth.generateDepositEthTxData({ + lendingPool, + user, + amount: '1', + }); + + expect(txData.to).toEqual(wethGatewayAddress); + expect(txData.from).toEqual(user); + expect(txData.data).toEqual( + '0x474cf53d000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000030000000000000000000000000000000000000000000000000000000000000000', + ); + + const onBehalfOf = '0x0000000000000000000000000000000000000004'; + + const txDataUpdatedParams = weth.generateDepositEthTxData({ + lendingPool, + user, + amount: '1', + onBehalfOf, + referralCode: '0', + }); + + expect(txDataUpdatedParams.to).toEqual(wethGatewayAddress); + expect(txDataUpdatedParams.from).toEqual(user); + expect(txDataUpdatedParams.data).toEqual( + '0x474cf53d000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000000', + ); + }); + }); + describe('generateBorrowEthTxData', () => { + it('generates borrowETH tx data', async () => { + const provider: providers.Provider = new providers.JsonRpcProvider(); + const erc20Service = new ERC20Service(provider); + const weth = new WETHGatewayService( + provider, + erc20Service, + wethGatewayAddress, + ); + const user = '0x0000000000000000000000000000000000000003'; + const txData = weth.generateBorrowEthTxData({ + lendingPool, + user, + amount: '1', + referralCode: '0', + }); + + expect(txData.to).toEqual(wethGatewayAddress); + expect(txData.from).toEqual(user); + expect(txData.data).toEqual( + '0xe74f7b85000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000', + ); + + const txDataStable = weth.generateBorrowEthTxData({ + lendingPool, + user, + amount: '1', + debtTokenAddress: '', + }); + + expect(txDataStable.data).toEqual( + '0xe74f7b85000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000', + ); + }); + }); + describe('generateRepayEthTxData', () => { + it('generates repayETH tx data with variable debt', () => { + const provider: providers.Provider = new providers.JsonRpcProvider(); + const erc20Service = new ERC20Service(provider); + const weth = new WETHGatewayService( + provider, + erc20Service, + wethGatewayAddress, + ); + const user = '0x0000000000000000000000000000000000000003'; + const txData = weth.generateRepayEthTxData({ + lendingPool, + user, + amount: '1', + }); + + expect(txData.to).toEqual(wethGatewayAddress); + expect(txData.from).toEqual(user); + expect(txData.data).toEqual( + '0xbcc3c255000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000003', + ); + + const onBehalfOf = '0x0000000000000000000000000000000000000004'; + + const txDataUpdatedParams = weth.generateRepayEthTxData({ + lendingPool, + user, + amount: '1', + onBehalfOf, + }); + + expect(txDataUpdatedParams.to).toEqual(wethGatewayAddress); + expect(txDataUpdatedParams.from).toEqual(user); + expect(txDataUpdatedParams.data).toEqual( + '0xbcc3c255000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000004', + ); + }); + }); + describe('depositETH', () => { + const user = '0x0000000000000000000000000000000000000003'; + const onBehalfOf = '0x0000000000000000000000000000000000000004'; + const amount = '123.456'; + const referralCode = '0'; + const provider: providers.Provider = new providers.JsonRpcProvider(); + jest + .spyOn(provider, 'getGasPrice') + .mockImplementation(async () => Promise.resolve(BigNumber.from(1))); + const erc20Service = new ERC20Service(provider); + afterEach(() => { + jest.clearAllMocks(); + }); + + it('Expects the deposit tx object to be correct with all params', async () => { + const weth = new WETHGatewayService( + provider, + erc20Service, + wethGatewayAddress, + ); + const txObj = weth.depositETH({ + lendingPool, + user, + amount, + onBehalfOf, + referralCode, + }); + expect(txObj.length).toEqual(1); + expect(txObj[0].txType).toEqual(eEthereumTxType.DLP_ACTION); + + const tx: transactionType = await txObj[0].tx(); + expect(tx.to).toEqual(wethGatewayAddress); + expect(tx.from).toEqual(user); + expect(tx.gasLimit).toEqual(BigNumber.from(1)); + + const decoded = utils.defaultAbiCoder.decode( + ['address', 'address', 'uint16'], + utils.hexDataSlice(tx.data ?? '', 4), + ); + + expect(decoded[0]).toEqual(lendingPool); + expect(decoded[1]).toEqual(onBehalfOf); + expect(decoded[2]).toEqual(Number(referralCode)); + + // gas price + const gasPrice: GasType | null = await txObj[0].gas(); + expect(gasPrice).not.toBeNull(); + expect(gasPrice?.gasLimit).toEqual('1'); + expect(gasPrice?.gasPrice).toEqual('1'); + }); + it('Expects the deposit tx object to be correct without onBehalfOf', async () => { + const weth = new WETHGatewayService( + provider, + erc20Service, + wethGatewayAddress, + ); + const txObj = weth.depositETH({ + lendingPool, + user, + amount, + referralCode, + }); + expect(txObj.length).toEqual(1); + expect(txObj[0].txType).toEqual(eEthereumTxType.DLP_ACTION); + + const tx: transactionType = await txObj[0].tx(); + expect(tx.to).toEqual(wethGatewayAddress); + expect(tx.from).toEqual(user); + expect(tx.gasLimit).toEqual(BigNumber.from(1)); + + const decoded = utils.defaultAbiCoder.decode( + ['address', 'address', 'uint16'], + utils.hexDataSlice(tx.data ?? '', 4), + ); + + expect(decoded[0]).toEqual(lendingPool); + expect(decoded[1]).toEqual(user); + expect(decoded[2]).toEqual(Number(referralCode)); + + // gas price + const gasPrice: GasType | null = await txObj[0].gas(); + expect(gasPrice).not.toBeNull(); + expect(gasPrice?.gasLimit).toEqual('1'); + expect(gasPrice?.gasPrice).toEqual('1'); + }); + it('Expects the deposit tx object to be correct without referralCode', async () => { + const weth = new WETHGatewayService( + provider, + erc20Service, + wethGatewayAddress, + ); + const txObj = weth.depositETH({ + lendingPool, + user, + amount, + }); + expect(txObj.length).toEqual(1); + expect(txObj[0].txType).toEqual(eEthereumTxType.DLP_ACTION); + + const tx: transactionType = await txObj[0].tx(); + expect(tx.to).toEqual(wethGatewayAddress); + expect(tx.from).toEqual(user); + expect(tx.gasLimit).toEqual(BigNumber.from(1)); + + const decoded = utils.defaultAbiCoder.decode( + ['address', 'address', 'uint16'], + utils.hexDataSlice(tx.data ?? '', 4), + ); + + expect(decoded[0]).toEqual(lendingPool); + expect(decoded[1]).toEqual(user); + expect(decoded[2]).toEqual(0); + + // gas price + const gasPrice: GasType | null = await txObj[0].gas(); + expect(gasPrice).not.toBeNull(); + expect(gasPrice?.gasLimit).toEqual('1'); + expect(gasPrice?.gasPrice).toEqual('1'); + }); + it('Expects to fail when initialized without gateway address', () => { + const weth = new WETHGatewayService(provider, erc20Service); + + const txObj = weth.depositETH({ + lendingPool, + user, + amount, + }); + + expect(txObj.length).toEqual(0); + }); + it('Expects to fail when user is not address', () => { + const weth = new WETHGatewayService( + provider, + erc20Service, + wethGatewayAddress, + ); + const user = 'asdf'; + expect(() => + weth.depositETH({ + lendingPool, + user, + amount, + onBehalfOf, + referralCode, + }), + ).toThrowError(`Address: ${user} is not a valid ethereum Address`); + }); + it('Expects to fail when lendingPool is not address', () => { + const weth = new WETHGatewayService( + provider, + erc20Service, + wethGatewayAddress, + ); + const lendingPool = 'asdf'; + expect(() => + weth.depositETH({ + lendingPool, + user, + amount, + onBehalfOf, + referralCode, + }), + ).toThrowError(`Address: ${lendingPool} is not a valid ethereum Address`); + }); + it('Expects to fail when amount is not positive', () => { + const weth = new WETHGatewayService( + provider, + erc20Service, + wethGatewayAddress, + ); + const amount = '0'; + expect(() => + weth.depositETH({ + lendingPool, + user, + amount, + onBehalfOf, + referralCode, + }), + ).toThrowError(`Amount: ${amount} needs to be greater than 0`); + }); + it('Expects to fail when amount is not number', () => { + const weth = new WETHGatewayService( + provider, + erc20Service, + wethGatewayAddress, + ); + const amount = 'asdf'; + expect(() => + weth.depositETH({ + lendingPool, + user, + amount, + onBehalfOf, + referralCode, + }), + ).toThrowError(`Amount: ${amount} needs to be greater than 0`); + }); + it('Expects to fail when onBehalfOf is not address', () => { + const weth = new WETHGatewayService( + provider, + erc20Service, + wethGatewayAddress, + ); + const onBehalfOf = 'asdf'; + expect(() => + weth.depositETH({ + lendingPool, + user, + amount, + onBehalfOf, + referralCode, + }), + ).toThrowError(`Address: ${onBehalfOf} is not a valid ethereum Address`); + }); + it('Expects to fail when referral is not number', () => { + const weth = new WETHGatewayService( + provider, + erc20Service, + wethGatewayAddress, + ); + const referralCode = 'asdf'; + expect(() => + weth.depositETH({ + lendingPool, + user, + amount, + onBehalfOf, + referralCode, + }), + ).toThrowError( + `Amount: ${referralCode} needs to be greater or equal than 0`, + ); + }); + }); + describe('withdrawETH', () => { + const user = '0x0000000000000000000000000000000000000003'; + const debtTokenAddress = '0x0000000000000000000000000000000000000005'; + const amount = '123.456'; + const referralCode = '0'; + const provider: providers.Provider = new providers.JsonRpcProvider(); + jest + .spyOn(provider, 'getGasPrice') + .mockImplementation(async () => Promise.resolve(BigNumber.from(1))); + const erc20Service = new ERC20Service(provider); + + afterEach(() => { + jest.clearAllMocks(); + }); + it('Expects the borrow tx object to be correct with all params and variable rate without approval', async () => { + const weth = new WETHGatewayService( + provider, + erc20Service, + wethGatewayAddress, + ); + + const isApprovedSpy = jest + .spyOn(weth.baseDebtTokenService, 'isDelegationApproved') + .mockImplementation(async () => Promise.resolve(false)); + + const approveSpy = jest + .spyOn(weth.baseDebtTokenService, 'approveDelegation') + .mockImplementation(() => ({ + txType: eEthereumTxType.ERC20_APPROVAL, + tx: async () => ({}), + gas: async () => ({ + gasLimit: '1', + gasPrice: '1', + }), + })); + const txObj = await weth.borrowETH({ + lendingPool, + user, + amount, + debtTokenAddress, + referralCode, + }); + + await expect(async () => + weth.borrowETH({ + lendingPool, + user, + amount, + referralCode, + }), + ).rejects.toThrowError( + `To borrow ETH you need to pass the variable WETH debt Token Address`, + ); + + expect(isApprovedSpy).toHaveBeenCalled(); + expect(approveSpy).toHaveBeenCalled(); + expect(txObj.length).toEqual(2); + expect(txObj[1].txType).toEqual(eEthereumTxType.DLP_ACTION); + + const tx: transactionType = await txObj[1].tx(); + expect(tx.to).toEqual(wethGatewayAddress); + expect(tx.from).toEqual(user); + expect(tx.gasLimit).toEqual(BigNumber.from(1)); + + const decoded = utils.defaultAbiCoder.decode( + ['address', 'uint256', 'uint16'], + utils.hexDataSlice(tx.data ?? '', 4), + ); + + expect(decoded[0]).toEqual(lendingPool); + expect(decoded[1]).toEqual(BigNumber.from(valueToWei(amount, 18))); + expect(decoded[2]).toEqual(Number(referralCode)); + + // gas price + const gasPrice: GasType | null = await txObj[1].gas(); + expect(gasPrice).not.toBeNull(); + expect(gasPrice?.gasLimit).toEqual('450000'); + expect(gasPrice?.gasPrice).toEqual('1'); + }); + it('Expects the borrow tx object to be correct with all params and variable rate already approved', async () => { + const weth = new WETHGatewayService( + provider, + erc20Service, + wethGatewayAddress, + ); + + const isApprovedSpy = jest + .spyOn(weth.baseDebtTokenService, 'isDelegationApproved') + .mockImplementation(async () => Promise.resolve(true)); + + const txObj = await weth.borrowETH({ + lendingPool, + user, + amount, + debtTokenAddress, + referralCode, + }); + + expect(isApprovedSpy).toHaveBeenCalled(); + expect(txObj.length).toEqual(1); + expect(txObj[0].txType).toEqual(eEthereumTxType.DLP_ACTION); + + const tx: transactionType = await txObj[0].tx(); + expect(tx.to).toEqual(wethGatewayAddress); + expect(tx.from).toEqual(user); + expect(tx.gasLimit).toEqual(BigNumber.from(1)); + + const decoded = utils.defaultAbiCoder.decode( + ['address', 'uint256', 'uint16'], + utils.hexDataSlice(tx.data ?? '', 4), + ); + + expect(decoded[0]).toEqual(lendingPool); + expect(decoded[1]).toEqual(BigNumber.from(valueToWei(amount, 18))); + expect(decoded[2]).toEqual(Number(referralCode)); + + // gas price + const gasPrice: GasType | null = await txObj[0].gas(); + expect(gasPrice).not.toBeNull(); + expect(gasPrice?.gasLimit).toEqual('1'); + expect(gasPrice?.gasPrice).toEqual('1'); + }); + it('Expects the borrow tx object to be correct without referralCode', async () => { + const weth = new WETHGatewayService( + provider, + erc20Service, + wethGatewayAddress, + ); + + const isApprovedSpy = jest + .spyOn(weth.baseDebtTokenService, 'isDelegationApproved') + .mockImplementation(async () => Promise.resolve(true)); + + const txObj = await weth.borrowETH({ + lendingPool, + user, + amount, + debtTokenAddress, + }); + + expect(isApprovedSpy).toHaveBeenCalled(); + expect(txObj.length).toEqual(1); + expect(txObj[0].txType).toEqual(eEthereumTxType.DLP_ACTION); + + const tx: transactionType = await txObj[0].tx(); + expect(tx.to).toEqual(wethGatewayAddress); + expect(tx.from).toEqual(user); + expect(tx.gasLimit).toEqual(BigNumber.from(1)); + + const decoded = utils.defaultAbiCoder.decode( + ['address', 'uint256', 'uint16'], + utils.hexDataSlice(tx.data ?? '', 4), + ); + + expect(decoded[0]).toEqual(lendingPool); + expect(decoded[1]).toEqual(BigNumber.from(valueToWei(amount, 18))); + expect(decoded[2]).toEqual(0); + + // gas price + const gasPrice: GasType | null = await txObj[0].gas(); + expect(gasPrice).not.toBeNull(); + expect(gasPrice?.gasLimit).toEqual('1'); + expect(gasPrice?.gasPrice).toEqual('1'); + }); + it('Expects to fail when initialized without gateway address', async () => { + const weth = new WETHGatewayService(provider, erc20Service); + + const txObj = await weth.borrowETH({ + lendingPool, + user, + amount, + debtTokenAddress, + referralCode, + }); + + expect(txObj.length).toEqual(0); + }); + it('Expects to fail when user is not address', async () => { + const weth = new WETHGatewayService( + provider, + erc20Service, + wethGatewayAddress, + ); + const user = 'asdf'; + await expect(async () => + weth.borrowETH({ + lendingPool, + user, + amount, + debtTokenAddress, + referralCode, + }), + ).rejects.toThrowError( + `Address: ${user} is not a valid ethereum Address`, + ); + }); + it('Expects to fail when lendingPool is not address', async () => { + const weth = new WETHGatewayService( + provider, + erc20Service, + wethGatewayAddress, + ); + const lendingPool = 'asdf'; + await expect(async () => + weth.borrowETH({ + lendingPool, + user, + amount, + debtTokenAddress, + referralCode, + }), + ).rejects.toThrowError( + `Address: ${lendingPool} is not a valid ethereum Address`, + ); + }); + it('Expects to fail when amount is not positive', async () => { + const weth = new WETHGatewayService( + provider, + erc20Service, + wethGatewayAddress, + ); + const amount = '0'; + await expect(async () => + weth.borrowETH({ + lendingPool, + user, + amount, + debtTokenAddress, + referralCode, + }), + ).rejects.toThrowError(`Amount: ${amount} needs to be greater than 0`); + }); + it('Expects to fail when amount is not number', async () => { + const weth = new WETHGatewayService( + provider, + erc20Service, + wethGatewayAddress, + ); + const amount = 'asdf'; + await expect(async () => + weth.borrowETH({ + lendingPool, + user, + amount, + debtTokenAddress, + referralCode, + }), + ).rejects.toThrowError(`Amount: ${amount} needs to be greater than 0`); + }); + it('Expects to fail when debtTokenAddress is not address', async () => { + const weth = new WETHGatewayService( + provider, + erc20Service, + wethGatewayAddress, + ); + const debtTokenAddress = 'asdf'; + await expect(async () => + weth.borrowETH({ + lendingPool, + user, + amount, + debtTokenAddress, + referralCode, + }), + ).rejects.toThrowError( + `Address: ${debtTokenAddress} is not a valid ethereum Address`, + ); + }); + it('Expects to fail when referral is not number', async () => { + const weth = new WETHGatewayService( + provider, + erc20Service, + wethGatewayAddress, + ); + const referralCode = 'asdf'; + await expect(async () => + weth.borrowETH({ + lendingPool, + user, + amount, + debtTokenAddress, + referralCode, + }), + ).rejects.toThrowError( + `Amount: ${referralCode} needs to be greater or equal than 0`, + ); + }); + }); + describe('repayETH', () => { + const user = '0x0000000000000000000000000000000000000003'; + const onBehalfOf = '0x0000000000000000000000000000000000000004'; + const aTokenAddress = '0x0000000000000000000000000000000000000005'; + const amount = '123.456'; + const provider: providers.Provider = new providers.JsonRpcProvider(); + jest + .spyOn(provider, 'getGasPrice') + .mockImplementation(async () => Promise.resolve(BigNumber.from(1))); + const erc20Service = new ERC20Service(provider); + + afterEach(() => { + jest.clearAllMocks(); + }); + it('Expects the withdraw tx object to be correct with all params and not approved', async () => { + const isApprovedSpy = jest + .spyOn(erc20Service, 'isApproved') + .mockImplementation(async () => Promise.resolve(false)); + const approveSpy = jest + .spyOn(erc20Service, 'approve') + .mockImplementation(() => ({ + txType: eEthereumTxType.ERC20_APPROVAL, + tx: async () => ({}), + gas: async () => ({ + gasLimit: '1', + gasPrice: '1', + }), + })); + + const weth = new WETHGatewayService( + provider, + erc20Service, + wethGatewayAddress, + ); + const txObj = await weth.withdrawETH({ + lendingPool, + user, + amount, + onBehalfOf, + aTokenAddress, + }); + + expect(isApprovedSpy).toHaveBeenCalled(); + expect(approveSpy).toHaveBeenCalled(); + expect(txObj.length).toEqual(2); + expect(txObj[1].txType).toEqual(eEthereumTxType.DLP_ACTION); + + const tx: transactionType = await txObj[1].tx(); + expect(tx.to).toEqual(wethGatewayAddress); + expect(tx.from).toEqual(user); + expect(tx.gasLimit).toEqual(BigNumber.from(1)); + + const decoded = utils.defaultAbiCoder.decode( + ['address', 'uint256', 'address'], + utils.hexDataSlice(tx.data ?? '', 4), + ); + + expect(decoded[0]).toEqual(lendingPool); + expect(decoded[1]).toEqual(BigNumber.from(valueToWei(amount, 18))); + expect(decoded[2]).toEqual(onBehalfOf); + + // gas price + const gasPrice: GasType | null = await txObj[1].gas(); + expect(gasPrice).not.toBeNull(); + expect(gasPrice?.gasLimit).toEqual('640000'); + expect(gasPrice?.gasPrice).toEqual('1'); + }); + it('Expects the withdraw tx object to be correct with all params and amount -1 and approved', async () => { + const isApprovedSpy = jest + .spyOn(erc20Service, 'isApproved') + .mockImplementation(async () => Promise.resolve(true)); + + const weth = new WETHGatewayService( + provider, + erc20Service, + wethGatewayAddress, + ); + const amount = '-1'; + const txObj = await weth.withdrawETH({ + lendingPool, + user, + amount, + onBehalfOf, + aTokenAddress, + }); + + expect(isApprovedSpy).toHaveBeenCalled(); + expect(txObj.length).toEqual(1); + expect(txObj[0].txType).toEqual(eEthereumTxType.DLP_ACTION); + + const tx: transactionType = await txObj[0].tx(); + expect(tx.to).toEqual(wethGatewayAddress); + expect(tx.from).toEqual(user); + expect(tx.gasLimit).toEqual(BigNumber.from(1)); + + const decoded = utils.defaultAbiCoder.decode( + ['address', 'uint256', 'address'], + utils.hexDataSlice(tx.data ?? '', 4), + ); + + expect(decoded[0]).toEqual(lendingPool); + expect(decoded[1]).toEqual(constants.MaxUint256); + expect(decoded[2]).toEqual(onBehalfOf); + + // gas price + const gasPrice: GasType | null = await txObj[0].gas(); + expect(gasPrice).not.toBeNull(); + expect(gasPrice?.gasLimit).toEqual('1'); + expect(gasPrice?.gasPrice).toEqual('1'); + }); + it('Expects the withdraw tx object to be correct without onBehalfOf', async () => { + const isApprovedSpy = jest + .spyOn(erc20Service, 'isApproved') + .mockImplementation(async () => Promise.resolve(true)); + + const weth = new WETHGatewayService( + provider, + erc20Service, + wethGatewayAddress, + ); + + const txObj = await weth.withdrawETH({ + lendingPool, + user, + amount, + aTokenAddress, + }); + + expect(isApprovedSpy).toHaveBeenCalled(); + expect(txObj.length).toEqual(1); + expect(txObj[0].txType).toEqual(eEthereumTxType.DLP_ACTION); + + const tx: transactionType = await txObj[0].tx(); + expect(tx.to).toEqual(wethGatewayAddress); + expect(tx.from).toEqual(user); + expect(tx.gasLimit).toEqual(BigNumber.from(1)); + + const decoded = utils.defaultAbiCoder.decode( + ['address', 'uint256', 'address'], + utils.hexDataSlice(tx.data ?? '', 4), + ); + + expect(decoded[0]).toEqual(lendingPool); + expect(decoded[1]).toEqual(BigNumber.from(valueToWei(amount, 18))); + expect(decoded[2]).toEqual(user); + + // gas price + const gasPrice: GasType | null = await txObj[0].gas(); + expect(gasPrice).not.toBeNull(); + expect(gasPrice?.gasLimit).toEqual('1'); + expect(gasPrice?.gasPrice).toEqual('1'); + }); + it('Expects to fail when initialized without gateway address', async () => { + const weth = new WETHGatewayService(provider, erc20Service); + + const txObj = await weth.withdrawETH({ + lendingPool, + user, + amount, + aTokenAddress, + }); + + expect(txObj.length).toEqual(0); + }); + it('Expects to fail when user is not address', async () => { + const weth = new WETHGatewayService( + provider, + erc20Service, + wethGatewayAddress, + ); + const user = 'asdf'; + await expect(async () => + weth.withdrawETH({ + lendingPool, + user, + amount, + aTokenAddress, + }), + ).rejects.toThrowError( + `Address: ${user} is not a valid ethereum Address`, + ); + }); + it('Expects to fail when lendingPool is not address', async () => { + const weth = new WETHGatewayService( + provider, + erc20Service, + wethGatewayAddress, + ); + const lendingPool = 'asdf'; + await expect(async () => + weth.withdrawETH({ + lendingPool, + user, + amount, + aTokenAddress, + }), + ).rejects.toThrowError( + `Address: ${lendingPool} is not a valid ethereum Address`, + ); + }); + it('Expects to fail when amount is not positive', async () => { + const weth = new WETHGatewayService( + provider, + erc20Service, + wethGatewayAddress, + ); + const amount = '0'; + await expect(async () => + weth.withdrawETH({ + lendingPool, + user, + amount, + aTokenAddress, + }), + ).rejects.toThrowError(`Amount: ${amount} needs to be greater than 0`); + }); + it('Expects to fail when amount is not number', async () => { + const weth = new WETHGatewayService( + provider, + erc20Service, + wethGatewayAddress, + ); + const amount = 'asdf'; + await expect(async () => + weth.withdrawETH({ + lendingPool, + user, + amount, + aTokenAddress, + }), + ).rejects.toThrowError(`Amount: ${amount} needs to be greater than 0`); + }); + it('Expects to fail when aTokenAddress is not address', async () => { + const weth = new WETHGatewayService( + provider, + erc20Service, + wethGatewayAddress, + ); + const aTokenAddress = 'asdf'; + await expect(async () => + weth.withdrawETH({ + lendingPool, + user, + amount, + aTokenAddress, + }), + ).rejects.toThrowError( + `Address: ${aTokenAddress} is not a valid ethereum Address`, + ); + }); + it('Expects to fail when onBehalfOf is not address', async () => { + const weth = new WETHGatewayService( + provider, + erc20Service, + wethGatewayAddress, + ); + const onBehalfOf = 'asdf'; + await expect(async () => + weth.withdrawETH({ + lendingPool, + user, + amount, + aTokenAddress, + onBehalfOf, + }), + ).rejects.toThrowError( + `Address: ${onBehalfOf} is not a valid ethereum Address`, + ); + }); + }); + describe('borrowETH', () => { + const user = '0x0000000000000000000000000000000000000003'; + const onBehalfOf = '0x0000000000000000000000000000000000000004'; + const amount = '123.456'; + const provider: providers.Provider = new providers.JsonRpcProvider(); + jest + .spyOn(provider, 'getGasPrice') + .mockImplementation(async () => Promise.resolve(BigNumber.from(1))); + const erc20Service = new ERC20Service(provider); + + afterEach(() => { + jest.clearAllMocks(); + }); + it('Expects the repay tx object to be correct with all params', async () => { + const weth = new WETHGatewayService( + provider, + erc20Service, + wethGatewayAddress, + ); + const txObj = weth.repayETH({ + lendingPool, + user, + amount, + onBehalfOf, + }); + + expect(txObj.length).toEqual(1); + expect(txObj[0].txType).toEqual(eEthereumTxType.DLP_ACTION); + + const tx: transactionType = await txObj[0].tx(); + expect(tx.to).toEqual(wethGatewayAddress); + expect(tx.from).toEqual(user); + expect(tx.gasLimit).toEqual(BigNumber.from(1)); + + const decoded = utils.defaultAbiCoder.decode( + ['address', 'uint256', 'address'], + utils.hexDataSlice(tx.data ?? '', 4), + ); + + expect(decoded[0]).toEqual(lendingPool); + expect(decoded[1]).toEqual(BigNumber.from(valueToWei(amount, 18))); + expect(decoded[2]).toEqual(onBehalfOf); + + // gas price + const gasPrice: GasType | null = await txObj[0].gas(); + expect(gasPrice).not.toBeNull(); + expect(gasPrice?.gasLimit).toEqual('1'); + expect(gasPrice?.gasPrice).toEqual('1'); + }); + it('Expects the repay tx object to be correct without onBehalfOf', async () => { + const weth = new WETHGatewayService( + provider, + erc20Service, + wethGatewayAddress, + ); + + const txObj = weth.repayETH({ + lendingPool, + user, + amount, + }); + + expect(txObj.length).toEqual(1); + expect(txObj[0].txType).toEqual(eEthereumTxType.DLP_ACTION); + + const tx: transactionType = await txObj[0].tx(); + expect(tx.to).toEqual(wethGatewayAddress); + expect(tx.from).toEqual(user); + expect(tx.gasLimit).toEqual(BigNumber.from(1)); + + const decoded = utils.defaultAbiCoder.decode( + ['address', 'uint256', 'address'], + utils.hexDataSlice(tx.data ?? '', 4), + ); + + expect(decoded[0]).toEqual(lendingPool); + expect(decoded[1]).toEqual(BigNumber.from(valueToWei(amount, 18))); + expect(decoded[2]).toEqual(user); + + // gas price + const gasPrice: GasType | null = await txObj[0].gas(); + expect(gasPrice).not.toBeNull(); + expect(gasPrice?.gasLimit).toEqual('1'); + expect(gasPrice?.gasPrice).toEqual('1'); + }); + it('Expects to fail when initialized without gateway address', () => { + const weth = new WETHGatewayService(provider, erc20Service); + + const txObj = weth.repayETH({ + lendingPool, + user, + amount, + }); + + expect(txObj.length).toEqual(0); + }); + it('Expects to fail when user is not address', () => { + const weth = new WETHGatewayService( + provider, + erc20Service, + wethGatewayAddress, + ); + const user = 'asdf'; + expect(() => + weth.repayETH({ + lendingPool, + user, + amount, + }), + ).toThrowError(`Address: ${user} is not a valid ethereum Address`); + }); + it('Expects to fail when lendingPool is not address', () => { + const weth = new WETHGatewayService( + provider, + erc20Service, + wethGatewayAddress, + ); + const lendingPool = 'asdf'; + expect(() => + weth.repayETH({ + lendingPool, + user, + amount, + }), + ).toThrowError(`Address: ${lendingPool} is not a valid ethereum Address`); + }); + it('Expects to fail when amount is not positive', () => { + const weth = new WETHGatewayService( + provider, + erc20Service, + wethGatewayAddress, + ); + const amount = '0'; + expect(() => + weth.repayETH({ + lendingPool, + user, + amount, + }), + ).toThrowError(`Amount: ${amount} needs to be greater than 0`); + }); + it('Expects to fail when amount is not number', () => { + const weth = new WETHGatewayService( + provider, + erc20Service, + wethGatewayAddress, + ); + const amount = 'asdf'; + expect(() => + weth.repayETH({ + lendingPool, + user, + amount, + }), + ).toThrowError(`Amount: ${amount} needs to be greater than 0`); + }); + it('Expects to fail when onBehalfOf is not address', () => { + const weth = new WETHGatewayService( + provider, + erc20Service, + wethGatewayAddress, + ); + const onBehalfOf = 'asdf'; + expect(() => + weth.repayETH({ + lendingPool, + user, + amount, + onBehalfOf, + }), + ).toThrowError(`Address: ${onBehalfOf} is not a valid ethereum Address`); + }); + }); +});