diff --git a/packages/contract-helpers/src/commons/validators/methodValidators.ts b/packages/contract-helpers/src/commons/validators/methodValidators.ts index 3c5bfaa1..9bf54fcd 100644 --- a/packages/contract-helpers/src/commons/validators/methodValidators.ts +++ b/packages/contract-helpers/src/commons/validators/methodValidators.ts @@ -365,6 +365,8 @@ export function ERC20Validator( amountGtThan0OrMinus1(target, propertyName, arguments); + amount0OrPositiveValidator(target, propertyName, arguments); + return method.apply(this, arguments); }; } diff --git a/packages/contract-helpers/src/erc20-2612/erc20_2612.test.ts b/packages/contract-helpers/src/erc20-2612/erc20_2612.test.ts index 454d0fd5..95804ecf 100644 --- a/packages/contract-helpers/src/erc20-2612/erc20_2612.test.ts +++ b/packages/contract-helpers/src/erc20-2612/erc20_2612.test.ts @@ -1,4 +1,5 @@ -import { BigNumber, providers } from 'ethers'; +import { BigNumber, constants, providers } from 'ethers'; +import { valueToWei } from '../commons/utils'; import { IERC202612 } from './typechain/IERC202612'; import { IERC202612__factory } from './typechain/IERC202612__factory'; import { ERC20_2612Interface, ERC20_2612Service } from './index'; @@ -78,4 +79,212 @@ describe('ERC20_2612', () => { ); }); }); + + describe('signERC20Approval', () => { + const user = '0x0000000000000000000000000000000000000006'; + const reserve = '0x0000000000000000000000000000000000000007'; + const spender = '0x0000000000000000000000000000000000000008'; + const amount = '123.456'; + const decimals = 18; + const deadline = Math.round(Date.now() / 1000 + 3600).toString(); + jest.spyOn(provider, 'getTransactionCount').mockResolvedValue(1); + jest.spyOn(provider, 'getNetwork').mockResolvedValue({ + name: 'mainnet', + chainId: 1, + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('Expects the permission string to be returned when all params', async () => { + const instance = new ERC20_2612Service(provider); + + jest.spyOn(instance.erc20Service, 'getTokenData').mockReturnValue( + Promise.resolve({ + name: 'mockToken', + decimals, + symbol: 'MT', + address: '0x0000000000000000000000000000000000000006', + }), + ); + + jest + .spyOn(instance.erc20Service, 'isApproved') + .mockReturnValue(Promise.resolve(false)); + + jest.spyOn(instance, 'getNonce').mockReturnValue(Promise.resolve(1)); + const signature: string = await instance.signERC20Approval({ + user, + reserve, + spender, + amount, + deadline, + }); + + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + const { primaryType, domain, message } = await JSON.parse(signature); + expect(primaryType).toEqual('Permit'); + expect(domain.name).toEqual('mockToken'); + expect(domain.chainId).toEqual(1); + + expect(message.owner).toEqual(user); + expect(message.spender).toEqual(spender); + expect(message.value).toEqual(valueToWei(amount, decimals)); + expect(message.nonce).toEqual(1); + expect(message.deadline).toEqual(deadline); + }); + it('Expects the permission string to be returned when all params and amount -1', async () => { + const instance = new ERC20_2612Service(provider); + + jest.spyOn(instance.erc20Service, 'getTokenData').mockReturnValue( + Promise.resolve({ + name: 'mockToken', + decimals, + symbol: 'MT', + address: '0x0000000000000000000000000000000000000006', + }), + ); + + jest + .spyOn(instance.erc20Service, 'isApproved') + .mockReturnValue(Promise.resolve(false)); + + jest.spyOn(instance, 'getNonce').mockReturnValue(Promise.resolve(1)); + + const amount = '-1'; + const signature: string = await instance.signERC20Approval({ + user, + reserve, + spender, + amount, + deadline, + }); + + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + const { primaryType, domain, message } = await JSON.parse(signature); + expect(primaryType).toEqual('Permit'); + expect(domain.name).toEqual('mockToken'); + expect(domain.chainId).toEqual(1); + + expect(message.owner).toEqual(user); + expect(message.spender).toEqual(spender); + expect(message.value).toEqual(constants.MaxUint256.toString()); + expect(message.nonce).toEqual(1); + expect(message.deadline).toEqual(deadline); + }); + it('Expects the permission string to be `` when no nonce', async () => { + const instance = new ERC20_2612Service(provider); + + jest.spyOn(instance.erc20Service, 'getTokenData').mockReturnValue( + Promise.resolve({ + name: 'mockToken', + decimals, + symbol: 'MT', + address: '0x0000000000000000000000000000000000000006', + }), + ); + + jest + .spyOn(instance.erc20Service, 'isApproved') + .mockReturnValue(Promise.resolve(false)); + jest.spyOn(instance, 'getNonce').mockReturnValue(Promise.resolve(null)); + + const signature: string = await instance.signERC20Approval({ + user, + reserve, + spender, + amount, + deadline, + }); + + expect(signature).toEqual(''); + }); + it('Expects the permission string to be `` when already approved', async () => { + const instance = new ERC20_2612Service(provider); + + jest.spyOn(instance.erc20Service, 'getTokenData').mockReturnValue( + Promise.resolve({ + name: 'mockToken', + decimals, + symbol: 'MT', + address: '0x0000000000000000000000000000000000000006', + }), + ); + + jest + .spyOn(instance.erc20Service, 'isApproved') + .mockReturnValue(Promise.resolve(true)); + + const signature: string = await instance.signERC20Approval({ + user, + reserve, + spender, + amount, + deadline, + }); + + expect(signature).toEqual(''); + }); + it('Expects to fail when user not eth address', async () => { + const instance = new ERC20_2612Service(provider); + + const user = 'asdf'; + await expect(async () => + instance.signERC20Approval({ + user, + reserve, + spender, + amount, + deadline, + }), + ).rejects.toThrowError( + `Address: ${user} is not a valid ethereum Address`, + ); + }); + it('Expects to fail when reserve not eth address', async () => { + const instance = new ERC20_2612Service(provider); + + const reserve = 'asdf'; + await expect(async () => + instance.signERC20Approval({ + user, + reserve, + spender, + amount, + deadline, + }), + ).rejects.toThrowError( + `Address: ${reserve} is not a valid ethereum Address`, + ); + }); + it('Expects to fail when amount not positive', async () => { + const instance = new ERC20_2612Service(provider); + + const amount = '0'; + await expect(async () => + instance.signERC20Approval({ + user, + reserve, + spender, + amount, + deadline, + }), + ).rejects.toThrowError(`Amount: ${amount} needs to be greater than 0`); + }); + it('Expects to fail when amount not number', async () => { + const instance = new ERC20_2612Service(provider); + + const amount = 'asdf'; + await expect(async () => + instance.signERC20Approval({ + user, + reserve, + spender, + amount, + deadline, + }), + ).rejects.toThrowError(`Amount: ${amount} needs to be greater than 0`); + }); + }); }); diff --git a/packages/contract-helpers/src/erc20-2612/index.ts b/packages/contract-helpers/src/erc20-2612/index.ts index 767dbb35..c702493a 100644 --- a/packages/contract-helpers/src/erc20-2612/index.ts +++ b/packages/contract-helpers/src/erc20-2612/index.ts @@ -1,23 +1,39 @@ -import { BigNumber, providers } from 'ethers'; +import { BigNumber, constants, providers } from 'ethers'; import BaseService from '../commons/BaseService'; +import { tEthereumAddress } from '../commons/types'; +import { valueToWei } from '../commons/utils'; import { ERC20Validator } from '../commons/validators/methodValidators'; -import { isEthAddress } from '../commons/validators/paramValidators'; +import { + isEthAddress, + isPositiveOrMinusOneAmount, +} from '../commons/validators/paramValidators'; +import { ERC20Service, IERC20ServiceInterface } from '../erc20-contract'; import { IERC202612 } from './typechain/IERC202612'; import { IERC202612__factory } from './typechain/IERC202612__factory'; export type GetNonceType = { token: string; owner: string }; +export type SignERC20ApprovalType = { + user: tEthereumAddress; + reserve: tEthereumAddress; + spender: tEthereumAddress; + amount: string; + deadline: string; +}; export interface ERC20_2612Interface { getNonce: (args: GetNonceType) => Promise; + signERC20Approval: (args: SignERC20ApprovalType) => Promise; } export class ERC20_2612Service extends BaseService implements ERC20_2612Interface { + readonly erc20Service: IERC20ServiceInterface; + constructor(provider: providers.Provider) { super(provider, IERC202612__factory); - + this.erc20Service = new ERC20Service(provider); this.getNonce = this.getNonce.bind(this); } @@ -45,4 +61,77 @@ export class ERC20_2612Service return null; } + + // Sign permit supply + @ERC20Validator + public async signERC20Approval( + @isEthAddress('user') + @isEthAddress('reserve') + @isEthAddress('spender') + @isPositiveOrMinusOneAmount('amount') + { user, reserve, spender, amount, deadline }: SignERC20ApprovalType, + ): Promise { + const { getTokenData, isApproved } = this.erc20Service; + const { name, decimals } = await getTokenData(reserve); + + const convertedAmount = + amount === '-1' + ? constants.MaxUint256.toString() + : valueToWei(amount, decimals); + + const approved = await isApproved({ + token: reserve, + user, + spender, + amount, + }); + + if (approved) { + return ''; + } + + const { chainId } = await this.provider.getNetwork(); + + const nonce = await this.getNonce({ + token: reserve, + owner: user, + }); + + if (nonce === null) { + return ''; + } + + const typeData = { + types: { + EIP712Domain: [ + { name: 'name', type: 'string' }, + { name: 'version', type: 'string' }, + { name: 'chainId', type: 'uint256' }, + { name: 'verifyingContract', type: 'address' }, + ], + Permit: [ + { name: 'owner', type: 'address' }, + { name: 'spender', type: 'address' }, + { name: 'value', type: 'uint256' }, + { name: 'nonce', type: 'uint256' }, + { name: 'deadline', type: 'uint256' }, + ], + }, + primaryType: 'Permit', + domain: { + name, + version: '1', + chainId, + verifyingContract: reserve, + }, + message: { + owner: user, + spender, + value: convertedAmount, + nonce, + deadline, + }, + }; + return JSON.stringify(typeData); + } } diff --git a/packages/contract-helpers/src/index.ts b/packages/contract-helpers/src/index.ts index 67fd01b2..a1b242ef 100644 --- a/packages/contract-helpers/src/index.ts +++ b/packages/contract-helpers/src/index.ts @@ -13,6 +13,7 @@ export * from './uiStakeDataProvider-contract'; export * from './incentive-controller'; export * from './incentive-controller-v2'; export * from './erc20-contract'; +export * from './erc20-2612'; export * from './lendingPool-contract'; export * from './faucet-contract'; export * from './staking-contract'; diff --git a/packages/contract-helpers/src/v3-pool-contract/index.ts b/packages/contract-helpers/src/v3-pool-contract/index.ts index 3673ed9e..a3194a3e 100644 --- a/packages/contract-helpers/src/v3-pool-contract/index.ts +++ b/packages/contract-helpers/src/v3-pool-contract/index.ts @@ -57,7 +57,6 @@ import { LPRepayWithPermitParamsType, LPSetUsageAsCollateral, LPSetUserEModeType, - LPSignERC20ApprovalType, LPSupplyWithPermitType, LPSwapBorrowRateMode, LPSwapCollateral, @@ -73,7 +72,6 @@ export interface PoolInterface { supply: ( args: LPSupplyParamsType, ) => Promise; - signERC20Approval: (args: LPSignERC20ApprovalType) => Promise; supplyWithPermit: ( args: LPSupplyWithPermitType, ) => Promise; @@ -401,78 +399,6 @@ export class Pool extends BaseService implements PoolInterface { return txs; } - // Sign permit supply - @LPValidatorV3 - public async signERC20Approval( - @isEthAddress('user') - @isEthAddress('reserve') - @isPositiveOrMinusOneAmount('amount') - { user, reserve, amount, deadline }: LPSignERC20ApprovalType, - ): Promise { - const { getTokenData, isApproved } = this.erc20Service; - const { name, decimals } = await getTokenData(reserve); - - const convertedAmount = - amount === '-1' - ? constants.MaxUint256.toString() - : valueToWei(amount, decimals); - - const approved = await isApproved({ - token: reserve, - user, - spender: this.poolAddress, - amount, - }); - - if (approved) { - return ''; - } - - const { chainId } = await this.provider.getNetwork(); - - const nonce = await this.erc20_2612Service.getNonce({ - token: reserve, - owner: user, - }); - - if (nonce === null) { - return ''; - } - - const typeData = { - types: { - EIP712Domain: [ - { name: 'name', type: 'string' }, - { name: 'version', type: 'string' }, - { name: 'chainId', type: 'uint256' }, - { name: 'verifyingContract', type: 'address' }, - ], - Permit: [ - { name: 'owner', type: 'address' }, - { name: 'spender', type: 'address' }, - { name: 'value', type: 'uint256' }, - { name: 'nonce', type: 'uint256' }, - { name: 'deadline', type: 'uint256' }, - ], - }, - primaryType: 'Permit', - domain: { - name, - version: '1', - chainId, - verifyingContract: reserve, - }, - message: { - owner: user, - spender: this.poolAddress, - value: convertedAmount, - nonce, - deadline, - }, - }; - return JSON.stringify(typeData); - } - @LPValidatorV3 public async supplyWithPermit( @isEthAddress('user') diff --git a/packages/contract-helpers/src/v3-pool-contract/lendingPoolTypes.ts b/packages/contract-helpers/src/v3-pool-contract/lendingPoolTypes.ts index e50b8c19..5750fd17 100644 --- a/packages/contract-helpers/src/v3-pool-contract/lendingPoolTypes.ts +++ b/packages/contract-helpers/src/v3-pool-contract/lendingPoolTypes.ts @@ -141,13 +141,6 @@ export type LPRepayWithPermitParamsType = { deadline: string; }; -export type LPSignERC20ApprovalType = { - user: tEthereumAddress; - reserve: tEthereumAddress; - amount: string; - deadline: string; -}; - export type LPSetUserEModeType = { user: string; categoryId: number; diff --git a/packages/contract-helpers/src/v3-pool-contract/pool.test.ts b/packages/contract-helpers/src/v3-pool-contract/pool.test.ts index e54aeeba..755fbe06 100644 --- a/packages/contract-helpers/src/v3-pool-contract/pool.test.ts +++ b/packages/contract-helpers/src/v3-pool-contract/pool.test.ts @@ -777,201 +777,6 @@ describe('Pool', () => { ).rejects.toThrowError(`Amount: ${amount} needs to be greater than 0`); }); }); - describe('signERC20Approval', () => { - const user = '0x0000000000000000000000000000000000000006'; - const reserve = '0x0000000000000000000000000000000000000007'; - const amount = '123.456'; - const decimals = 18; - const deadline = Math.round(Date.now() / 1000 + 3600).toString(); - jest.spyOn(provider, 'getTransactionCount').mockResolvedValue(1); - jest.spyOn(provider, 'getNetwork').mockResolvedValue({ - name: 'mainnet', - chainId: 1, - }); - - afterEach(() => { - jest.clearAllMocks(); - }); - - const config = { POOL }; - it('Expects the permission string to be returned when all params', async () => { - const poolInstance = new Pool(provider, config); - - jest.spyOn(poolInstance.erc20Service, 'getTokenData').mockReturnValue( - Promise.resolve({ - name: 'mockToken', - decimals, - symbol: 'MT', - address: '0x0000000000000000000000000000000000000006', - }), - ); - - jest - .spyOn(poolInstance.erc20Service, 'isApproved') - .mockReturnValue(Promise.resolve(false)); - - jest - .spyOn(poolInstance.erc20_2612Service, 'getNonce') - .mockReturnValue(Promise.resolve(1)); - const signature: string = await poolInstance.signERC20Approval({ - user, - reserve, - amount, - deadline, - }); - - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - const { primaryType, domain, message } = await JSON.parse(signature); - expect(primaryType).toEqual('Permit'); - expect(domain.name).toEqual('mockToken'); - expect(domain.chainId).toEqual(1); - - expect(message.owner).toEqual(user); - expect(message.spender).toEqual(POOL); - expect(message.value).toEqual(valueToWei(amount, decimals)); - expect(message.nonce).toEqual(1); - expect(message.deadline).toEqual(deadline); - }); - it('Expects the permission string to be returned when all params and amount -1', async () => { - const poolInstance = new Pool(provider, config); - - jest.spyOn(poolInstance.erc20Service, 'getTokenData').mockReturnValue( - Promise.resolve({ - name: 'mockToken', - decimals, - symbol: 'MT', - address: '0x0000000000000000000000000000000000000006', - }), - ); - - jest - .spyOn(poolInstance.erc20Service, 'isApproved') - .mockReturnValue(Promise.resolve(false)); - - jest - .spyOn(poolInstance.erc20_2612Service, 'getNonce') - .mockReturnValue(Promise.resolve(1)); - - const amount = '-1'; - const signature: string = await poolInstance.signERC20Approval({ - user, - reserve, - amount, - deadline, - }); - - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - const { primaryType, domain, message } = await JSON.parse(signature); - expect(primaryType).toEqual('Permit'); - expect(domain.name).toEqual('mockToken'); - expect(domain.chainId).toEqual(1); - - expect(message.owner).toEqual(user); - expect(message.spender).toEqual(POOL); - expect(message.value).toEqual(constants.MaxUint256.toString()); - expect(message.nonce).toEqual(1); - expect(message.deadline).toEqual(deadline); - }); - it('Expects the permission string to be `` when no nonce', async () => { - const poolInstance = new Pool(provider, config); - - jest.spyOn(poolInstance.erc20Service, 'getTokenData').mockReturnValue( - Promise.resolve({ - name: 'mockToken', - decimals, - symbol: 'MT', - address: '0x0000000000000000000000000000000000000006', - }), - ); - - jest - .spyOn(poolInstance.erc20Service, 'isApproved') - .mockReturnValue(Promise.resolve(false)); - jest - .spyOn(poolInstance.erc20_2612Service, 'getNonce') - .mockReturnValue(Promise.resolve(null)); - - const signature: string = await poolInstance.signERC20Approval({ - user, - reserve, - amount, - deadline, - }); - - expect(signature).toEqual(''); - }); - it('Expects the permission string to be `` when already approved', async () => { - const poolInstance = new Pool(provider, config); - - jest.spyOn(poolInstance.erc20Service, 'getTokenData').mockReturnValue( - Promise.resolve({ - name: 'mockToken', - decimals, - symbol: 'MT', - address: '0x0000000000000000000000000000000000000006', - }), - ); - - jest - .spyOn(poolInstance.erc20Service, 'isApproved') - .mockReturnValue(Promise.resolve(true)); - - const signature: string = await poolInstance.signERC20Approval({ - user, - reserve, - amount, - deadline, - }); - - expect(signature).toEqual(''); - }); - it('Expects to fail when not initialized with POOL', async () => { - const poolInstance = new Pool(provider, { POOL: 'asdf' }); - const signature: string = await poolInstance.signERC20Approval({ - user, - reserve, - amount, - deadline, - }); - expect(signature).toEqual([]); - }); - it('Expects to fail when user not eth address', async () => { - const poolInstance = new Pool(provider, { POOL }); - - const user = 'asdf'; - await expect(async () => - poolInstance.signERC20Approval({ user, reserve, amount, deadline }), - ).rejects.toThrowError( - `Address: ${user} is not a valid ethereum Address`, - ); - }); - it('Expects to fail when reserve not eth address', async () => { - const poolInstance = new Pool(provider, { POOL }); - - const reserve = 'asdf'; - await expect(async () => - poolInstance.signERC20Approval({ user, reserve, amount, deadline }), - ).rejects.toThrowError( - `Address: ${reserve} is not a valid ethereum Address`, - ); - }); - it('Expects to fail when amount not positive', async () => { - const poolInstance = new Pool(provider, { POOL }); - - const amount = '0'; - await expect(async () => - poolInstance.signERC20Approval({ user, reserve, amount, deadline }), - ).rejects.toThrowError(`Amount: ${amount} needs to be greater than 0`); - }); - it('Expects to fail when amount not number', async () => { - const poolInstance = new Pool(provider, { POOL }); - - const amount = 'asdf'; - await expect(async () => - poolInstance.signERC20Approval({ user, reserve, amount, deadline }), - ).rejects.toThrowError(`Amount: ${amount} needs to be greater than 0`); - }); - }); describe('supplyWithPermit', () => { const user = '0x0000000000000000000000000000000000000006'; const reserve = '0x0000000000000000000000000000000000000007'; @@ -985,6 +790,12 @@ describe('Pool', () => { const amount = '123.456'; const decimals = 18; + jest.spyOn(provider, 'getTransactionCount').mockResolvedValue(1); + jest.spyOn(provider, 'getNetwork').mockResolvedValue({ + name: 'mainnet', + chainId: 1, + }); + afterEach(() => { jest.clearAllMocks(); });