diff --git a/packages/smart-contracts/scripts/3_deploy_chainlink_contract.ts b/packages/smart-contracts/scripts/3_deploy_chainlink_contract.ts index e159a826eb..3cfae99652 100644 --- a/packages/smart-contracts/scripts/3_deploy_chainlink_contract.ts +++ b/packages/smart-contracts/scripts/3_deploy_chainlink_contract.ts @@ -2,6 +2,7 @@ import '@nomiclabs/hardhat-ethers'; import { CurrencyManager } from '@requestnetwork/currency'; import { HardhatRuntimeEnvironment } from 'hardhat/types'; import deployERC20ConversionProxy from './erc20-conversion-proxy'; +import deployEthConversionProxy from './eth-conversion-proxy'; import deploySwapConversion from './erc20-swap-to-conversion'; import { deployOne } from './deploy-one'; @@ -15,9 +16,9 @@ export default async function deploy(args: any, hre: HardhatRuntimeEnvironment) const currencyManager = CurrencyManager.getDefault(); // all these addresses are for test purposes - const ETH_address = currencyManager.fromSymbol('ETH')!.hash; - const USD_address = currencyManager.fromSymbol('USD')!.hash; - const EUR_address = currencyManager.fromSymbol('EUR')!.hash; + const ETH_hash = currencyManager.fromSymbol('ETH')!.hash; + const USD_hash = currencyManager.fromSymbol('USD')!.hash; + const EUR_hash = currencyManager.fromSymbol('EUR')!.hash; // Cf. ERC20Alpha in TestERC20.sol const DAI_address = '0x38cF23C52Bb4B13F051Aec09580a2dE845a7FA35'; @@ -31,8 +32,8 @@ export default async function deploy(args: any, hre: HardhatRuntimeEnvironment) // all these aggregators are for test purposes await conversionPathInstance.updateAggregatorsList( - [DAI_address, EUR_address, ETH_address, USDT_address], - [USD_address, USD_address, USD_address, ETH_address], + [DAI_address, EUR_hash, ETH_hash, USDT_address], + [USD_hash, USD_hash, USD_hash, ETH_hash], [AggDAI_USD_address, AggEUR_USD_address, AggETH_USD_address, AggUSDT_ETH_address], ); console.log('AggregatorsList updated.'); @@ -65,6 +66,16 @@ export default async function deploy(args: any, hre: HardhatRuntimeEnvironment) // FIXME: should try to retrieve information from artifacts instead await erc20SwapConversion.approveRouterToSpend('0x9FBDa871d559710256a2502A2517b794B482Db40'); + // EthConversion + const ethConversionProxyAddress = await deployEthConversionProxy( + { + ...args, + chainlinkConversionPathAddress: conversionPathInstance.address, + ethFeeProxyAddress: '0x3d49d1eF2adE060a33c6E6Aa213513A7EE9a6241', + nativeTokenHash: ETH_hash, + }, + hre, + ); // ---------------------------------- console.log('Contracts deployed'); console.log(` @@ -73,5 +84,6 @@ export default async function deploy(args: any, hre: HardhatRuntimeEnvironment) ChainlinkConversionPath: ${conversionPathInstance.address} Erc20ConversionProxy: ${erc20ConversionAddress} Erc20SwapConversionProxy: ${erc20SwapConversionAddress} + EthConversionProxy: ${ethConversionProxyAddress} `); } diff --git a/packages/smart-contracts/scripts/eth-conversion-proxy.ts b/packages/smart-contracts/scripts/eth-conversion-proxy.ts new file mode 100644 index 0000000000..65a9f56e70 --- /dev/null +++ b/packages/smart-contracts/scripts/eth-conversion-proxy.ts @@ -0,0 +1,33 @@ +import { HardhatRuntimeEnvironment } from 'hardhat/types'; +import { deployOne } from './deploy-one'; + +export default async function deploy( + args: { chainlinkConversionPathAddress?: string; ethFeeProxyAddress?: string, nativeTokenHash?: string }, + hre: HardhatRuntimeEnvironment, +) { + const contractName = 'EthConversionProxy'; + + if (!args.chainlinkConversionPathAddress) { + // FIXME: should try to retrieve information from artifacts instead + console.error( + `Missing ChainlinkConversionPath on ${hre.network.name}, cannot deploy ${contractName}.`, + ); + return; + } + if (!args.ethFeeProxyAddress) { + // FIXME: should try to retrieve information from artifacts instead + console.error(`Missing EthereumFeeProxy on ${hre.network.name}, cannot deploy ${contractName}.`); + return; + } + + if (!args.nativeTokenHash) { + console.error(`Missing nativeTokenHash on ${hre.network.name}, cannot deploy ${contractName}.`); + return; + } + + return deployOne(args, hre, contractName, [ + args.ethFeeProxyAddress, + args.chainlinkConversionPathAddress, + args.nativeTokenHash, + ]); +} diff --git a/packages/smart-contracts/src/contracts/EthConversionProxy.sol b/packages/smart-contracts/src/contracts/EthConversionProxy.sol new file mode 100644 index 0000000000..0ae2eee2c7 --- /dev/null +++ b/packages/smart-contracts/src/contracts/EthConversionProxy.sol @@ -0,0 +1,124 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import "./ChainlinkConversionPath.sol"; +import "@openzeppelin/contracts/security/ReentrancyGuard.sol"; + +/** + * @title EthConversionProxy + * @notice This contract converts from chainlink then swaps ETH (or native token) + * before paying a request thanks to a conversion payment proxy. + * The inheritance from ReentrancyGuard is required to perform + * "transferExactEthWithReferenceAndFee" on the eth-fee-proxy contract + */ +contract EthConversionProxy is ReentrancyGuard { + address public paymentProxy; + ChainlinkConversionPath public chainlinkConversionPath; + address public nativeTokenHash; + + constructor(address _paymentProxyAddress, address _chainlinkConversionPathAddress, address _nativeTokenHash) { + paymentProxy = _paymentProxyAddress; + chainlinkConversionPath = ChainlinkConversionPath(_chainlinkConversionPathAddress); + nativeTokenHash = _nativeTokenHash; + } + + // Event to declare a conversion with a reference + event TransferWithConversionAndReference( + uint256 amount, + address currency, + bytes indexed paymentReference, + uint256 feeAmount, + uint256 maxRateTimespan + ); + + // Event to declare a transfer with a reference + // This event is emitted by this contract from a delegate call of the payment-proxy + event TransferWithReferenceAndFee( + address to, + uint256 amount, + bytes indexed paymentReference, + uint256 feeAmount, + address feeAddress + ); + + /** + * @notice Performs an ETH transfer with a reference computing the payment amount based on the request amount + * @param _to Transfer recipient of the payement + * @param _requestAmount Request amount + * @param _path Conversion path + * @param _paymentReference Reference of the payment related + * @param _feeAmount The amount of the payment fee + * @param _feeAddress The fee recipient + * @param _maxRateTimespan Max time span with the oldestrate, ignored if zero + */ + function transferWithReferenceAndFee( + address _to, + uint256 _requestAmount, + address[] calldata _path, + bytes calldata _paymentReference, + uint256 _feeAmount, + address _feeAddress, + uint256 _maxRateTimespan + ) + external + payable + { + require( + _path[_path.length - 1] == nativeTokenHash, + "payment currency must be the native token" + ); + + (uint256 amountToPay, uint256 amountToPayInFees) = getConversions( + _path, + _requestAmount, + _feeAmount, + _maxRateTimespan); + + // Pay the request and fees + (bool status, ) = paymentProxy.delegatecall( + abi.encodeWithSignature( + "transferExactEthWithReferenceAndFee(address,uint256,bytes,uint256,address)", + _to, + amountToPay, + _paymentReference, + amountToPayInFees, + _feeAddress + ) + ); + + require(status, "paymentProxy transferExactEthWithReferenceAndFee failed"); + + // Event to declare a transfer with a reference + emit TransferWithConversionAndReference( + _requestAmount, + // request currency + _path[0], + _paymentReference, + _feeAmount, + _maxRateTimespan + ); + } + + function getConversions( + address[] memory _path, + uint256 _requestAmount, + uint256 _feeAmount, + uint256 _maxRateTimespan + ) + internal + view + returns (uint256 amountToPay, uint256 amountToPayInFees) + { + (uint256 rate, uint256 oldestTimestampRate, uint256 decimals) = chainlinkConversionPath.getRate(_path); + + // Check rate timespan + require( + _maxRateTimespan == 0 || block.timestamp - oldestTimestampRate <= _maxRateTimespan, + "aggregator rate is outdated" + ); + + // Get the amount to pay in the native token + amountToPay = (_requestAmount * rate) / decimals; + amountToPayInFees = (_feeAmount * rate) /decimals; + } +} diff --git a/packages/smart-contracts/src/lib/artifacts/EthConversionProxy/0.1.0.json b/packages/smart-contracts/src/lib/artifacts/EthConversionProxy/0.1.0.json new file mode 100644 index 0000000000..b81f587a30 --- /dev/null +++ b/packages/smart-contracts/src/lib/artifacts/EthConversionProxy/0.1.0.json @@ -0,0 +1,181 @@ +{ + "abi": [ + { + "inputs": [ + { + "internalType": "address", + "name": "_paymentProxyAddress", + "type": "address" + }, + { + "internalType": "address", + "name": "_chainlinkConversionPathAddress", + "type": "address" + }, + { + "internalType": "address", + "name": "_nativeTokenHash", + "type": "address" + } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "address", + "name": "currency", + "type": "address" + }, + { + "indexed": true, + "internalType": "bytes", + "name": "paymentReference", + "type": "bytes" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "feeAmount", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "maxRateTimespan", + "type": "uint256" + } + ], + "name": "TransferWithConversionAndReference", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "indexed": true, + "internalType": "bytes", + "name": "paymentReference", + "type": "bytes" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "feeAmount", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "address", + "name": "feeAddress", + "type": "address" + } + ], + "name": "TransferWithReferenceAndFee", + "type": "event" + }, + { + "inputs": [], + "name": "chainlinkConversionPath", + "outputs": [ + { + "internalType": "contract ChainlinkConversionPath", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "nativeTokenHash", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "paymentProxy", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "_requestAmount", + "type": "uint256" + }, + { + "internalType": "address[]", + "name": "_path", + "type": "address[]" + }, + { + "internalType": "bytes", + "name": "_paymentReference", + "type": "bytes" + }, + { + "internalType": "uint256", + "name": "_feeAmount", + "type": "uint256" + }, + { + "internalType": "address", + "name": "_feeAddress", + "type": "address" + }, + { + "internalType": "uint256", + "name": "_maxRateTimespan", + "type": "uint256" + } + ], + "name": "transferWithReferenceAndFee", + "outputs": [], + "stateMutability": "payable", + "type": "function" + } + ] +} diff --git a/packages/smart-contracts/src/lib/artifacts/EthConversionProxy/index.ts b/packages/smart-contracts/src/lib/artifacts/EthConversionProxy/index.ts new file mode 100644 index 0000000000..813eff6775 --- /dev/null +++ b/packages/smart-contracts/src/lib/artifacts/EthConversionProxy/index.ts @@ -0,0 +1,19 @@ +import { ContractArtifact } from '../../ContractArtifact'; + +import { abi as ABI_0_1_0 } from './0.1.0.json'; +import type { EthConversionProxy } from '../../../types/EthConversionProxy'; + +export const ethConversionArtifact = new ContractArtifact( + { + '0.1.0': { + abi: ABI_0_1_0, + deployment: { + private: { + address: '0x8273e4B8ED6c78e252a9fCa5563Adfcc75C91b2A', + creationBlockNumber: 0, + }, + }, + }, + }, + '0.1.0', +); diff --git a/packages/smart-contracts/src/lib/artifacts/index.ts b/packages/smart-contracts/src/lib/artifacts/index.ts index ed59dbdfd6..b20aa2919a 100644 --- a/packages/smart-contracts/src/lib/artifacts/index.ts +++ b/packages/smart-contracts/src/lib/artifacts/index.ts @@ -9,6 +9,7 @@ export * from './ERC20SwapToPay'; export * from './Erc20SwapConversion'; export * from './EthereumProxy'; export * from './EthereumFeeProxy'; +export * from './EthConversionProxy'; /** * Request Storage diff --git a/packages/smart-contracts/test/contracts/ChainlinkConversionPath.test.ts b/packages/smart-contracts/test/contracts/ChainlinkConversionPath.test.ts index c378272629..ef6b401924 100644 --- a/packages/smart-contracts/test/contracts/ChainlinkConversionPath.test.ts +++ b/packages/smart-contracts/test/contracts/ChainlinkConversionPath.test.ts @@ -14,9 +14,9 @@ const address5 = '0x5555555555555555555555555555555555555555'; const address6 = '0x6666666666666666666666666666666666666666'; const currencyManager = CurrencyManager.getDefault(); -const ETH_address = currencyManager.fromSymbol('ETH')!.hash; -const USD_address = currencyManager.fromSymbol('USD')!.hash; -const EUR_address = currencyManager.fromSymbol('EUR')!.hash; +const ETH_hash = currencyManager.fromSymbol('ETH')!.hash; +const USD_hash = currencyManager.fromSymbol('USD')!.hash; +const EUR_hash = currencyManager.fromSymbol('EUR')!.hash; let DAI_address: string; let USDT_address: string; let conversionPathInstance: ChainlinkConversionPath; @@ -60,29 +60,29 @@ describe('contract: ChainlinkConversionPath', () => { describe('getRate', async () => { describe('only fiat rates', async () => { it('can get rate from EUR to USD', async () => { - const conversion = await conversionPathInstance.getRate([EUR_address, USD_address]); + const conversion = await conversionPathInstance.getRate([EUR_hash, USD_hash]); expect(conversion.rate.toString(), '1200000000000000000'); }); it('can get rate from USD to EUR', async () => { - const conversion = await conversionPathInstance.getRate([USD_address, EUR_address]); + const conversion = await conversionPathInstance.getRate([USD_hash, EUR_hash]); expect(conversion.rate.toString(), '833333333333333333'); }); it('can get rate from USD to EUR to USD', async () => { const conversion = await conversionPathInstance.getRate([ - USD_address, - EUR_address, - USD_address, + USD_hash, + EUR_hash, + USD_hash, ]); expect(conversion.rate.toString(), '999999999999999999'); }); it('can get rate from ETH to USD to EUR', async () => { const conversion = await conversionPathInstance.getRate([ - ETH_address, - USD_address, - EUR_address, + ETH_hash, + USD_hash, + EUR_hash, ]); expect(conversion.rate.toString(), '41666666666'); }); @@ -91,33 +91,33 @@ describe('contract: ChainlinkConversionPath', () => { describe('Ethereum rates', async () => { it('can get rate from USD to ETH', async () => { - const conversion = await conversionPathInstance.getRate([USD_address, ETH_address]); + const conversion = await conversionPathInstance.getRate([USD_hash, ETH_hash]); expect(conversion.rate.toString(), '20000000000000000000000000'); }); it('can get rate from ETH to USD', async () => { - const conversion = await conversionPathInstance.getRate([ETH_address, USD_address]); + const conversion = await conversionPathInstance.getRate([ETH_hash, USD_hash]); expect(conversion.rate.toString(), '50000000000'); }); it('can get rate from EUR to USD to ETH', async () => { const conversion = await conversionPathInstance.getRate([ - EUR_address, - USD_address, - ETH_address, + EUR_hash, + USD_hash, + ETH_hash, ]); expect(conversion.rate.toString(), '24000000000000000000000000'); }); it('can get rate from USD to ERC20', async () => { - const conversion = await conversionPathInstance.getRate([USD_address, DAI_address]); + const conversion = await conversionPathInstance.getRate([USD_hash, DAI_address]); expect(conversion.rate.toString(), '9900990099009900990099009900'); }); it('can get rate from ETH to USD to ERC20', async () => { const conversion = await conversionPathInstance.getRate([ - ETH_address, - USD_address, + ETH_hash, + USD_hash, DAI_address, ]); expect(conversion.rate.toString(), '495049504950495049504'); @@ -127,8 +127,8 @@ describe('contract: ChainlinkConversionPath', () => { describe('USDT rates', async () => { it('can get rate from USD to ETH to USDT', async () => { const conversion = await conversionPathInstance.getRate([ - USD_address, - ETH_address, + USD_hash, + ETH_hash, USDT_address, ]); expect(conversion.rate.toString(), '10000000000000000'); @@ -137,8 +137,8 @@ describe('contract: ChainlinkConversionPath', () => { it('can get rate from USDT to ETH to USD', async () => { const conversion = await conversionPathInstance.getRate([ USDT_address, - ETH_address, - USD_address, + ETH_hash, + USD_hash, ]); expect(conversion.rate.toString(), '100000000000000000000'); }); @@ -148,25 +148,25 @@ describe('contract: ChainlinkConversionPath', () => { describe('only fiat conversion', async () => { it('can convert EUR to USD', async () => { const conversion = await conversionPathInstance.getConversion('10000000000', [ - EUR_address, - USD_address, + EUR_hash, + USD_hash, ]); expect(conversion.result.toString(), '12000000000'); }); it('can convert USD to EUR', async () => { const conversion = await conversionPathInstance.getConversion('10000000000', [ - USD_address, - EUR_address, + USD_hash, + EUR_hash, ]); expect(conversion.result.toString(), '8333333333'); }); it('can convert USD to EUR to USD', async () => { const conversion = await conversionPathInstance.getConversion('10000000000', [ - USD_address, - EUR_address, - USD_address, + USD_hash, + EUR_hash, + USD_hash, ]); expect(conversion.result.toString(), '9999999999'); }); @@ -175,34 +175,34 @@ describe('contract: ChainlinkConversionPath', () => { describe('Ethereum conversion', async () => { it('can convert USD to ETH', async () => { const conversion = await conversionPathInstance.getConversion('100000000000', [ - USD_address, - ETH_address, + USD_hash, + ETH_hash, ]); expect(conversion.result.toString(), '2000000000000000000'); }); it('can convert ETH to USD', async () => { const conversion = await conversionPathInstance.getConversion('2000000000000000000', [ - ETH_address, - USD_address, + ETH_hash, + USD_hash, ]); expect(conversion.result.toString(), '100000000000'); }); it('can convert EUR to USD to ETH', async () => { const conversion = await conversionPathInstance.getConversion('100000000000', [ - EUR_address, - USD_address, - ETH_address, + EUR_hash, + USD_hash, + ETH_hash, ]); expect(conversion.result.toString(), '2400000000000000000'); }); it('can convert ETH to USD to EUR', async () => { const conversion = await conversionPathInstance.getConversion('2000000000000000000', [ - ETH_address, - USD_address, - EUR_address, + ETH_hash, + USD_hash, + EUR_hash, ]); expect(conversion.result.toString(), '83333333332'); }); @@ -211,8 +211,8 @@ describe('contract: ChainlinkConversionPath', () => { describe('USDT conversion', async () => { it('can convert USD to ETH to USDT', async () => { const conversion = await conversionPathInstance.getConversion('10000000000', [ - USD_address, - ETH_address, + USD_hash, + ETH_hash, USDT_address, ]); expect(conversion.result.toString(), '100000000'); @@ -221,8 +221,8 @@ describe('contract: ChainlinkConversionPath', () => { it('can convert USDT to ETH to USD', async () => { const conversion = await conversionPathInstance.getConversion('100000000', [ USDT_address, - ETH_address, - USD_address, + ETH_hash, + USD_hash, ]); expect(conversion.result.toString(), '10000000000'); }); diff --git a/packages/smart-contracts/test/contracts/Erc20ConversionProxy.test.ts b/packages/smart-contracts/test/contracts/Erc20ConversionProxy.test.ts index d366bc4b0c..4634afb29b 100644 --- a/packages/smart-contracts/test/contracts/Erc20ConversionProxy.test.ts +++ b/packages/smart-contracts/test/contracts/Erc20ConversionProxy.test.ts @@ -22,17 +22,17 @@ describe('contract: Erc20ConversionProxy', () => { let to: string; let feeAddress: string; let signer: Signer; - const smallAmountInFIAT = '100000000'; // 1 with 8 decimal - const smallerAmountInFIAT = '10000000'; // 0.1 with 8 decimal + const amountInFiat = '100000000'; // 1 with 8 decimal + const feesAmountInFiat = '10000000'; // 0.1 with 8 decimal const thousandWith18Decimal = '1000000000000000000000'; const hundredWith18Decimal = '100000000000000000000'; const referenceExample = '0xaaaa'; const currencyManager = CurrencyManager.getDefault(); - const ETH_address = currencyManager.fromSymbol('ETH')!.hash; - const USD_address = currencyManager.fromSymbol('USD')!.hash; - const EUR_address = currencyManager.fromSymbol('EUR')!.hash; + const ETH_hash = currencyManager.fromSymbol('ETH')!.hash; + const USD_hash = currencyManager.fromSymbol('USD')!.hash; + const EUR_hash = currencyManager.fromSymbol('EUR')!.hash; let DAI_address: string; let testErc20ConversionProxy: Erc20ConversionProxy; @@ -58,7 +58,7 @@ describe('contract: Erc20ConversionProxy', () => { describe('transferFromWithReferenceAndFee', () => { describe('transferFromWithReferenceAndFee with DAI', () => { it('allows to transfer DAI tokens for USD payment', async function () { - const path = [USD_address, DAI_address]; + const path = [USD_hash, DAI_address]; await testERC20.approve(testErc20ConversionProxy.address, thousandWith18Decimal, { from, }); @@ -66,16 +66,16 @@ describe('contract: Erc20ConversionProxy', () => { const fromOldBalance = await testERC20.balanceOf(from); const toOldBalance = await testERC20.balanceOf(to); const feeOldBalance = await testERC20.balanceOf(feeAddress); - const conversionToPay = await chainlinkPath.getConversion(smallAmountInFIAT, path); - const conversionFees = await chainlinkPath.getConversion(smallerAmountInFIAT, path); + const conversionToPay = await chainlinkPath.getConversion(amountInFiat, path); + const conversionFees = await chainlinkPath.getConversion(feesAmountInFiat, path); await expect( testErc20ConversionProxy.transferFromWithReferenceAndFee( to, - smallAmountInFIAT, + amountInFiat, path, referenceExample, - smallerAmountInFIAT, + feesAmountInFiat, feeAddress, hundredWith18Decimal, 0, @@ -83,10 +83,10 @@ describe('contract: Erc20ConversionProxy', () => { ) .to.emit(testErc20ConversionProxy, 'TransferWithConversionAndReference') .withArgs( - smallAmountInFIAT, + amountInFiat, ethers.utils.getAddress(path[0]), ethers.utils.keccak256(referenceExample), - smallerAmountInFIAT, + feesAmountInFiat, '0', ); @@ -114,22 +114,22 @@ describe('contract: Erc20ConversionProxy', () => { }); it('allows to transfer DAI tokens for EUR payment', async function () { - const path = [EUR_address, USD_address, DAI_address]; + const path = [EUR_hash, USD_hash, DAI_address]; await testERC20.approve(testErc20ConversionProxy.address, thousandWith18Decimal, { from }); const fromOldBalance = await testERC20.balanceOf(from); const toOldBalance = await testERC20.balanceOf(to); const feeOldBalance = await testERC20.balanceOf(feeAddress); - const conversionToPay = await chainlinkPath.getConversion(smallAmountInFIAT, path); - const conversionFees = await chainlinkPath.getConversion(smallerAmountInFIAT, path); + const conversionToPay = await chainlinkPath.getConversion(amountInFiat, path); + const conversionFees = await chainlinkPath.getConversion(feesAmountInFiat, path); await expect( testErc20ConversionProxy.transferFromWithReferenceAndFee( to, - smallAmountInFIAT, + amountInFiat, path, referenceExample, - smallerAmountInFIAT, + feesAmountInFiat, feeAddress, hundredWith18Decimal, 0, @@ -138,10 +138,10 @@ describe('contract: Erc20ConversionProxy', () => { .to.emit(testERC20, 'Transfer') .to.emit(testErc20ConversionProxy, 'TransferWithConversionAndReference') .withArgs( - smallAmountInFIAT, + amountInFiat, ethers.utils.getAddress(path[0]), ethers.utils.keccak256(referenceExample), - smallerAmountInFIAT, + feesAmountInFiat, '0', ) .to.emit(testErc20ConversionProxy, 'TransferWithReferenceAndFee') @@ -173,7 +173,7 @@ describe('contract: Erc20ConversionProxy', () => { describe('transferFromWithReferenceAndFee with errors', () => { it('cannot transfer with invalid path', async function () { - const path = [EUR_address, ETH_address, DAI_address]; + const path = [EUR_hash, ETH_hash, DAI_address]; await testERC20.approve(testErc20ConversionProxy.address, thousandWith18Decimal, { from, }); @@ -181,10 +181,10 @@ describe('contract: Erc20ConversionProxy', () => { await expect( testErc20ConversionProxy.transferFromWithReferenceAndFee( to, - smallAmountInFIAT, + amountInFiat, path, referenceExample, - smallerAmountInFIAT, + feesAmountInFiat, feeAddress, hundredWith18Decimal, 0, @@ -194,7 +194,7 @@ describe('contract: Erc20ConversionProxy', () => { }); it('cannot transfer if max to spend too low', async function () { - const path = [USD_address, DAI_address]; + const path = [USD_hash, DAI_address]; await testERC20.approve(testErc20ConversionProxy.address, thousandWith18Decimal, { from, }); @@ -202,10 +202,10 @@ describe('contract: Erc20ConversionProxy', () => { await expect( testErc20ConversionProxy.transferFromWithReferenceAndFee( to, - smallAmountInFIAT, + amountInFiat, path, referenceExample, - smallerAmountInFIAT, + feesAmountInFiat, feeAddress, 100, 0, @@ -215,7 +215,7 @@ describe('contract: Erc20ConversionProxy', () => { }); it('cannot transfer if rate is too old', async function () { - const path = [USD_address, DAI_address]; + const path = [USD_hash, DAI_address]; await testERC20.approve(testErc20ConversionProxy.address, thousandWith18Decimal, { from, }); @@ -223,10 +223,10 @@ describe('contract: Erc20ConversionProxy', () => { await expect( testErc20ConversionProxy.transferFromWithReferenceAndFee( to, - smallAmountInFIAT, + amountInFiat, path, referenceExample, - smallerAmountInFIAT, + feesAmountInFiat, feeAddress, hundredWith18Decimal, 10, // ten secondes diff --git a/packages/smart-contracts/test/contracts/EthConversionProxy.test.ts b/packages/smart-contracts/test/contracts/EthConversionProxy.test.ts new file mode 100644 index 0000000000..5806ad5b09 --- /dev/null +++ b/packages/smart-contracts/test/contracts/EthConversionProxy.test.ts @@ -0,0 +1,212 @@ +import { ethers, network } from 'hardhat'; +import { + EthereumFeeProxy__factory, + EthConversionProxy__factory, + EthereumFeeProxy, + ChainlinkConversionPath, + EthConversionProxy, +} from '../../src/types'; +import { BigNumber, Signer } from 'ethers'; +import { expect, use } from 'chai'; +import { solidity } from 'ethereum-waffle'; +import { CurrencyManager } from '@requestnetwork/currency'; +import { chainlinkConversionPath } from '../../src/lib'; + +use(solidity); + +describe('contract: EthConversionProxy', () => { + let from: string; + let to: string; + let feeAddress: string; + let signer: Signer; + const amountInFiat = BigNumber.from('100000000'); + const feesAmountInFiat = BigNumber.from('10000000'); + const referenceExample = '0xaaaa'; + + const currencyManager = CurrencyManager.getDefault(); + + const ETH_hash = currencyManager.fromSymbol('ETH')!.hash; + const USD_hash = currencyManager.fromSymbol('USD')!.hash; + const EUR_hash = currencyManager.fromSymbol('EUR')!.hash; + + let testEthConversionProxy: EthConversionProxy; + let ethFeeProxy: EthereumFeeProxy; + let chainlinkPath: ChainlinkConversionPath; + const provider = new ethers.providers.JsonRpcProvider(); + + before(async () => { + [from, to, feeAddress] = (await ethers.getSigners()).map((s) => s.address); + [signer] = await ethers.getSigners(); + + chainlinkPath = chainlinkConversionPath.connect(network.name, signer); + ethFeeProxy = await new EthereumFeeProxy__factory(signer).deploy(); + testEthConversionProxy = await new EthConversionProxy__factory(signer).deploy( + ethFeeProxy.address, + chainlinkPath.address, + ETH_hash, + ); + }); + + describe('transferWithReferenceAndFee', () => { + describe('transferWithReferenceAndFee with ETH', () => { + it('allows to transfer ETH for USD payment', async function () { + const path = [USD_hash, ETH_hash]; + + const fromOldBalance = await provider.getBalance(from); + const toOldBalance = await provider.getBalance(to); + const feeOldBalance = await provider.getBalance(feeAddress); + const conversionToPay = await chainlinkPath.getConversion(amountInFiat, path); + const conversionFees = await chainlinkPath.getConversion(feesAmountInFiat, path); + + const tx = testEthConversionProxy.transferWithReferenceAndFee( + to, + amountInFiat, + path, + referenceExample, + feesAmountInFiat, + feeAddress, + 0, + { + value: conversionFees.result.add(conversionToPay.result), + } + ) + + await expect(tx) + .to.emit(testEthConversionProxy, 'TransferWithConversionAndReference') + .withArgs( + amountInFiat, + ethers.utils.getAddress(path[0]), + ethers.utils.keccak256(referenceExample), + feesAmountInFiat, + '0', + ); + + const receipt = await (await tx).wait(); + + const fromNewBalance = await provider.getBalance(from); + const toNewBalance = await provider.getBalance(to); + const feeNewBalance = await provider.getBalance(feeAddress); + const contractBalance = await provider.getBalance(testEthConversionProxy.address); + + const toDiffBalance = BigNumber.from(toNewBalance) + .sub(toOldBalance) + .toString(); + const feeDiffBalance = BigNumber.from(feeNewBalance) + .sub(feeOldBalance) + .toString(); + + const gasPrice = (await provider.getFeeData()).gasPrice || 0; + // Check balance changes + expect(fromNewBalance.toString()).to.equals( + fromOldBalance.sub(conversionToPay.result).sub(conversionFees.result).sub(receipt.gasUsed.mul(gasPrice)).toString(), + ); + expect(toDiffBalance).to.equals(conversionToPay.result.toString()); + expect(feeDiffBalance).to.equals(conversionFees.result.toString()); + expect(contractBalance.toString()).to.equals("0") + }); + + it('allows to transfer ETH for EUR payment and extra msg.value', async function () { + const path = [EUR_hash, USD_hash, ETH_hash]; + + const fromOldBalance = await provider.getBalance(from); + const toOldBalance = await provider.getBalance(to); + const feeOldBalance = await provider.getBalance(feeAddress); + const conversionToPay = await chainlinkPath.getConversion(amountInFiat, path); + const conversionFees = await chainlinkPath.getConversion(feesAmountInFiat, path); + + const tx = testEthConversionProxy.transferWithReferenceAndFee( + to, + amountInFiat, + path, + referenceExample, + feesAmountInFiat, + feeAddress, + 0, + { + value: conversionFees.result.add(conversionToPay.result)//.add("100000"), + } + ) + + await expect(tx) + .to.emit(testEthConversionProxy, 'TransferWithConversionAndReference') + .withArgs( + amountInFiat, + ethers.utils.getAddress(path[0]), + ethers.utils.keccak256(referenceExample), + feesAmountInFiat, + '0', + ); + + const receipt = await (await tx).wait(); + + const fromNewBalance = await provider.getBalance(from); + const toNewBalance = await provider.getBalance(to); + const feeNewBalance = await provider.getBalance(feeAddress); + const contractBalance = await provider.getBalance(testEthConversionProxy.address); + const contractFeeBalance = await provider.getBalance(ethFeeProxy.address); + + const toDiffBalance = BigNumber.from(toNewBalance) + .sub(toOldBalance) + .toString(); + const feeDiffBalance = BigNumber.from(feeNewBalance) + .sub(feeOldBalance) + .toString(); + + expect(contractBalance.toString()).to.equals("0"); + expect(contractFeeBalance.toString()).to.equals("0"); + + const gasPrice = (await provider.getFeeData()).gasPrice || 0; + // Check balance changes + expect(fromNewBalance.toString()).to.equals( + fromOldBalance.sub(conversionToPay.result).sub(conversionFees.result).sub(receipt.cumulativeGasUsed.mul(gasPrice)).toString(), + ); + expect(toDiffBalance).to.equals(conversionToPay.result.toString()); + expect(feeDiffBalance).to.equals(conversionFees.result.toString()); + }); + }); + + describe('transferWithReferenceAndFee with errors', () => { + it('cannot transfer if msg.value too low', async function () { + const path = [USD_hash, ETH_hash]; + + const conversionToPay = await chainlinkPath.getConversion(amountInFiat, path); + + await expect( + testEthConversionProxy.transferWithReferenceAndFee( + to, + amountInFiat, + path, + referenceExample, + feesAmountInFiat, + feeAddress, + 0, + { + value: conversionToPay.result, + } + ), + ).to.be.revertedWith('revert paymentProxy transferExactEthWithReferenceAndFee failed') + }); + + it('cannot transfer if rate is too old', async function () { + const path = [USD_hash, ETH_hash]; + + const conversionToPay = await chainlinkPath.getConversion(amountInFiat, path); + const conversionFees = await chainlinkPath.getConversion(feesAmountInFiat, path); + await expect( + testEthConversionProxy.transferWithReferenceAndFee( + to, + amountInFiat, + path, + referenceExample, + feesAmountInFiat, + feeAddress, + 1, // second + { + value: conversionFees.result.add(conversionToPay.result), + } + ), + ).to.be.revertedWith('revert aggregator rate is outdated') + }); + }); + }); +});