From 71d0605ee96ad7ce8e1a203d69711295de3f5458 Mon Sep 17 00:00:00 2001 From: olivier7delf <55892112+olivier7delf@users.noreply.github.com> Date: Wed, 1 Jun 2022 11:26:31 +0200 Subject: [PATCH 001/105] batch erc20 conversion contract and local deployment batch conv tests rename files clean batch tests batch deploy cleaning uncomment batchERC20ConversionPaymentsMultiTokensEasy test test: clean logs --- packages/smart-contracts/package.json | 7 +- .../scripts/test-deploy-all.ts | 2 + ...test-deploy-batch-conversion-deployment.ts | 68 +++ .../BatchERC20ConversionPayments.sol | 320 ++++++++++ .../src/contracts/BatchPaymentsPublic.sol | 359 ++++++++++++ .../BatchConversionPayments/0.1.0.json | 549 ++++++++++++++++++ .../BatchConversionPayments/index.ts | 20 + .../src/lib/artifacts/index.ts | 1 + .../BatchERC20ConversionPayments.test.ts | 396 +++++++++++++ 9 files changed, 1721 insertions(+), 1 deletion(-) create mode 100644 packages/smart-contracts/scripts/test-deploy-batch-conversion-deployment.ts create mode 100644 packages/smart-contracts/src/contracts/BatchERC20ConversionPayments.sol create mode 100644 packages/smart-contracts/src/contracts/BatchPaymentsPublic.sol create mode 100644 packages/smart-contracts/src/lib/artifacts/BatchConversionPayments/0.1.0.json create mode 100644 packages/smart-contracts/src/lib/artifacts/BatchConversionPayments/index.ts create mode 100644 packages/smart-contracts/test/contracts/BatchERC20ConversionPayments.test.ts diff --git a/packages/smart-contracts/package.json b/packages/smart-contracts/package.json index 6f1cfd6e4..7fc6c9af3 100644 --- a/packages/smart-contracts/package.json +++ b/packages/smart-contracts/package.json @@ -35,6 +35,8 @@ "build:lib": "tsc -b tsconfig.build.json && cp src/types/*.d.ts dist/src/types && cp -r dist/src/types types", "build:sol": "yarn hardhat compile", "build": "yarn build:sol && yarn build:lib", + "build:copy": "cp build/src/contracts/BatchConversionPayments.sol/BatchConversionPayments.json src/lib/artifacts/BatchConversionPayments", + "cbuildcopy": "yarn clean && yarn build && yarn build:copy", "clean:types": "shx rm -rf types && shx rm -rf src/types", "clean:lib": "shx rm -rf dist tsconfig.tsbuildinfo tsconfig.build.tsbuildinfo", "clean:hardhat": "shx rm -rf cache && shx rm -rf build", @@ -48,7 +50,10 @@ "ganache": "ganache-cli -l 90000000 -p 8545 -m \"candy maple cake sugar pudding cream honey rich smooth crumble sweet treat\"", "deploy": "yarn hardhat deploy-local-env --network private", "test": "yarn hardhat test --network private", - "test:lib": "yarn jest test/lib" + "test:lib": "yarn jest test/lib", + "testp": "yarn test test/contracts/BatchERC20ConversionPayments.test.ts", + "testcp": "yarn hardhat compile && yarn testp", + "redeploy": "yarn clean && yarn build && yarn deploy" }, "dependencies": { "tslib": "2.3.1" diff --git a/packages/smart-contracts/scripts/test-deploy-all.ts b/packages/smart-contracts/scripts/test-deploy-all.ts index b38550783..392f32525 100644 --- a/packages/smart-contracts/scripts/test-deploy-all.ts +++ b/packages/smart-contracts/scripts/test-deploy-all.ts @@ -5,6 +5,7 @@ import deployConversion from './test-deploy_chainlink_contract'; import { deployEscrow } from './test-deploy-escrow-deployment'; import { deployBatchPayment } from './test-deploy-batch-erc-eth-deployment'; import { deploySuperFluid } from './test-deploy-superfluid'; +import { deployBatchConversionPayment } from './test-deploy-batch-conversion-deployment'; // Deploys, set up the contracts export default async function deploy(_args: any, hre: HardhatRuntimeEnvironment): Promise { @@ -14,4 +15,5 @@ export default async function deploy(_args: any, hre: HardhatRuntimeEnvironment) await deployEscrow(hre); await deployBatchPayment(_args, hre); await deploySuperFluid(hre); + await deployBatchConversionPayment(_args, hre); } diff --git a/packages/smart-contracts/scripts/test-deploy-batch-conversion-deployment.ts b/packages/smart-contracts/scripts/test-deploy-batch-conversion-deployment.ts new file mode 100644 index 000000000..12015a5cd --- /dev/null +++ b/packages/smart-contracts/scripts/test-deploy-batch-conversion-deployment.ts @@ -0,0 +1,68 @@ +import '@nomiclabs/hardhat-ethers'; +import { HardhatRuntimeEnvironment } from 'hardhat/types'; +import { deployOne } from './deploy-one'; + +import { batchConversionPaymentsArtifact } from '../src/lib'; +import { chainlinkConversionPath as chainlinkConvArtifact } from '../src/lib'; +import { CurrencyManager } from '@requestnetwork/currency'; + +// Deploys, set up the contracts +export async function deployBatchConversionPayment( + args: any, + hre: HardhatRuntimeEnvironment, +): Promise { + try { + console.log('start BatchConversionPayments'); + const _ERC20FeeProxyAddress = '0x75c35C980C0d37ef46DF04d31A140b65503c0eEd'; + const _EthereumFeeProxyAddress = '0x3d49d1eF2adE060a33c6E6Aa213513A7EE9a6241'; + const _chainlinkConversionPath = '0x4e71920b7330515faf5EA0c690f1aD06a85fB60c'; + const _paymentErc20ConversionFeeProxy = '0xdE5491f774F0Cb009ABcEA7326342E105dbb1B2E'; + + // Deploy BatchConversionPayments contract + const { address: BatchConversionPaymentsAddress } = await deployOne( + args, + hre, + 'BatchConversionPayments', + { + constructorArguments: [ + _ERC20FeeProxyAddress, + _EthereumFeeProxyAddress, + _paymentErc20ConversionFeeProxy, + _chainlinkConversionPath, + await (await hre.ethers.getSigners())[0].getAddress(), + ], + }, + ); + + // Initialize batch conversion fee, useful to others packages. + const [owner] = await hre.ethers.getSigners(); + const batchConversion = batchConversionPaymentsArtifact.connect(hre.network.name, owner); + await batchConversion.connect(owner).setBasicFee(10); + await batchConversion.connect(owner).setBatchFee(30); + await batchConversion.connect(owner).setBatchConversionFee(30); + + // Add a second ERC20 token and aggregator - useful for batch test + const erc20Factory = await hre.ethers.getContractFactory('TestERC20'); + const testERC20FakeFAU = await erc20Factory.deploy('1000000000000000000000000000000'); + const { address: AggFakeFAU_USD_address } = await deployOne(args, hre, 'AggregatorMock', { + constructorArguments: [201000000, 8, 60], + }); + const conversionPathInstance = chainlinkConvArtifact.connect('private', owner); + const currencyManager = CurrencyManager.getDefault(); + const USD_hash = currencyManager.fromSymbol('USD')!.hash; + await conversionPathInstance.updateAggregatorsList( + [testERC20FakeFAU.address], + [USD_hash], + [AggFakeFAU_USD_address], + ); + + // ---------------------------------- + console.log('Contracts deployed'); + console.log(` + testERC20FakeFAU.address: ${testERC20FakeFAU.address} + BatchConversionPayments: ${BatchConversionPaymentsAddress} + `); + } catch (e) { + console.error(e); + } +} diff --git a/packages/smart-contracts/src/contracts/BatchERC20ConversionPayments.sol b/packages/smart-contracts/src/contracts/BatchERC20ConversionPayments.sol new file mode 100644 index 000000000..63c1ca40b --- /dev/null +++ b/packages/smart-contracts/src/contracts/BatchERC20ConversionPayments.sol @@ -0,0 +1,320 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.4; + +import '@openzeppelin/contracts/token/ERC20/IERC20.sol'; +import './lib/SafeERC20.sol'; +import '@openzeppelin/contracts/access/Ownable.sol'; +import './interfaces/ERC20FeeProxy.sol'; +import './interfaces/EthereumFeeProxy.sol'; +import '@openzeppelin/contracts/security/ReentrancyGuard.sol'; +import './interfaces/IERC20ConversionProxy.sol'; +import '@openzeppelin/contracts/token/ERC20/IERC20.sol'; +import './ChainlinkConversionPath.sol'; +import './BatchPaymentsPublic.sol'; + +/** + * @title BatchConversionPayments + * @notice This contract makes multiple conversion payments with references, in one transaction: + * - on: ERC20 Payment Proxy of the Request Network protocol + * - to: multiple addresses + * - fees: ERC20 proxy fees and additional batch conversion fee are paid to the same address. + * If one transaction of the batch fail, every transactions are reverted. + * @dev Please notify than fees are now divided by 10_000 instead of 1_000 in previous version + */ +contract BatchConversionPayments is BatchPaymentsPublic { + using SafeERC20 for IERC20; + + IERC20ConversionProxy conversionPaymentProxy; + ChainlinkConversionPath public chainlinkConversionPath; + + // @dev: Between 0 and 10000, i.e: batchFee = 100 represent 1% of fee + uint256 public batchConversionFee; + uint256 public basicFee; + + /** + Every information of a request, excepted the feeAddress + */ + struct RequestInfo { + address _recipient; + uint256 _requestAmount; + address[] _path; + bytes _paymentReference; + uint256 _feeAmount; + uint256 _maxToSpend; + uint256 _maxRateTimespan; + } + + /** + * It is the structure of the input for the function from contract BatchPaymentsPublic + */ + struct RequestsInfoParent { + address[] _tokenAddresses; + address[] _recipients; + uint256[] _amounts; + bytes[] _paymentReferences; + uint256[] _feeAmounts; + } + + struct MetaRequestsInfo { + uint256 paymentNetworkId; + RequestInfo[] requestsInfo; + RequestsInfoParent requestsInfoParent; + } + + // Summarize informations for each path + struct Path { + address[] path; + uint256 amountToPay; + uint256 amountToPayInFees; + uint256 maxRateTimespan; + uint256 rate; + uint256 decimals; + uint256 oldestTimestampRate; + } + + /** + * @param _paymentErc20FeeProxy The address to the ERC20 payment proxy to use. + * @param _paymentEthFeeProxy The address to the Ethereum payment proxy to use. + * @param _paymentErc20ConversionFeeProxy The address of the ERC20 Conversion payment proxy to use. + * @param _chainlinkConversionPathAddress The address of the conversion path contract + * @param _owner Owner of the contract. + */ + constructor( + address _paymentErc20FeeProxy, + address _paymentEthFeeProxy, + address _paymentErc20ConversionFeeProxy, + address _chainlinkConversionPathAddress, + address _owner + ) BatchPaymentsPublic(_paymentErc20FeeProxy, _paymentEthFeeProxy, _owner) { + paymentErc20FeeProxy = IERC20FeeProxy(_paymentErc20FeeProxy); + paymentEthFeeProxy = IEthereumFeeProxy(_paymentEthFeeProxy); + + conversionPaymentProxy = IERC20ConversionProxy(_paymentErc20ConversionFeeProxy); + chainlinkConversionPath = ChainlinkConversionPath(_chainlinkConversionPathAddress); + transferOwnership(_owner); + + basicFee = 0; + batchFee = 0; + batchConversionFee = 0; + } + + /** + * Batch payments on different payment network in the same time + * - batchERC20ConversionPaymentsMultiTokens, paymentNetworks: 0 + * - batchERC20PaymentsWithReference, paymentNetworks: 1 + * - batchERC20PaymentsMultiTokensWithReference, paymentNetworks: 2 + * - batchEthPaymentsWithReference, paymentNetworks: 3 + * @param metaRequestsInfos contains paymentNetworkId and requestsInfo + * - paymentNetworkId requests are group by paymentType to be paid with the appropriate function + * - requestsInfo all information required for conversion requests to be paid + * - requestsInfoParent all information required for None-conversion requests to be paid + * @param _feeAddress The address of the proxy to send the fees + */ + function batchRouter(MetaRequestsInfo[] calldata metaRequestsInfos, address _feeAddress) + external + { + require(metaRequestsInfos.length < 4, 'more than 4 requestsinfo'); + for (uint256 i = 0; i < metaRequestsInfos.length; i++) { + MetaRequestsInfo calldata metaRequestsInfo = metaRequestsInfos[i]; + if (metaRequestsInfo.paymentNetworkId == 0) { + batchERC20ConversionPaymentsMultiTokensEasy(metaRequestsInfo.requestsInfo, _feeAddress); + } else if (metaRequestsInfo.paymentNetworkId == 1) { + batchERC20PaymentsWithReference( + metaRequestsInfo.requestsInfoParent._tokenAddresses[0], + metaRequestsInfo.requestsInfoParent._recipients, + metaRequestsInfo.requestsInfoParent._amounts, + metaRequestsInfo.requestsInfoParent._paymentReferences, + metaRequestsInfo.requestsInfoParent._feeAmounts, + _feeAddress + ); + } else if (metaRequestsInfo.paymentNetworkId == 2) { + batchERC20PaymentsMultiTokensWithReference( + metaRequestsInfo.requestsInfoParent._tokenAddresses, + metaRequestsInfo.requestsInfoParent._recipients, + metaRequestsInfo.requestsInfoParent._amounts, + metaRequestsInfo.requestsInfoParent._paymentReferences, + metaRequestsInfo.requestsInfoParent._feeAmounts, + _feeAddress + ); + } else if (metaRequestsInfo.paymentNetworkId == 3) { + batchEthPaymentsWithReference( + metaRequestsInfo.requestsInfoParent._recipients, + metaRequestsInfo.requestsInfoParent._amounts, + metaRequestsInfo.requestsInfoParent._paymentReferences, + metaRequestsInfo.requestsInfoParent._feeAmounts, + payable(_feeAddress) + ); + } else { + revert('wrong paymentNetworkId'); + } + } + } + + /** + * @notice Transfers a batch of ERC20 tokens with a reference with amount based on the request amount in fiat + * @param requestsInfo containing every information of a request + * _recipient Transfer recipients of the payement + * _requestAmount Request amounts + * _path Conversion paths + * _paymentReference References of the payment related + * _feeAmount The amounts of the payment fee + * _maxToSpend Amounts max that we can spend on the behalf of the user: it includes fee proxy but NOT the batchCoversionFee + * _maxRateTimespan Max times span with the oldestrate, ignored if zero + * @param _feeAddress The fee recipient + */ + function batchERC20ConversionPaymentsMultiTokensEasy( + RequestInfo[] calldata requestsInfo, + address _feeAddress + ) public { + Token[] memory uTokens = new Token[](requestsInfo.length); + for (uint256 j = 0; j < requestsInfo.length; j++) { + for (uint256 k = 0; k < requestsInfo.length; k++) { + // If the token is already in the existing uTokens list + if (uTokens[k].tokenAddress == requestsInfo[j]._path[requestsInfo[j]._path.length - 1]) { + uTokens[k].amountAndFee += requestsInfo[j]._maxToSpend; + break; + } + // If the token is not in the list (amountAndFee = 0), and amount + fee > 0 + if (uTokens[k].amountAndFee == 0 && (requestsInfo[j]._maxToSpend) > 0) { + uTokens[k].tokenAddress = requestsInfo[j]._path[requestsInfo[j]._path.length - 1]; + // amountAndFee is used to store _maxToSpend, useful to send enough tokens to this contract + uTokens[k].amountAndFee = requestsInfo[j]._maxToSpend; + break; + } + } + } + + for (uint256 k = 0; k < uTokens.length && uTokens[k].amountAndFee > 0; k++) { + IERC20 requestedToken = IERC20(uTokens[k].tokenAddress); + uTokens[k].batchFeeAmount = (uTokens[k].amountAndFee * batchConversionFee) / 10000; + // Check proxy's allowance from user, and user's funds to pay approximated amounts. + require( + requestedToken.allowance(msg.sender, address(this)) >= uTokens[k].amountAndFee, + 'Not sufficient allowance for batch to pay' + ); + require(requestedToken.balanceOf(msg.sender) >= uTokens[k].amountAndFee, 'not enough funds'); + require( + requestedToken.balanceOf(msg.sender) >= uTokens[k].amountAndFee + uTokens[k].batchFeeAmount, + 'not enough funds to pay approximated batchConversionFee' + ); + + // Transfer the amount and fee required for the token on the batch conversion contract + require( + safeTransferFrom(uTokens[k].tokenAddress, address(this), uTokens[k].amountAndFee), + 'payment transferFrom() failed' + ); + + // Batch contract approves Erc20ConversionProxy to spend the token + if ( + requestedToken.allowance(address(this), address(conversionPaymentProxy)) < + uTokens[k].amountAndFee + ) { + approveConversionPaymentProxyToSpend(uTokens[k].tokenAddress); + } + } + // Batch Conversion contract pays the requests using Erc20ConversionFeeProxy + for (uint256 i = 0; i < requestsInfo.length; i++) { + RequestInfo memory rI = requestsInfo[i]; + conversionPaymentProxy.transferFromWithReferenceAndFee( + rI._recipient, + rI._requestAmount, + rI._path, + rI._paymentReference, + rI._feeAmount, + _feeAddress, + rI._maxToSpend, + rI._maxRateTimespan + ); + } + + // batch send back to the payer the tokens not spent + for (uint256 k = 0; k < uTokens.length && uTokens[k].amountAndFee > 0; k++) { + IERC20 requestedToken = IERC20(uTokens[k].tokenAddress); + + // excessAmount = maxToSpend - reallySpent + uint256 excessAmount = requestedToken.balanceOf(address(this)); + if (excessAmount > 0) { + requestedToken.safeTransfer(msg.sender, excessAmount); + } + + // uint256 amountAndFeePaid = ; + // // amount * (1+.001) = realAmountAndFee + // // amount = realAmountAndFee / 1.001 + // // reduce the usual .1% fee. precision at 1 wei because of solidity rounding. + // uint256 amountPaid = ; + // // --13199869437493199 + // // +-13188118811881187 + // uint256 batchFeeAmount = ; + + // Payer pays batch fee amount + require( + safeTransferFrom( + uTokens[k].tokenAddress, + _feeAddress, + ((((uTokens[k].amountAndFee - excessAmount) * 10000) / (10000 + basicFee)) * + batchConversionFee) / 10000 + ), + 'batch fee transferFrom() failed' + ); + } + } + + /** + * Function to get fresh data from chainlinkConversionPath to do conversion. + */ + function getRate(address[] memory _path, uint256 _maxRateTimespan) + internal + view + returns ( + uint256, + uint256, + uint256 + ) + { + (uint256 rate, uint256 oldestTimestampRate, uint256 decimals) = chainlinkConversionPath.getRate( + _path + ); + + // Check rate timespan + require( + _maxRateTimespan == 0 || block.timestamp - oldestTimestampRate <= _maxRateTimespan, + 'aggregator rate is outdated' + ); + return (rate, decimals, oldestTimestampRate); + } + + /** + * @notice Authorizes the conveersion proxy to spend a new request currency (ERC20). + * @param _erc20Address Address of an ERC20 used as the request currency. + */ + function approveConversionPaymentProxyToSpend(address _erc20Address) public { + IERC20 erc20 = IERC20(_erc20Address); + uint256 max = 2**256 - 1; + erc20.safeApprove(address(conversionPaymentProxy), max); + } + + /* + * Admin functions to edit the conversion proxies address + */ + + /** fees applied on a single request*/ + function setBasicFee(uint256 _basicFee) public onlyOwner { + basicFee = _basicFee; + } + + function setBatchConversionFee(uint256 _batchConversionFee) public onlyOwner { + batchConversionFee = _batchConversionFee; + } + + function setConversionPaymentProxy(address _paymentErc20ConversionFeeProxy) public onlyOwner { + conversionPaymentProxy = IERC20ConversionProxy(_paymentErc20ConversionFeeProxy); + } + + /** + * @notice Update the conversion path contract used to fetch conversions + * @param _chainlinkConversionPathAddress address of the conversion path contract + */ + function setConversionPathAddress(address _chainlinkConversionPathAddress) external onlyOwner { + chainlinkConversionPath = ChainlinkConversionPath(_chainlinkConversionPathAddress); + } +} diff --git a/packages/smart-contracts/src/contracts/BatchPaymentsPublic.sol b/packages/smart-contracts/src/contracts/BatchPaymentsPublic.sol new file mode 100644 index 000000000..bafcdffbd --- /dev/null +++ b/packages/smart-contracts/src/contracts/BatchPaymentsPublic.sol @@ -0,0 +1,359 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.4; + +import '@openzeppelin/contracts/token/ERC20/IERC20.sol'; +import './lib/SafeERC20.sol'; +import '@openzeppelin/contracts/access/Ownable.sol'; +import './interfaces/ERC20FeeProxy.sol'; +import './interfaces/EthereumFeeProxy.sol'; +import '@openzeppelin/contracts/security/ReentrancyGuard.sol'; + +/** + * @title BatchPayments + * @notice This contract makes multiple payments with references, in one transaction: + * - on: ERC20 Payment Proxy and ETH Payment Proxy of the Request Network protocol + * - to: multiple addresses + * - fees: ERC20 and ETH proxies fees are paid to the same address. + * An additional batch fee is paid to the same address. + * If one transaction of the batch fail, every transactions are reverted. + */ +contract BatchPaymentsPublic is Ownable, ReentrancyGuard { + using SafeERC20 for IERC20; + + IERC20FeeProxy public paymentErc20FeeProxy; + IEthereumFeeProxy public paymentEthFeeProxy; + + // @dev: Between 0 and 10000, i.e: batchFee = 100 represent 1% of fee + uint256 public batchFee; + + struct Token { + address tokenAddress; + uint256 amountAndFee; + uint256 batchFeeAmount; + } + + /** + * @param _paymentErc20FeeProxy The address to the ERC20 payment proxy to use. + * @param _paymentEthFeeProxy The address to the Ethereum payment proxy to use. + * @param _owner Owner of the contract. + */ + constructor( + address _paymentErc20FeeProxy, + address _paymentEthFeeProxy, + address _owner + ) { + paymentErc20FeeProxy = IERC20FeeProxy(_paymentErc20FeeProxy); + paymentEthFeeProxy = IEthereumFeeProxy(_paymentEthFeeProxy); + transferOwnership(_owner); + batchFee = 0; + } + + // batch Eth requires batch contract to receive funds from ethFeeProxy + receive() external payable { + require(msg.value == 0, 'Non-payable'); + } + + /** + * @notice Send a batch of Eth payments w/fees with paymentReferences to multiple accounts. + * If one payment failed, the whole batch is reverted + * @param _recipients List of recipients accounts. + * @param _amounts List of amounts, corresponding to recipients[]. + * @param _paymentReferences List of paymentRefs, corr. to the recipients[]. + * @param _feeAmounts List of amounts of the payment fee, corr. to the recipients[]. + * @param _feeAddress The fee recipient. + * @dev It uses EthereumFeeProxy to pay an invoice and fees, with a payment reference. + * Make sure: msg.value >= sum(_amouts)+sum(_feeAmounts)+sumBatchFeeAmount + */ + function batchEthPaymentsWithReference( + address[] calldata _recipients, + uint256[] calldata _amounts, + bytes[] calldata _paymentReferences, + uint256[] calldata _feeAmounts, + address payable _feeAddress + ) public payable nonReentrant { + require( + _recipients.length == _amounts.length && + _recipients.length == _paymentReferences.length && + _recipients.length == _feeAmounts.length, + 'the input arrays must have the same length' + ); + + // amount is used to get the total amount and then used as batch fee amount + uint256 amount = 0; + + // Batch contract pays the requests thourgh EthFeeProxy + for (uint256 i = 0; i < _recipients.length; i++) { + require(address(this).balance >= _amounts[i] + _feeAmounts[i], 'not enough funds'); + amount += _amounts[i]; + + paymentEthFeeProxy.transferWithReferenceAndFee{value: _amounts[i] + _feeAmounts[i]}( + payable(_recipients[i]), + _paymentReferences[i], + _feeAmounts[i], + payable(_feeAddress) + ); + } + + // amount is updated into batch fee amount + amount = (amount * batchFee) / 10000; + // Check that batch contract has enough funds to pay batch fee + require(address(this).balance >= amount, 'not enough funds for batch fee'); + // Batch pays batch fee + _feeAddress.transfer(amount); + + // Batch contract transfers the remaining ethers to the payer + if (address(this).balance > 0) { + (bool sendBackSuccess, ) = payable(msg.sender).call{value: address(this).balance}(''); + require(sendBackSuccess, 'Could not send remaining funds to the payer'); + } + } + + /** + * @notice Send a batch of erc20 payments w/fees with paymentReferences to multiple accounts. + * @param _tokenAddress Token to transact with. + * @param _recipients List of recipients accounts. + * @param _amounts List of amounts, corresponding to recipients[]. + * @param _paymentReferences List of paymentRefs, corr. to the recipients[] and . + * @param _feeAmounts List of amounts of the payment fee, corr. to the recipients[]. + * @param _feeAddress The fee recipient. + * @dev Uses ERC20FeeProxy to pay an invoice and fees, with a payment reference. + * Make sure the contract has allowance to spend the payer token. + * Make sure the payer has enough tokens to pay the amount, the fee, the batch fee + */ + function batchERC20PaymentsWithReference( + address _tokenAddress, + address[] calldata _recipients, + uint256[] calldata _amounts, + bytes[] calldata _paymentReferences, + uint256[] calldata _feeAmounts, + address _feeAddress + ) public { + require( + _recipients.length == _amounts.length && + _recipients.length == _paymentReferences.length && + _recipients.length == _feeAmounts.length, + 'the input arrays must have the same length' + ); + + // amount is used to get the total amount and fee, and then used as batch fee amount + uint256 amount = 0; + for (uint256 i = 0; i < _recipients.length; i++) { + amount += _amounts[i] + _feeAmounts[i]; + } + + // Transfer the amount and fee from the payer to the batch contract + IERC20 requestedToken = IERC20(_tokenAddress); + require( + requestedToken.allowance(msg.sender, address(this)) >= amount, + 'Not sufficient allowance for batch to pay' + ); + require(requestedToken.balanceOf(msg.sender) >= amount, 'not enough funds'); + require( + safeTransferFrom(_tokenAddress, address(this), amount), + 'payment transferFrom() failed' + ); + + // Batch contract approve Erc20FeeProxy to spend the token + if (requestedToken.allowance(address(this), address(paymentErc20FeeProxy)) < amount) { + approvePaymentProxyToSpend(address(requestedToken)); + } + + // Batch contract pays the requests using Erc20FeeProxy + for (uint256 i = 0; i < _recipients.length; i++) { + // amount is updated to become the sum of amounts, to calculate batch fee amount + amount -= _feeAmounts[i]; + paymentErc20FeeProxy.transferFromWithReferenceAndFee( + _tokenAddress, + _recipients[i], + _amounts[i], + _paymentReferences[i], + _feeAmounts[i], + _feeAddress + ); + } + + // amount is updated into batch fee amount + amount = (amount * batchFee) / 10000; + // Check if the payer has enough funds to pay batch fee + require(requestedToken.balanceOf(msg.sender) >= amount, 'not enough funds for the batch fee'); + + // Payer pays batch fee amount + require( + safeTransferFrom(_tokenAddress, _feeAddress, amount), + 'batch fee transferFrom() failed' + ); + } + + /** + * @notice Send a batch of erc20 payments on multiple tokens w/fees with paymentReferences to multiple accounts. + * @param _tokenAddresses List of tokens to transact with. + * @param _recipients List of recipients accounts. + * @param _amounts List of amounts, corresponding to recipients[]. + * @param _paymentReferences List of paymentRefs, corr. to the recipients[]. + * @param _feeAmounts List of amounts of the payment fee, corr. to the recipients[]. + * @param _feeAddress The fee recipient. + * @dev It uses ERC20FeeProxy to pay an invoice and fees, with a payment reference. + * Make sure the contract has allowance to spend the payer token. + * Make sure the payer has enough tokens to pay the amount, the fee, the batch fee + */ + function batchERC20PaymentsMultiTokensWithReference( + address[] calldata _tokenAddresses, + address[] calldata _recipients, + uint256[] calldata _amounts, + bytes[] calldata _paymentReferences, + uint256[] calldata _feeAmounts, + address _feeAddress + ) public { + require( + _tokenAddresses.length == _recipients.length && + _tokenAddresses.length == _amounts.length && + _tokenAddresses.length == _paymentReferences.length && + _tokenAddresses.length == _feeAmounts.length, + 'the input arrays must have the same length' + ); + + // Create a list of unique tokens used and the amounts associated + // Only considere tokens having: amounts + feeAmounts > 0 + // batchFeeAmount is the amount's sum, and then, batch fee rate is applied + Token[] memory uniqueTokens = new Token[](_tokenAddresses.length); + for (uint256 i = 0; i < _tokenAddresses.length; i++) { + for (uint256 j = 0; j < _tokenAddresses.length; j++) { + // If the token is already in the existing uniqueTokens list + if (uniqueTokens[j].tokenAddress == _tokenAddresses[i]) { + uniqueTokens[j].amountAndFee += _amounts[i] + _feeAmounts[i]; + uniqueTokens[j].batchFeeAmount += _amounts[i]; + break; + } + // If the token is not in the list (amountAndFee = 0), and amount + fee > 0 + if (uniqueTokens[j].amountAndFee == 0 && (_amounts[i] + _feeAmounts[i]) > 0) { + uniqueTokens[j].tokenAddress = _tokenAddresses[i]; + uniqueTokens[j].amountAndFee = _amounts[i] + _feeAmounts[i]; + uniqueTokens[j].batchFeeAmount = _amounts[i]; + break; + } + } + } + + // The payer transfers tokens to the batch contract and pays batch fee + for (uint256 i = 0; i < uniqueTokens.length && uniqueTokens[i].amountAndFee > 0; i++) { + uniqueTokens[i].batchFeeAmount = (uniqueTokens[i].batchFeeAmount * batchFee) / 10000; + IERC20 requestedToken = IERC20(uniqueTokens[i].tokenAddress); + + require( + requestedToken.allowance(msg.sender, address(this)) >= + uniqueTokens[i].amountAndFee + uniqueTokens[i].batchFeeAmount, + 'Not sufficient allowance for batch to pay' + ); + // check if the payer can pay the amount, the fee, and the batchFee + require( + requestedToken.balanceOf(msg.sender) >= + uniqueTokens[i].amountAndFee + uniqueTokens[i].batchFeeAmount, + 'not enough funds' + ); + + // Transfer only the amount and fee required for the token on the batch contract + require( + safeTransferFrom(uniqueTokens[i].tokenAddress, address(this), uniqueTokens[i].amountAndFee), + 'payment transferFrom() failed' + ); + + // Batch contract approves Erc20FeeProxy to spend the token + if ( + requestedToken.allowance(address(this), address(paymentErc20FeeProxy)) < + uniqueTokens[i].amountAndFee + ) { + approvePaymentProxyToSpend(address(requestedToken)); + } + + // Payer pays batch fee amount + require( + safeTransferFrom(uniqueTokens[i].tokenAddress, _feeAddress, uniqueTokens[i].batchFeeAmount), + 'batch fee transferFrom() failed' + ); + } + + // Batch contract pays the requests using Erc20FeeProxy + for (uint256 i = 0; i < _recipients.length; i++) { + paymentErc20FeeProxy.transferFromWithReferenceAndFee( + _tokenAddresses[i], + _recipients[i], + _amounts[i], + _paymentReferences[i], + _feeAmounts[i], + _feeAddress + ); + } + } + + /** + * @notice Authorizes the proxy to spend a new request currency (ERC20). + * @param _erc20Address Address of an ERC20 used as the request currency. + */ + function approvePaymentProxyToSpend(address _erc20Address) public { + IERC20 erc20 = IERC20(_erc20Address); + uint256 max = 2**256 - 1; + erc20.safeApprove(address(paymentErc20FeeProxy), max); + } + + /** + * @notice Call transferFrom ERC20 function and validates the return data of a ERC20 contract call. + * @dev This is necessary because of non-standard ERC20 tokens that don't have a return value. + * @return result The return value of the ERC20 call, returning true for non-standard tokens + */ + function safeTransferFrom( + address _tokenAddress, + address _to, + uint256 _amount + ) internal returns (bool result) { + /* solium-disable security/no-inline-assembly */ + // check if the address is a contract + assembly { + if iszero(extcodesize(_tokenAddress)) { + revert(0, 0) + } + } + + // solium-disable-next-line security/no-low-level-calls + (bool success, ) = _tokenAddress.call( + abi.encodeWithSignature('transferFrom(address,address,uint256)', msg.sender, _to, _amount) + ); + + assembly { + switch returndatasize() + case 0 { + // Not a standard erc20 + result := 1 + } + case 32 { + // Standard erc20 + returndatacopy(0, 0, 32) + result := mload(0) + } + default { + // Anything else, should revert for safety + revert(0, 0) + } + } + + require(success, 'transferFrom() has been reverted'); + + /* solium-enable security/no-inline-assembly */ + return result; + } + + /* + * Admin functions to edit the proxies address + */ + + function setBatchFee(uint256 _batchFee) public onlyOwner { + batchFee = _batchFee; + } + + function setPaymentErc20FeeProxy(address _paymentErc20FeeProxy) public onlyOwner { + paymentErc20FeeProxy = IERC20FeeProxy(_paymentErc20FeeProxy); + } + + function setPaymentEthFeeProxy(address _paymentEthFeeProxy) public onlyOwner { + paymentEthFeeProxy = IEthereumFeeProxy(_paymentEthFeeProxy); + } +} diff --git a/packages/smart-contracts/src/lib/artifacts/BatchConversionPayments/0.1.0.json b/packages/smart-contracts/src/lib/artifacts/BatchConversionPayments/0.1.0.json new file mode 100644 index 000000000..766fb754c --- /dev/null +++ b/packages/smart-contracts/src/lib/artifacts/BatchConversionPayments/0.1.0.json @@ -0,0 +1,549 @@ +{ + "abi": [ + { + "inputs": [ + { + "internalType": "address", + "name": "_paymentErc20FeeProxy", + "type": "address" + }, + { + "internalType": "address", + "name": "_paymentEthFeeProxy", + "type": "address" + }, + { + "internalType": "address", + "name": "_paymentErc20ConversionFeeProxy", + "type": "address" + }, + { + "internalType": "address", + "name": "_chainlinkConversionPathAddress", + "type": "address" + }, + { + "internalType": "address", + "name": "_owner", + "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" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_erc20Address", + "type": "address" + } + ], + "name": "approveConversionPaymentProxyToSpend", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_erc20Address", + "type": "address" + } + ], + "name": "approvePaymentProxyToSpend", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "basicFee", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "batchConversionFee", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "internalType": "address", + "name": "_recipient", + "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": "uint256", + "name": "_maxToSpend", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "_maxRateTimespan", + "type": "uint256" + } + ], + "internalType": "struct BatchConversionPayments.RequestInfo[]", + "name": "requestsInfo", + "type": "tuple[]" + }, + { + "internalType": "address", + "name": "_feeAddress", + "type": "address" + } + ], + "name": "batchERC20ConversionPaymentsMultiTokensEasy", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address[]", + "name": "_tokenAddresses", + "type": "address[]" + }, + { + "internalType": "address[]", + "name": "_recipients", + "type": "address[]" + }, + { + "internalType": "uint256[]", + "name": "_amounts", + "type": "uint256[]" + }, + { + "internalType": "bytes[]", + "name": "_paymentReferences", + "type": "bytes[]" + }, + { + "internalType": "uint256[]", + "name": "_feeAmounts", + "type": "uint256[]" + }, + { + "internalType": "address", + "name": "_feeAddress", + "type": "address" + } + ], + "name": "batchERC20PaymentsMultiTokensWithReference", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_tokenAddress", + "type": "address" + }, + { + "internalType": "address[]", + "name": "_recipients", + "type": "address[]" + }, + { + "internalType": "uint256[]", + "name": "_amounts", + "type": "uint256[]" + }, + { + "internalType": "bytes[]", + "name": "_paymentReferences", + "type": "bytes[]" + }, + { + "internalType": "uint256[]", + "name": "_feeAmounts", + "type": "uint256[]" + }, + { + "internalType": "address", + "name": "_feeAddress", + "type": "address" + } + ], + "name": "batchERC20PaymentsWithReference", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address[]", + "name": "_recipients", + "type": "address[]" + }, + { + "internalType": "uint256[]", + "name": "_amounts", + "type": "uint256[]" + }, + { + "internalType": "bytes[]", + "name": "_paymentReferences", + "type": "bytes[]" + }, + { + "internalType": "uint256[]", + "name": "_feeAmounts", + "type": "uint256[]" + }, + { + "internalType": "address payable", + "name": "_feeAddress", + "type": "address" + } + ], + "name": "batchEthPaymentsWithReference", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [], + "name": "batchFee", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "internalType": "uint256", + "name": "paymentNetworkId", + "type": "uint256" + }, + { + "components": [ + { + "internalType": "address", + "name": "_recipient", + "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": "uint256", + "name": "_maxToSpend", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "_maxRateTimespan", + "type": "uint256" + } + ], + "internalType": "struct BatchConversionPayments.RequestInfo[]", + "name": "requestsInfo", + "type": "tuple[]" + }, + { + "components": [ + { + "internalType": "address[]", + "name": "_tokenAddresses", + "type": "address[]" + }, + { + "internalType": "address[]", + "name": "_recipients", + "type": "address[]" + }, + { + "internalType": "uint256[]", + "name": "_amounts", + "type": "uint256[]" + }, + { + "internalType": "bytes[]", + "name": "_paymentReferences", + "type": "bytes[]" + }, + { + "internalType": "uint256[]", + "name": "_feeAmounts", + "type": "uint256[]" + } + ], + "internalType": "struct BatchConversionPayments.RequestsInfoParent", + "name": "requestsInfoParent", + "type": "tuple" + } + ], + "internalType": "struct BatchConversionPayments.MetaRequestsInfo[]", + "name": "metaRequestsInfos", + "type": "tuple[]" + }, + { + "internalType": "address", + "name": "_feeAddress", + "type": "address" + } + ], + "name": "batchRouter", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "chainlinkConversionPath", + "outputs": [ + { + "internalType": "contract ChainlinkConversionPath", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "owner", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "paymentErc20FeeProxy", + "outputs": [ + { + "internalType": "contract IERC20FeeProxy", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "paymentEthFeeProxy", + "outputs": [ + { + "internalType": "contract IEthereumFeeProxy", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "renounceOwnership", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "_basicFee", + "type": "uint256" + } + ], + "name": "setBasicFee", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "_batchConversionFee", + "type": "uint256" + } + ], + "name": "setBatchConversionFee", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "_batchFee", + "type": "uint256" + } + ], + "name": "setBatchFee", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_chainlinkConversionPathAddress", + "type": "address" + } + ], + "name": "setConversionPathAddress", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_paymentErc20ConversionFeeProxy", + "type": "address" + } + ], + "name": "setConversionPaymentProxy", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_paymentErc20FeeProxy", + "type": "address" + } + ], + "name": "setPaymentErc20FeeProxy", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_paymentEthFeeProxy", + "type": "address" + } + ], + "name": "setPaymentEthFeeProxy", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "newOwner", + "type": "address" + } + ], + "name": "transferOwnership", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "stateMutability": "payable", + "type": "receive" + } + ] +} diff --git a/packages/smart-contracts/src/lib/artifacts/BatchConversionPayments/index.ts b/packages/smart-contracts/src/lib/artifacts/BatchConversionPayments/index.ts new file mode 100644 index 000000000..d83849d90 --- /dev/null +++ b/packages/smart-contracts/src/lib/artifacts/BatchConversionPayments/index.ts @@ -0,0 +1,20 @@ +import { ContractArtifact } from '../../ContractArtifact'; + +import { abi as ABI_0_1_0 } from './0.1.0.json'; +// @ts-ignore Cannot find module +import type { BatchConversionPayments } from '../../../types/BatchConversionPayments'; + +export const batchConversionPaymentsArtifact = new ContractArtifact( + { + '0.1.0': { + abi: ABI_0_1_0, + deployment: { + private: { + address: '0x2e335F247E91caa168c64b63104C4475b2af3942', + 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 72b4f2f15..e807a3e58 100644 --- a/packages/smart-contracts/src/lib/artifacts/index.ts +++ b/packages/smart-contracts/src/lib/artifacts/index.ts @@ -12,6 +12,7 @@ export * from './EthereumFeeProxy'; export * from './EthConversionProxy'; export * from './ERC20EscrowToPay'; export * from './BatchPayments'; +export * from './BatchConversionPayments'; /** * Request Storage */ diff --git a/packages/smart-contracts/test/contracts/BatchERC20ConversionPayments.test.ts b/packages/smart-contracts/test/contracts/BatchERC20ConversionPayments.test.ts new file mode 100644 index 000000000..d1cccc109 --- /dev/null +++ b/packages/smart-contracts/test/contracts/BatchERC20ConversionPayments.test.ts @@ -0,0 +1,396 @@ +import { ethers, network } from 'hardhat'; +import { + ERC20FeeProxy__factory, + Erc20ConversionProxy__factory, + BatchConversionPayments__factory, + EthereumFeeProxy__factory, + ERC20FeeProxy, + EthereumFeeProxy, + ChainlinkConversionPath, + TestERC20, + Erc20ConversionProxy, + TestERC20__factory, + BatchConversionPayments, +} from '../../src/types'; +import { BigNumber, BigNumberish, BytesLike, ContractTransaction, Signer } from 'ethers'; +import { expect } from 'chai'; +import { CurrencyManager } from '@requestnetwork/currency'; +import { chainlinkConversionPath } from '../../src/lib'; +import { localERC20AlphaArtifact } from './localArtifacts'; +import Utils from '@requestnetwork/utils'; + +describe('contract: BatchErc20ConversionPayments', () => { + let from: string; + let to: string; + let feeAddress: string; + let batchAddress: string; + let signer: Signer; + const basicFee = 10; + const batchFee = 30; + const batchConvFee = 100; + const amountInFiat = '100000000'; // 1 with 8 decimal + const feesAmountInFiat = '100000'; // 0.001 with 8 decimal + const thousandWith18Decimal = '1000000000000000000000'; + // const hundredWith18Decimal = '100000000000000000000'; + 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 DAI_address: string; + let USDT_address: string; + let fakeFAU_address: string; + + let testErc20ConversionProxy: Erc20ConversionProxy; + let testBatchConversionProxy: BatchConversionPayments; + let testERC20: TestERC20; + let testERC20b: TestERC20; + let erc20FeeProxy: ERC20FeeProxy; + let ethereumFeeProxy: EthereumFeeProxy; + let chainlinkPath: ChainlinkConversionPath; + + let path: string[]; + type ConvToPay = [BigNumber, BigNumber] & { + result: BigNumber; + oldestRateTimestamp: BigNumber; + }; + let conversionToPay: ConvToPay; + let conversionFees: ConvToPay; + + let fromOldBalance: BigNumber; + let toOldBalance: BigNumber; + let feeOldBalance: BigNumber; + let batchOldBalance: BigNumber; + + let fromBalance: BigNumber; + let toBalance: BigNumber; + let feeBalance: BigNumber; + let batchBalance: BigNumber; + + let fromDiffBalanceExpected: BigNumber; + let toDiffBalanceExpected: BigNumber; + let feeDiffBalanceExpected: BigNumber; + + type RequestInfo = { + _recipient: string; + _requestAmount: BigNumberish; + _path: string[]; + _paymentReference: BytesLike; + _feeAmount: BigNumberish; + _maxToSpend: BigNumberish; + _maxRateTimespan: BigNumberish; + }; + let requestInfo: RequestInfo; + + // type + let requestsInfoParent1 = { + _tokenAddresses: [], + _recipients: [], + _amounts: [], + _paymentReferences: [], + _feeAmounts: [], + }; + let emitOneTx: Function; + + let batchConvFunction: (args: any, feeAddress: string) => Promise; + let argTemplate: Function; + + before(async () => { + [from, to, feeAddress] = (await ethers.getSigners()).map((s) => s.address); + [signer] = await ethers.getSigners(); + + chainlinkPath = chainlinkConversionPath.connect(network.name, signer); + erc20FeeProxy = await new ERC20FeeProxy__factory(signer).deploy(); + ethereumFeeProxy = await new EthereumFeeProxy__factory(signer).deploy(); + testErc20ConversionProxy = await new Erc20ConversionProxy__factory(signer).deploy( + erc20FeeProxy.address, + chainlinkPath.address, + await signer.getAddress(), + ); + testBatchConversionProxy = await new BatchConversionPayments__factory(signer).deploy( + erc20FeeProxy.address, + ethereumFeeProxy.address, + testErc20ConversionProxy.address, + chainlinkPath.address, + await signer.getAddress(), + ); + + await testBatchConversionProxy.setBasicFee(basicFee); + await testBatchConversionProxy.setBatchFee(batchFee); + await testBatchConversionProxy.setBatchConversionFee(batchConvFee); + + DAI_address = localERC20AlphaArtifact.getAddress(network.name); + testERC20 = new TestERC20__factory(signer).attach(DAI_address); + + fakeFAU_address = '0x7153CCD1a20Bbb2f6dc89c1024de368326EC6b4F'; + testERC20b = new TestERC20__factory(signer).attach(fakeFAU_address); + USDT_address = '0xF328c11c4dF88d18FcBd30ad38d8B4714F4b33bF'; // '0xF328c11c4dF88d18FcBd30ad38d8B4714F4b33bF'; + batchAddress = testBatchConversionProxy.address; + emitOneTx = ( + result: Chai.Assertion, + requestInfo: RequestInfo, + _conversionToPay = conversionToPay, + _conversionFees = conversionFees, + _testErc20ConversionProxy = testErc20ConversionProxy, + ) => { + return result.to + .emit(testErc20ConversionProxy, 'TransferWithConversionAndReference') + .withArgs( + requestInfo._requestAmount, + ethers.utils.getAddress(requestInfo._path[0]), + ethers.utils.keccak256(referenceExample), + requestInfo._feeAmount, + '0', + ) + .to.emit(testErc20ConversionProxy, 'TransferWithReferenceAndFee') + .withArgs( + ethers.utils.getAddress(DAI_address), + ethers.utils.getAddress(requestInfo._recipient), + _conversionToPay.result, + ethers.utils.keccak256(referenceExample), + _conversionFees.result, + feeAddress, + ); + }; + }); + + const batchFeeToPay = (conversionAmountToPay: BigNumber) => { + return conversionAmountToPay.mul(batchConvFee).div(10000); + }; + + const initConvToPayAndRequestInfo = async ( + _recipient: string, + _path: string[], + _requestAmount: string, + _feeAmount: string, + _maxRateTimespan: number, + _chainlinkPath: ChainlinkConversionPath, + ) => { + conversionToPay = await _chainlinkPath.getConversion(_requestAmount, _path); + conversionFees = await _chainlinkPath.getConversion(_feeAmount, _path); + requestInfo = { + _recipient: _recipient, + _requestAmount: _requestAmount, + _path: _path, + _paymentReference: referenceExample, + _feeAmount: _feeAmount, + _maxToSpend: conversionToPay.result.add(conversionFees.result).toString(), + _maxRateTimespan: _maxRateTimespan, + }; + }; + beforeEach(async () => { + fromDiffBalanceExpected = BigNumber.from(0); + toDiffBalanceExpected = BigNumber.from(0); + feeDiffBalanceExpected = BigNumber.from(0); + path = [USD_hash, DAI_address]; + initConvToPayAndRequestInfo(to, path, amountInFiat, feesAmountInFiat, 0, chainlinkPath); + await testERC20.approve(testBatchConversionProxy.address, thousandWith18Decimal, { + from, + }); + await testERC20b.approve(testBatchConversionProxy.address, thousandWith18Decimal, { + from, + }); + fromOldBalance = await testERC20.connect(signer).balanceOf(from); + toOldBalance = await testERC20.balanceOf(to); + feeOldBalance = await testERC20.balanceOf(feeAddress); + batchOldBalance = await testERC20.balanceOf(batchAddress); + }); + afterEach(async () => { + fromBalance = await testERC20.balanceOf(from); + toBalance = await testERC20.balanceOf(to); + feeBalance = await testERC20.balanceOf(feeAddress); + batchBalance = await testERC20.balanceOf(batchAddress); + + const fromDiffBalance = BigNumber.from(fromBalance.toString()) + .sub(fromOldBalance.toString()) + .toString(); + const toDiffBalance = BigNumber.from(toBalance.toString()) + .sub(toOldBalance.toString()) + .toString(); + const feeDiffBalance = BigNumber.from(feeBalance.toString()) + .sub(feeOldBalance.toString()) + .toString(); + const batchDiffBalance = BigNumber.from(batchBalance.toString()) + .sub(batchOldBalance.toString()) + .toString(); + + // Check balance changes + expect(fromDiffBalance).to.equals( + (fromDiffBalanceExpected.toString() !== '0' ? '-' : '') + fromDiffBalanceExpected.toString(), + 'fromDiffBalance', + ); + expect(toDiffBalance).to.equals(toDiffBalanceExpected.toString(), 'toDiffBalance'); + expect(feeDiffBalance).to.equals(feeDiffBalanceExpected.toString(), 'feeDiffBalance'); + expect(batchDiffBalance).to.equals('0', 'batchDiffBalance'); + }); + + const calculBalances = ( + _conversionToPay: ConvToPay, + _conversionFees: ConvToPay, + _conversionsToPay: ConvToPay[], + ) => { + fromDiffBalanceExpected = fromDiffBalanceExpected + .add(_conversionToPay.result) + .add(_conversionFees.result); + + toDiffBalanceExpected = toDiffBalanceExpected.add(_conversionToPay.result); + feeDiffBalanceExpected = feeDiffBalanceExpected.add(_conversionFees.result); + if (_conversionsToPay.length > 0) calculBatchFeeBalances(_conversionsToPay); + }; + + const calculBatchFeeBalances = (_conversionsToPay: ConvToPay[]) => { + let sumToPay = BigNumber.from(0); + for (let i = 0; i < _conversionsToPay.length; i++) { + sumToPay = sumToPay.add(_conversionsToPay[i].result); + } + fromDiffBalanceExpected = fromDiffBalanceExpected.add(batchFeeToPay(sumToPay)); + feeDiffBalanceExpected = feeDiffBalanceExpected.add(batchFeeToPay(sumToPay)); + }; + + const transferOneTokenConv = async (path: string[], logGas = false) => { + await initConvToPayAndRequestInfo(to, path, amountInFiat, feesAmountInFiat, 0, chainlinkPath); + + const result = batchConvFunction(argTemplate([requestInfo]), feeAddress); + if (logGas) { + const tx = await result; + await tx.wait(1); + const receipt = await tx.wait(); + console.log(`gas consumption: `, receipt.gasUsed.toString()); + } else { + await emitOneTx(expect(result), requestInfo, conversionToPay, conversionFees); + } + + calculBalances(conversionToPay, conversionFees, [conversionToPay]); + }; + + const twoTransferOneTokenConv = async (path2: string[], nTimes: number, logGas = false) => { + const coef = 2; + const amountInFiat2 = BigNumber.from(amountInFiat).mul(coef).toString(); + const feesAmountInFiat2 = BigNumber.from(feesAmountInFiat).mul(coef).toString(); + + const conversionToPay2 = await chainlinkPath.getConversion(amountInFiat2, path2); + const conversionFees2 = await chainlinkPath.getConversion(feesAmountInFiat2, path2); + + let requestInfo2 = Utils.deepCopy(requestInfo); + + requestInfo2._path = path2; + requestInfo2._requestAmount = amountInFiat2; + requestInfo2._feeAmount = feesAmountInFiat2; + requestInfo2._maxToSpend = conversionToPay2.result.add(conversionFees2.result).toString(); + + let requestInfos: RequestInfo[] = []; + let conversionsToPay: ConvToPay[] = []; + for (let i = 0; i < nTimes; i++) { + requestInfos = requestInfos.concat([requestInfo, requestInfo2]); + conversionsToPay = conversionsToPay.concat([conversionToPay, conversionToPay2]); + } + const result = batchConvFunction(argTemplate(requestInfos), feeAddress); + const tx = await result; + await tx.wait(1); + if (logGas) { + const receipt = await tx.wait(); + console.log(`${2 * nTimes} req, gas consumption: `, receipt.gasUsed.toString()); + } + + if ( + requestInfo._path[requestInfo._path.length - 1] === + requestInfo2._path[requestInfo2._path.length - 1] + ) { + for (let i = 0; i < nTimes - 1; i++) { + calculBalances(conversionToPay, conversionFees, []); + calculBalances(conversionToPay2, conversionFees2, []); + } + calculBalances(conversionToPay, conversionFees, []); + calculBalances(conversionToPay2, conversionFees2, conversionsToPay); + } else { + for (let i = 0; i < nTimes - 1; i++) { + calculBalances(conversionToPay, conversionFees, []); + } + const conversionsToPayBis = conversionsToPay.filter((_, i) => i % 2 === 0); + + calculBalances(conversionToPay, conversionFees, conversionsToPayBis); + } + }; + + const testSuite = (suiteName: string) => { + describe(suiteName, () => { + before(() => { + if (suiteName === 'batchRouter') { + batchConvFunction = testBatchConversionProxy.batchRouter; + argTemplate = (requestInfos: RequestInfo[]) => { + return [ + { + paymentNetworkId: '0', + requestsInfo: requestInfos, + requestsInfoParent: requestsInfoParent1, + }, + ]; + }; + } + if (suiteName === 'batchERC20ConversionPaymentsMultiTokensEasy') { + batchConvFunction = testBatchConversionProxy.batchERC20ConversionPaymentsMultiTokensEasy; + argTemplate = (requestInfos: RequestInfo[]) => { + return requestInfos; + }; + } + }); + + describe('batchERC20ConversionPaymentsMultiTokensEasy with DAI', async () => { + it('allows to transfer DAI tokens for USD payment', async () => { + await transferOneTokenConv(path); + }); + it('allows to transfer DAI tokens for EUR payment', async () => { + path = [EUR_hash, USD_hash, DAI_address]; + await transferOneTokenConv(path); + }); + it('allows to transfer 2 transactions DAI tokens for USD payment', async function () { + await twoTransferOneTokenConv(path, 1); + }); + it('allows to transfer DAI tokens for EUR payment - GAS', async () => { + path = [EUR_hash, USD_hash, DAI_address]; + await transferOneTokenConv(path, true); + }); + it('allows to transfer 2 transactions DAI tokens for USD and EUR payment - GAS', async function () { + const path2 = [EUR_hash, USD_hash, DAI_address]; + await twoTransferOneTokenConv(path2, 1, true); + }); + it('TMP allows to transfer two kind of tokens for USD - GAS', async function () { + const path2 = [USD_hash, fakeFAU_address]; + await twoTransferOneTokenConv(path2, 1, true); + }); + }); + }); + + describe('batchERC20ConversionPaymentsMultiTokensEasy with errors', () => { + it('cannot transfer with invalid path', async function () { + const wrongPath = [EUR_hash, ETH_hash, DAI_address]; + requestInfo._path = wrongPath; + await expect(batchConvFunction(argTemplate([requestInfo]), feeAddress)).to.be.revertedWith( + 'revert No aggregator found', + ); + }); + + it('cannot transfer if max to spend too low', async function () { + requestInfo._maxToSpend = conversionToPay.result + .add(conversionFees.result) + .sub(1) + .toString(); + await expect(batchConvFunction(argTemplate([requestInfo]), feeAddress)).to.be.revertedWith( + 'Amount to pay is over the user limit', + ); + }); + + it('cannot transfer if rate is too old', async function () { + requestInfo._maxRateTimespan = 10; + + await expect(batchConvFunction(argTemplate([requestInfo]), feeAddress)).to.be.revertedWith( + 'aggregator rate is outdated', + ); + }); + }); + }; + testSuite('batchRouter'); + testSuite('batchERC20ConversionPaymentsMultiTokensEasy'); +}); From 5f7fc4f8c272d1d289d3119bdeafbd292337856f Mon Sep 17 00:00:00 2001 From: olivier7delf <55892112+olivier7delf@users.noreply.github.com> Date: Wed, 13 Jul 2022 16:43:42 +0200 Subject: [PATCH 002/105] tests batchPayments functions --- .../BatchERC20ConversionPayments.test.ts | 246 ++++++++++++------ 1 file changed, 171 insertions(+), 75 deletions(-) diff --git a/packages/smart-contracts/test/contracts/BatchERC20ConversionPayments.test.ts b/packages/smart-contracts/test/contracts/BatchERC20ConversionPayments.test.ts index d1cccc109..ed4e1738d 100644 --- a/packages/smart-contracts/test/contracts/BatchERC20ConversionPayments.test.ts +++ b/packages/smart-contracts/test/contracts/BatchERC20ConversionPayments.test.ts @@ -26,12 +26,11 @@ describe('contract: BatchErc20ConversionPayments', () => { let batchAddress: string; let signer: Signer; const basicFee = 10; - const batchFee = 30; + const batchFee = 100; const batchConvFee = 100; const amountInFiat = '100000000'; // 1 with 8 decimal const feesAmountInFiat = '100000'; // 0.001 with 8 decimal const thousandWith18Decimal = '1000000000000000000000'; - // const hundredWith18Decimal = '100000000000000000000'; const referenceExample = '0xaaaa'; const currencyManager = CurrencyManager.getDefault(); @@ -40,7 +39,6 @@ describe('contract: BatchErc20ConversionPayments', () => { const USD_hash = currencyManager.fromSymbol('USD')!.hash; const EUR_hash = currencyManager.fromSymbol('EUR')!.hash; let DAI_address: string; - let USDT_address: string; let fakeFAU_address: string; let testErc20ConversionProxy: Erc20ConversionProxy; @@ -84,7 +82,6 @@ describe('contract: BatchErc20ConversionPayments', () => { }; let requestInfo: RequestInfo; - // type let requestsInfoParent1 = { _tokenAddresses: [], _recipients: [], @@ -92,9 +89,14 @@ describe('contract: BatchErc20ConversionPayments', () => { _paymentReferences: [], _feeAmounts: [], }; + /** Function used to emit events of batch conversion proxy */ let emitOneTx: Function; - + /** + * Function batch conversion, it can be the batchRouter function, used with conversion args, + * or directly batchERC20ConversionPaymentsMultiTokensEasy + * */ let batchConvFunction: (args: any, feeAddress: string) => Promise; + /** Format arguments so they can be used by batchConvFunction */ let argTemplate: Function; before(async () => { @@ -126,34 +128,7 @@ describe('contract: BatchErc20ConversionPayments', () => { fakeFAU_address = '0x7153CCD1a20Bbb2f6dc89c1024de368326EC6b4F'; testERC20b = new TestERC20__factory(signer).attach(fakeFAU_address); - USDT_address = '0xF328c11c4dF88d18FcBd30ad38d8B4714F4b33bF'; // '0xF328c11c4dF88d18FcBd30ad38d8B4714F4b33bF'; batchAddress = testBatchConversionProxy.address; - emitOneTx = ( - result: Chai.Assertion, - requestInfo: RequestInfo, - _conversionToPay = conversionToPay, - _conversionFees = conversionFees, - _testErc20ConversionProxy = testErc20ConversionProxy, - ) => { - return result.to - .emit(testErc20ConversionProxy, 'TransferWithConversionAndReference') - .withArgs( - requestInfo._requestAmount, - ethers.utils.getAddress(requestInfo._path[0]), - ethers.utils.keccak256(referenceExample), - requestInfo._feeAmount, - '0', - ) - .to.emit(testErc20ConversionProxy, 'TransferWithReferenceAndFee') - .withArgs( - ethers.utils.getAddress(DAI_address), - ethers.utils.getAddress(requestInfo._recipient), - _conversionToPay.result, - ethers.utils.keccak256(referenceExample), - _conversionFees.result, - feeAddress, - ); - }; }); const batchFeeToPay = (conversionAmountToPay: BigNumber) => { @@ -180,29 +155,30 @@ describe('contract: BatchErc20ConversionPayments', () => { _maxRateTimespan: _maxRateTimespan, }; }; + beforeEach(async () => { fromDiffBalanceExpected = BigNumber.from(0); toDiffBalanceExpected = BigNumber.from(0); feeDiffBalanceExpected = BigNumber.from(0); - path = [USD_hash, DAI_address]; - initConvToPayAndRequestInfo(to, path, amountInFiat, feesAmountInFiat, 0, chainlinkPath); await testERC20.approve(testBatchConversionProxy.address, thousandWith18Decimal, { from, }); await testERC20b.approve(testBatchConversionProxy.address, thousandWith18Decimal, { from, }); - fromOldBalance = await testERC20.connect(signer).balanceOf(from); + fromOldBalance = await testERC20.balanceOf(from); toOldBalance = await testERC20.balanceOf(to); feeOldBalance = await testERC20.balanceOf(feeAddress); batchOldBalance = await testERC20.balanceOf(batchAddress); }); + afterEach(async () => { fromBalance = await testERC20.balanceOf(from); toBalance = await testERC20.balanceOf(to); feeBalance = await testERC20.balanceOf(feeAddress); batchBalance = await testERC20.balanceOf(batchAddress); - + // console.log('fromOldBalance', fromOldBalance.toString()); + // console.log('fromBalance', fromBalance.toString()); const fromDiffBalance = BigNumber.from(fromBalance.toString()) .sub(fromOldBalance.toString()) .toString(); @@ -226,24 +202,28 @@ describe('contract: BatchErc20ConversionPayments', () => { expect(batchDiffBalance).to.equals('0', 'batchDiffBalance'); }); + /** Function used to calcul the expected new balance for batch conversion. + * It can also be used for batch IF batchFee == batchConvFee + * The 3rd arg is used to calcul batch fees + */ const calculBalances = ( - _conversionToPay: ConvToPay, - _conversionFees: ConvToPay, - _conversionsToPay: ConvToPay[], + _conversionToPay_result: BigNumber, + _conversionFees_result: BigNumber, + _conversionsToPay_results: BigNumber[], ) => { fromDiffBalanceExpected = fromDiffBalanceExpected - .add(_conversionToPay.result) - .add(_conversionFees.result); + .add(_conversionToPay_result) + .add(_conversionFees_result); - toDiffBalanceExpected = toDiffBalanceExpected.add(_conversionToPay.result); - feeDiffBalanceExpected = feeDiffBalanceExpected.add(_conversionFees.result); - if (_conversionsToPay.length > 0) calculBatchFeeBalances(_conversionsToPay); + toDiffBalanceExpected = toDiffBalanceExpected.add(_conversionToPay_result); + feeDiffBalanceExpected = feeDiffBalanceExpected.add(_conversionFees_result); + if (_conversionsToPay_results.length > 0) calculBatchFeeBalances(_conversionsToPay_results); }; - const calculBatchFeeBalances = (_conversionsToPay: ConvToPay[]) => { + const calculBatchFeeBalances = (_conversionsToPay_results: BigNumber[]) => { let sumToPay = BigNumber.from(0); - for (let i = 0; i < _conversionsToPay.length; i++) { - sumToPay = sumToPay.add(_conversionsToPay[i].result); + for (let i = 0; i < _conversionsToPay_results.length; i++) { + sumToPay = sumToPay.add(_conversionsToPay_results[i]); } fromDiffBalanceExpected = fromDiffBalanceExpected.add(batchFeeToPay(sumToPay)); feeDiffBalanceExpected = feeDiffBalanceExpected.add(batchFeeToPay(sumToPay)); @@ -262,7 +242,7 @@ describe('contract: BatchErc20ConversionPayments', () => { await emitOneTx(expect(result), requestInfo, conversionToPay, conversionFees); } - calculBalances(conversionToPay, conversionFees, [conversionToPay]); + calculBalances(conversionToPay.result, conversionFees.result, [conversionToPay.result]); }; const twoTransferOneTokenConv = async (path2: string[], nTimes: number, logGas = false) => { @@ -299,44 +279,82 @@ describe('contract: BatchErc20ConversionPayments', () => { requestInfo2._path[requestInfo2._path.length - 1] ) { for (let i = 0; i < nTimes - 1; i++) { - calculBalances(conversionToPay, conversionFees, []); - calculBalances(conversionToPay2, conversionFees2, []); + calculBalances(conversionToPay.result, conversionFees.result, []); + calculBalances(conversionToPay2.result, conversionFees2.result, []); } - calculBalances(conversionToPay, conversionFees, []); - calculBalances(conversionToPay2, conversionFees2, conversionsToPay); + calculBalances(conversionToPay.result, conversionFees.result, []); + calculBalances( + conversionToPay2.result, + conversionFees2.result, + conversionsToPay.map((ctp) => ctp.result), + ); } else { for (let i = 0; i < nTimes - 1; i++) { - calculBalances(conversionToPay, conversionFees, []); + calculBalances(conversionToPay.result, conversionFees.result, []); } const conversionsToPayBis = conversionsToPay.filter((_, i) => i % 2 === 0); - calculBalances(conversionToPay, conversionFees, conversionsToPayBis); + calculBalances( + conversionToPay.result, + conversionFees.result, + conversionsToPayBis.map((ctp) => ctp.result), + ); } }; const testSuite = (suiteName: string) => { - describe(suiteName, () => { - before(() => { - if (suiteName === 'batchRouter') { - batchConvFunction = testBatchConversionProxy.batchRouter; - argTemplate = (requestInfos: RequestInfo[]) => { - return [ - { - paymentNetworkId: '0', - requestsInfo: requestInfos, - requestsInfoParent: requestsInfoParent1, - }, - ]; - }; - } - if (suiteName === 'batchERC20ConversionPaymentsMultiTokensEasy') { - batchConvFunction = testBatchConversionProxy.batchERC20ConversionPaymentsMultiTokensEasy; - argTemplate = (requestInfos: RequestInfo[]) => { - return requestInfos; - }; - } - }); + emitOneTx = ( + result: Chai.Assertion, + requestInfo: RequestInfo, + _conversionToPay = conversionToPay, + _conversionFees = conversionFees, + _testErc20ConversionProxy = testErc20ConversionProxy, + ) => { + return result.to + .emit(testErc20ConversionProxy, 'TransferWithConversionAndReference') + .withArgs( + requestInfo._requestAmount, + ethers.utils.getAddress(requestInfo._path[0]), + ethers.utils.keccak256(referenceExample), + requestInfo._feeAmount, + '0', + ) + .to.emit(testErc20ConversionProxy, 'TransferWithReferenceAndFee') + .withArgs( + ethers.utils.getAddress(DAI_address), + ethers.utils.getAddress(requestInfo._recipient), + _conversionToPay.result, + ethers.utils.keccak256(referenceExample), + _conversionFees.result, + feeAddress, + ); + }; + beforeEach(async () => { + path = [USD_hash, DAI_address]; + initConvToPayAndRequestInfo(to, path, amountInFiat, feesAmountInFiat, 0, chainlinkPath); + }); + before(() => { + if (suiteName === 'batchRouter') { + batchConvFunction = testBatchConversionProxy.batchRouter; + argTemplate = (requestInfos: RequestInfo[]) => { + return [ + { + paymentNetworkId: '0', + requestsInfo: requestInfos, + requestsInfoParent: requestsInfoParent1, + }, + ]; + }; + } + if (suiteName === 'batchERC20ConversionPaymentsMultiTokensEasy') { + batchConvFunction = testBatchConversionProxy.batchERC20ConversionPaymentsMultiTokensEasy; + argTemplate = (requestInfos: RequestInfo[]) => { + return requestInfos; + }; + } + }); + describe(suiteName, () => { describe('batchERC20ConversionPaymentsMultiTokensEasy with DAI', async () => { it('allows to transfer DAI tokens for USD payment', async () => { await transferOneTokenConv(path); @@ -391,6 +409,84 @@ describe('contract: BatchErc20ConversionPayments', () => { }); }); }; + + /** Make sure the existing ERC20 functions from the parent contract BatchPaymentPublic.sol are still working */ + describe('Test BatchErc20Payments functions', () => { + const batchERC20Payments = async (isBatchRouter: boolean, subFunction: string) => { + const amount = 200; + const feeAmount = 20; + let batchFunction: Function; + const tokenAddress = testERC20.address; + let result; + if (isBatchRouter) { + batchFunction = testBatchConversionProxy.batchRouter; + result = batchFunction( + [ + { + paymentNetworkId: subFunction === 'batchERC20PaymentsWithReference' ? 1 : 2, + requestsInfo: [], + requestsInfoParent: { + _tokenAddresses: [tokenAddress], + _recipients: [to], + _amounts: [amount], + _paymentReferences: [referenceExample], + _feeAmounts: [feeAmount], + }, + }, + ], + feeAddress, + ); + } else { + batchFunction = + subFunction === 'batchERC20PaymentsWithReference' + ? testBatchConversionProxy.batchERC20PaymentsWithReference + : testBatchConversionProxy.batchERC20PaymentsMultiTokensWithReference; + result = batchFunction( + subFunction === 'batchERC20PaymentsWithReference' ? tokenAddress : [tokenAddress], + [to], + [amount], + [referenceExample], + [feeAmount], + feeAddress, + ); + } + await expect(result) + .to.emit(testERC20, 'Transfer') + .withArgs(from, batchAddress, amount + feeAmount) + .to.emit(erc20FeeProxy, 'TransferWithReferenceAndFee') + .withArgs( + testERC20.address, + to, + amount, + ethers.utils.keccak256(referenceExample), + feeAmount, + feeAddress, + ) + // batch fee amount from the spender to feeAddress + .to.emit(testERC20, 'Transfer') + .withArgs( + from, + feeAddress, + amount * (batchFee / 10_000), // batch fee amount = 200 * 1% + ); + + calculBalances(BigNumber.from(amount), BigNumber.from(feeAmount), [BigNumber.from(amount)]); + }; + it('batchERC20PaymentsWithReference transfers token', async function () { + await batchERC20Payments(false, 'batchERC20PaymentsWithReference'); + }); + it('with batchRouter, batchERC20PaymentsMultiTokensWithReference transfers token', async function () { + await batchERC20Payments(true, 'batchERC20PaymentsWithReference'); + }); + + it('batchERC20PaymentsMultiTokensWithReference transfers token', async function () { + await batchERC20Payments(false, 'batchERC20PaymentsMultiTokensWithReference'); + }); + it('with batchRouter, batchERC20PaymentsMultiTokensWithReference transfers token', async function () { + await batchERC20Payments(true, 'batchERC20PaymentsMultiTokensWithReference'); + }); + }); + testSuite('batchRouter'); testSuite('batchERC20ConversionPaymentsMultiTokensEasy'); }); From eb1c4d8025f6587eb868149344b16677cda3fcda Mon Sep 17 00:00:00 2001 From: olivier7delf <55892112+olivier7delf@users.noreply.github.com> Date: Fri, 15 Jul 2022 11:18:51 +0200 Subject: [PATCH 003/105] clear batch contract --- .../BatchERC20ConversionPayments.sol | 22 +------------------ 1 file changed, 1 insertion(+), 21 deletions(-) diff --git a/packages/smart-contracts/src/contracts/BatchERC20ConversionPayments.sol b/packages/smart-contracts/src/contracts/BatchERC20ConversionPayments.sol index 63c1ca40b..9f78ffc60 100644 --- a/packages/smart-contracts/src/contracts/BatchERC20ConversionPayments.sol +++ b/packages/smart-contracts/src/contracts/BatchERC20ConversionPayments.sol @@ -61,17 +61,6 @@ contract BatchConversionPayments is BatchPaymentsPublic { RequestsInfoParent requestsInfoParent; } - // Summarize informations for each path - struct Path { - address[] path; - uint256 amountToPay; - uint256 amountToPayInFees; - uint256 maxRateTimespan; - uint256 rate; - uint256 decimals; - uint256 oldestTimestampRate; - } - /** * @param _paymentErc20FeeProxy The address to the ERC20 payment proxy to use. * @param _paymentEthFeeProxy The address to the Ethereum payment proxy to use. @@ -227,7 +216,7 @@ contract BatchConversionPayments is BatchPaymentsPublic { ); } - // batch send back to the payer the tokens not spent + // batch send back to the payer the tokens not spent and pay the batch for (uint256 k = 0; k < uTokens.length && uTokens[k].amountAndFee > 0; k++) { IERC20 requestedToken = IERC20(uTokens[k].tokenAddress); @@ -237,15 +226,6 @@ contract BatchConversionPayments is BatchPaymentsPublic { requestedToken.safeTransfer(msg.sender, excessAmount); } - // uint256 amountAndFeePaid = ; - // // amount * (1+.001) = realAmountAndFee - // // amount = realAmountAndFee / 1.001 - // // reduce the usual .1% fee. precision at 1 wei because of solidity rounding. - // uint256 amountPaid = ; - // // --13199869437493199 - // // +-13188118811881187 - // uint256 batchFeeAmount = ; - // Payer pays batch fee amount require( safeTransferFrom( From db22abcbea33c78081be61f3fcfa2b451417382f Mon Sep 17 00:00:00 2001 From: olivier7delf <55892112+olivier7delf@users.noreply.github.com> Date: Mon, 25 Jul 2022 11:51:21 +0200 Subject: [PATCH 004/105] add comments in smart contract --- .../src/contracts/BatchERC20ConversionPayments.sol | 8 +++++--- .../test/contracts/BatchERC20ConversionPayments.test.ts | 2 -- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/smart-contracts/src/contracts/BatchERC20ConversionPayments.sol b/packages/smart-contracts/src/contracts/BatchERC20ConversionPayments.sol index 9f78ffc60..03e23f7b6 100644 --- a/packages/smart-contracts/src/contracts/BatchERC20ConversionPayments.sol +++ b/packages/smart-contracts/src/contracts/BatchERC20ConversionPayments.sol @@ -15,9 +15,11 @@ import './BatchPaymentsPublic.sol'; /** * @title BatchConversionPayments * @notice This contract makes multiple conversion payments with references, in one transaction: - * - on: ERC20 Payment Proxy of the Request Network protocol + * - on: + * - ERC20 tokens: using Erc20ConversionProxy and ERC20FeeProxy + * - Native token: as Eth, using EthConversionProxy and EthereumFeeProxy * - to: multiple addresses - * - fees: ERC20 proxy fees and additional batch conversion fee are paid to the same address. + * - fees: conversion proxy fees and additional batch conversion fee are paid to the same address. * If one transaction of the batch fail, every transactions are reverted. * @dev Please notify than fees are now divided by 10_000 instead of 1_000 in previous version */ @@ -216,7 +218,7 @@ contract BatchConversionPayments is BatchPaymentsPublic { ); } - // batch send back to the payer the tokens not spent and pay the batch + // batch send back to the payer the tokens not spent and pay the batch fee for (uint256 k = 0; k < uTokens.length && uTokens[k].amountAndFee > 0; k++) { IERC20 requestedToken = IERC20(uTokens[k].tokenAddress); diff --git a/packages/smart-contracts/test/contracts/BatchERC20ConversionPayments.test.ts b/packages/smart-contracts/test/contracts/BatchERC20ConversionPayments.test.ts index ed4e1738d..cbe0946dc 100644 --- a/packages/smart-contracts/test/contracts/BatchERC20ConversionPayments.test.ts +++ b/packages/smart-contracts/test/contracts/BatchERC20ConversionPayments.test.ts @@ -177,8 +177,6 @@ describe('contract: BatchErc20ConversionPayments', () => { toBalance = await testERC20.balanceOf(to); feeBalance = await testERC20.balanceOf(feeAddress); batchBalance = await testERC20.balanceOf(batchAddress); - // console.log('fromOldBalance', fromOldBalance.toString()); - // console.log('fromBalance', fromBalance.toString()); const fromDiffBalance = BigNumber.from(fromBalance.toString()) .sub(fromOldBalance.toString()) .toString(); From 1a3e6b82c64e349da0e3f7ef2dd736860e1f0f19 Mon Sep 17 00:00:00 2001 From: olivier7delf <55892112+olivier7delf@users.noreply.github.com> Date: Mon, 25 Jul 2022 14:48:06 +0200 Subject: [PATCH 005/105] add batchEthConversionPaymentsWithReference add atchEth tested and contracts cleaned --- packages/smart-contracts/package.json | 2 +- ...test-deploy-batch-conversion-deployment.ts | 2 + ...yments.sol => BatchConversionPayments.sol} | 231 +++++++++++++----- .../src/contracts/BatchPaymentsPublic.sol | 12 +- .../interfaces/IEthConversionProxy.sol | 50 ++++ .../BatchConversionPayments/0.1.0.json | 90 +++++-- ...est.ts => BatchConversionPayments.test.ts} | 209 ++++++++++++++-- 7 files changed, 496 insertions(+), 100 deletions(-) rename packages/smart-contracts/src/contracts/{BatchERC20ConversionPayments.sol => BatchConversionPayments.sol} (55%) create mode 100644 packages/smart-contracts/src/contracts/interfaces/IEthConversionProxy.sol rename packages/smart-contracts/test/contracts/{BatchERC20ConversionPayments.test.ts => BatchConversionPayments.test.ts} (69%) diff --git a/packages/smart-contracts/package.json b/packages/smart-contracts/package.json index 7fc6c9af3..73e9b1343 100644 --- a/packages/smart-contracts/package.json +++ b/packages/smart-contracts/package.json @@ -51,7 +51,7 @@ "deploy": "yarn hardhat deploy-local-env --network private", "test": "yarn hardhat test --network private", "test:lib": "yarn jest test/lib", - "testp": "yarn test test/contracts/BatchERC20ConversionPayments.test.ts", + "testp": "yarn test test/contracts/BatchConversionPayments.test.ts", "testcp": "yarn hardhat compile && yarn testp", "redeploy": "yarn clean && yarn build && yarn deploy" }, diff --git a/packages/smart-contracts/scripts/test-deploy-batch-conversion-deployment.ts b/packages/smart-contracts/scripts/test-deploy-batch-conversion-deployment.ts index 12015a5cd..28080f02e 100644 --- a/packages/smart-contracts/scripts/test-deploy-batch-conversion-deployment.ts +++ b/packages/smart-contracts/scripts/test-deploy-batch-conversion-deployment.ts @@ -17,6 +17,7 @@ export async function deployBatchConversionPayment( const _EthereumFeeProxyAddress = '0x3d49d1eF2adE060a33c6E6Aa213513A7EE9a6241'; const _chainlinkConversionPath = '0x4e71920b7330515faf5EA0c690f1aD06a85fB60c'; const _paymentErc20ConversionFeeProxy = '0xdE5491f774F0Cb009ABcEA7326342E105dbb1B2E'; + const _paymentEthConversionFeeProxy = '0x98d9f9e8DEbd4A632682ba207670d2a5ACD3c489'; // Deploy BatchConversionPayments contract const { address: BatchConversionPaymentsAddress } = await deployOne( @@ -28,6 +29,7 @@ export async function deployBatchConversionPayment( _ERC20FeeProxyAddress, _EthereumFeeProxyAddress, _paymentErc20ConversionFeeProxy, + _paymentEthConversionFeeProxy, _chainlinkConversionPath, await (await hre.ethers.getSigners())[0].getAddress(), ], diff --git a/packages/smart-contracts/src/contracts/BatchERC20ConversionPayments.sol b/packages/smart-contracts/src/contracts/BatchConversionPayments.sol similarity index 55% rename from packages/smart-contracts/src/contracts/BatchERC20ConversionPayments.sol rename to packages/smart-contracts/src/contracts/BatchConversionPayments.sol index 03e23f7b6..f71f67d24 100644 --- a/packages/smart-contracts/src/contracts/BatchERC20ConversionPayments.sol +++ b/packages/smart-contracts/src/contracts/BatchConversionPayments.sol @@ -1,40 +1,46 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.4; -import '@openzeppelin/contracts/token/ERC20/IERC20.sol'; -import './lib/SafeERC20.sol'; -import '@openzeppelin/contracts/access/Ownable.sol'; -import './interfaces/ERC20FeeProxy.sol'; -import './interfaces/EthereumFeeProxy.sol'; -import '@openzeppelin/contracts/security/ReentrancyGuard.sol'; import './interfaces/IERC20ConversionProxy.sol'; -import '@openzeppelin/contracts/token/ERC20/IERC20.sol'; +import './interfaces/IEthConversionProxy.sol'; import './ChainlinkConversionPath.sol'; import './BatchPaymentsPublic.sol'; /** * @title BatchConversionPayments * @notice This contract makes multiple conversion payments with references, in one transaction: - * - on: + * - on: * - ERC20 tokens: using Erc20ConversionProxy and ERC20FeeProxy * - Native token: as Eth, using EthConversionProxy and EthereumFeeProxy * - to: multiple addresses * - fees: conversion proxy fees and additional batch conversion fee are paid to the same address. + * batchRouter is the main function to batch every kind of payment at once. * If one transaction of the batch fail, every transactions are reverted. * @dev Please notify than fees are now divided by 10_000 instead of 1_000 in previous version + * batchRouter is the main function, but others batch payment functions are "public" in order to do + * gas optimization in some cases. */ contract BatchConversionPayments is BatchPaymentsPublic { using SafeERC20 for IERC20; IERC20ConversionProxy conversionPaymentProxy; + IEthConversionProxy conversionPaymentEthProxy; ChainlinkConversionPath public chainlinkConversionPath; - // @dev: Between 0 and 10000, i.e: batchFee = 100 represent 1% of fee + // Between 0 and 10000, i.e: batchFee = 100 represent 1% of fee uint256 public batchConversionFee; uint256 public basicFee; /** - Every information of a request, excepted the feeAddress + * @dev Every informations of a request, excepted the feeAddress + * _recipient Recipients address of the payement + * _requestAmount Request amount in fiat + * _path Conversion path + * _paymentReference References of the payment related + * _feeAmount The amount in fiat of the payment fee + * _maxToSpend Amounts max in token that we can spend on the behalf of the user: + * it includes fee proxy but NOT the batchConversionFee + * _maxRateTimespan Max times span with the oldestrate, ignored if zero */ struct RequestInfo { address _recipient; @@ -47,7 +53,7 @@ contract BatchConversionPayments is BatchPaymentsPublic { } /** - * It is the structure of the input for the function from contract BatchPaymentsPublic + * @dev It is the structure of the input for the function from contract BatchPaymentsPublic */ struct RequestsInfoParent { address[] _tokenAddresses; @@ -57,16 +63,31 @@ contract BatchConversionPayments is BatchPaymentsPublic { uint256[] _feeAmounts; } + /** + * @dev Used by batchRouter to hold information for any kind of request. + * - paymentNetworkId requests are group by paymentType to be paid with the appropriate function. + * More details in batchRouter description. + * - requestsInfo all informations required for conversion requests to be paid (=> paymentNetworkId equal 0 or 3) + * - requestsInfoParent all informations required for None-conversion requests to be paid + * (=> paymentNetworkId equal 1, 2, or 4) + */ struct MetaRequestsInfo { uint256 paymentNetworkId; RequestInfo[] requestsInfo; RequestsInfoParent requestsInfoParent; } + struct Path { + address[] _path; + uint256 rate; + uint256 decimals; + } + /** * @param _paymentErc20FeeProxy The address to the ERC20 payment proxy to use. * @param _paymentEthFeeProxy The address to the Ethereum payment proxy to use. * @param _paymentErc20ConversionFeeProxy The address of the ERC20 Conversion payment proxy to use. + * @param _paymentEthConversionFeeProxy The address of the ETH Conversion payment proxy to use. * @param _chainlinkConversionPathAddress The address of the conversion path contract * @param _owner Owner of the contract. */ @@ -74,6 +95,7 @@ contract BatchConversionPayments is BatchPaymentsPublic { address _paymentErc20FeeProxy, address _paymentEthFeeProxy, address _paymentErc20ConversionFeeProxy, + address _paymentEthConversionFeeProxy, address _chainlinkConversionPathAddress, address _owner ) BatchPaymentsPublic(_paymentErc20FeeProxy, _paymentEthFeeProxy, _owner) { @@ -81,6 +103,7 @@ contract BatchConversionPayments is BatchPaymentsPublic { paymentEthFeeProxy = IEthereumFeeProxy(_paymentEthFeeProxy); conversionPaymentProxy = IERC20ConversionProxy(_paymentErc20ConversionFeeProxy); + conversionPaymentEthProxy = IEthConversionProxy(_paymentEthConversionFeeProxy); chainlinkConversionPath = ChainlinkConversionPath(_chainlinkConversionPathAddress); transferOwnership(_owner); @@ -90,25 +113,26 @@ contract BatchConversionPayments is BatchPaymentsPublic { } /** - * Batch payments on different payment network in the same time + * @notice Batch payments on different payment networks at once. * - batchERC20ConversionPaymentsMultiTokens, paymentNetworks: 0 * - batchERC20PaymentsWithReference, paymentNetworks: 1 * - batchERC20PaymentsMultiTokensWithReference, paymentNetworks: 2 - * - batchEthPaymentsWithReference, paymentNetworks: 3 + * - batchEthConversionPaymentsWithReference, paymentNetworks: 3 + * - batchEthPaymentsWithReference, paymentNetworks: 4 * @param metaRequestsInfos contains paymentNetworkId and requestsInfo - * - paymentNetworkId requests are group by paymentType to be paid with the appropriate function - * - requestsInfo all information required for conversion requests to be paid - * - requestsInfoParent all information required for None-conversion requests to be paid * @param _feeAddress The address of the proxy to send the fees + * @dev batchRouter reduces gas consumption if you are using more than a single payment networks, + * else, it is more efficient to use the adapted batch function. */ function batchRouter(MetaRequestsInfo[] calldata metaRequestsInfos, address _feeAddress) external + payable { require(metaRequestsInfos.length < 4, 'more than 4 requestsinfo'); for (uint256 i = 0; i < metaRequestsInfos.length; i++) { MetaRequestsInfo calldata metaRequestsInfo = metaRequestsInfos[i]; if (metaRequestsInfo.paymentNetworkId == 0) { - batchERC20ConversionPaymentsMultiTokensEasy(metaRequestsInfo.requestsInfo, _feeAddress); + batchERC20ConversionPaymentsMultiTokens(metaRequestsInfo.requestsInfo, _feeAddress); } else if (metaRequestsInfo.paymentNetworkId == 1) { batchERC20PaymentsWithReference( metaRequestsInfo.requestsInfoParent._tokenAddresses[0], @@ -128,6 +152,11 @@ contract BatchConversionPayments is BatchPaymentsPublic { _feeAddress ); } else if (metaRequestsInfo.paymentNetworkId == 3) { + batchEthConversionPaymentsWithReference( + metaRequestsInfo.requestsInfo, + payable(_feeAddress) + ); + } else if (metaRequestsInfo.paymentNetworkId == 4) { batchEthPaymentsWithReference( metaRequestsInfo.requestsInfoParent._recipients, metaRequestsInfo.requestsInfoParent._amounts, @@ -142,41 +171,37 @@ contract BatchConversionPayments is BatchPaymentsPublic { } /** - * @notice Transfers a batch of ERC20 tokens with a reference with amount based on the request amount in fiat - * @param requestsInfo containing every information of a request - * _recipient Transfer recipients of the payement - * _requestAmount Request amounts - * _path Conversion paths - * _paymentReference References of the payment related - * _feeAmount The amounts of the payment fee - * _maxToSpend Amounts max that we can spend on the behalf of the user: it includes fee proxy but NOT the batchCoversionFee - * _maxRateTimespan Max times span with the oldestrate, ignored if zero + * @notice Transfers a batch of multiple ERC20 tokens with a reference with amount based on the request amount in fiat + * @param requestsInfo list of requestInfo, each one containing every informations of a request. * @param _feeAddress The fee recipient */ - function batchERC20ConversionPaymentsMultiTokensEasy( + function batchERC20ConversionPaymentsMultiTokens( RequestInfo[] calldata requestsInfo, address _feeAddress ) public { + // Aggregate _maxToSpend by token Token[] memory uTokens = new Token[](requestsInfo.length); - for (uint256 j = 0; j < requestsInfo.length; j++) { + for (uint256 i = 0; i < requestsInfo.length; i++) { for (uint256 k = 0; k < requestsInfo.length; k++) { // If the token is already in the existing uTokens list - if (uTokens[k].tokenAddress == requestsInfo[j]._path[requestsInfo[j]._path.length - 1]) { - uTokens[k].amountAndFee += requestsInfo[j]._maxToSpend; + if (uTokens[k].tokenAddress == requestsInfo[i]._path[requestsInfo[i]._path.length - 1]) { + uTokens[k].amountAndFee += requestsInfo[i]._maxToSpend; break; } - // If the token is not in the list (amountAndFee = 0), and amount + fee > 0 - if (uTokens[k].amountAndFee == 0 && (requestsInfo[j]._maxToSpend) > 0) { - uTokens[k].tokenAddress = requestsInfo[j]._path[requestsInfo[j]._path.length - 1]; + // If the token is not in the list (amountAndFee = 0) + else if (uTokens[k].amountAndFee == 0 && (requestsInfo[i]._maxToSpend) > 0) { + uTokens[k].tokenAddress = requestsInfo[i]._path[requestsInfo[i]._path.length - 1]; // amountAndFee is used to store _maxToSpend, useful to send enough tokens to this contract - uTokens[k].amountAndFee = requestsInfo[j]._maxToSpend; + uTokens[k].amountAndFee = requestsInfo[i]._maxToSpend; break; } } } + IERC20 requestedToken; + // For each token: check allowance, transfer funds on the contract and approve the paymentProxy to spend if needed for (uint256 k = 0; k < uTokens.length && uTokens[k].amountAndFee > 0; k++) { - IERC20 requestedToken = IERC20(uTokens[k].tokenAddress); + requestedToken = IERC20(uTokens[k].tokenAddress); uTokens[k].batchFeeAmount = (uTokens[k].amountAndFee * batchConversionFee) / 10000; // Check proxy's allowance from user, and user's funds to pay approximated amounts. require( @@ -203,7 +228,8 @@ contract BatchConversionPayments is BatchPaymentsPublic { approveConversionPaymentProxyToSpend(uTokens[k].tokenAddress); } } - // Batch Conversion contract pays the requests using Erc20ConversionFeeProxy + + // Batch pays the requests using Erc20ConversionFeeProxy for (uint256 i = 0; i < requestsInfo.length; i++) { RequestInfo memory rI = requestsInfo[i]; conversionPaymentProxy.transferFromWithReferenceAndFee( @@ -218,17 +244,18 @@ contract BatchConversionPayments is BatchPaymentsPublic { ); } - // batch send back to the payer the tokens not spent and pay the batch fee + // Batch sends back to the payer the tokens not spent and pays the batch fee for (uint256 k = 0; k < uTokens.length && uTokens[k].amountAndFee > 0; k++) { - IERC20 requestedToken = IERC20(uTokens[k].tokenAddress); + requestedToken = IERC20(uTokens[k].tokenAddress); - // excessAmount = maxToSpend - reallySpent + // Batch sends back to the payer the tokens not spent = excessAmount + // excessAmount = maxToSpend - reallySpent, which is equal to the remaining tokens on the contract uint256 excessAmount = requestedToken.balanceOf(address(this)); if (excessAmount > 0) { requestedToken.safeTransfer(msg.sender, excessAmount); } - // Payer pays batch fee amount + // Payer pays batch fees amount require( safeTransferFrom( uTokens[k].tokenAddress, @@ -242,16 +269,112 @@ contract BatchConversionPayments is BatchPaymentsPublic { } /** - * Function to get fresh data from chainlinkConversionPath to do conversion. + * @notice Send a batch of Eth conversion payments w/fees with paymentReferences to multiple accounts. + * If one payment failed, the whole batch is reverted. + * @param requestsInfo List of requestInfos, each one containing every informations of a request. + * _maxToSpend is not used in this function. + * @param _feeAddress The fee recipient. + * @dev It uses EthereumConversionProxy to pay an invoice and fees. + */ + function batchEthConversionPaymentsWithReference( + RequestInfo[] calldata requestsInfo, + address payable _feeAddress + ) public payable { + uint256 contractBalance = address(this).balance; + // amountAndFeeToPay in native token (as ETH), is updated at each payment + uint256 amountAndFeeToPay; + + // rPaths stores _path, rate, and decimals only once by path + Path[] memory rPaths = new Path[](requestsInfo.length); + for (uint256 i = 0; i < requestsInfo.length; i++) { + RequestInfo memory rI = requestsInfo[i]; + for (uint64 k = 0; k < requestsInfo.length; k++) { + // Check if the path is already known + if (rPaths[k].rate > 0 && rPaths[k]._path[0] == rI._path[0]) { + // use the already known rate and decimals from path already queried + amountAndFeeToPay = amountAndFeeConversion( + rI._requestAmount, + rI._feeAmount, + rPaths[k].rate, + rPaths[k].decimals + ); + break; + } else if (i == k) { + // set the path, and get the associated rate and decimals + rPaths[i]._path = rI._path; + (rPaths[i].rate, rPaths[i].decimals) = getRate(rI._path, rI._maxRateTimespan); + amountAndFeeToPay = amountAndFeeConversion( + rI._requestAmount, + rI._feeAmount, + rPaths[i].rate, + rPaths[i].decimals + ); + break; + } + } + + require(address(this).balance >= amountAndFeeToPay, 'not enough funds'); + + // Batch contract pays the requests through EthConversionProxy + conversionPaymentEthProxy.transferWithReferenceAndFee{value: amountAndFeeToPay}( + payable(rI._recipient), + rI._requestAmount, + rI._path, + rI._paymentReference, + rI._feeAmount, + _feeAddress, + rI._maxRateTimespan + ); + } + + // Check that batch contract has enough funds to pay batch conversion fees + uint256 amountBatchFees = ((((contractBalance - address(this).balance) * 10000) / + (10000 + basicFee)) * batchConversionFee) / 10000; + require(address(this).balance >= amountBatchFees, 'not enough funds for batch conversion fees'); + + // Batch contract pays batch fee + _feeAddress.transfer(amountBatchFees); + + // Batch contract transfers the remaining ethers to the payer + if (address(this).balance > 0) { + (bool sendBackSuccess, ) = payable(msg.sender).call{value: address(this).balance}(''); + require(sendBackSuccess, 'Could not send remaining funds to the payer'); + } + } + + /* + * Helper functions + */ + + /** + * @notice Calculate the amount of the conversion + */ + function amountAndFeeConversion( + uint256 requestAmount, + uint256 requestFee, + uint256 rate, + uint256 decimals + ) private pure returns (uint256) { + return (requestAmount * rate) / decimals + (requestFee * rate) / decimals; + } + + /** + * @notice Authorizes the conveersion proxy to spend a new request currency (ERC20). + * @param _erc20Address Address of an ERC20 used as the request currency. + */ + function approveConversionPaymentProxyToSpend(address _erc20Address) public { + IERC20 erc20 = IERC20(_erc20Address); + uint256 max = 2**256 - 1; + erc20.safeApprove(address(conversionPaymentProxy), max); + } + + /** + * @notice Get conversion rate and decimals from chainlink */ function getRate(address[] memory _path, uint256 _maxRateTimespan) internal view - returns ( - uint256, - uint256, - uint256 - ) + returns (uint256, uint256) { (uint256 rate, uint256 oldestTimestampRate, uint256 decimals) = chainlinkConversionPath.getRate( _path @@ -262,17 +385,7 @@ contract BatchConversionPayments is BatchPaymentsPublic { _maxRateTimespan == 0 || block.timestamp - oldestTimestampRate <= _maxRateTimespan, 'aggregator rate is outdated' ); - return (rate, decimals, oldestTimestampRate); - } - - /** - * @notice Authorizes the conveersion proxy to spend a new request currency (ERC20). - * @param _erc20Address Address of an ERC20 used as the request currency. - */ - function approveConversionPaymentProxyToSpend(address _erc20Address) public { - IERC20 erc20 = IERC20(_erc20Address); - uint256 max = 2**256 - 1; - erc20.safeApprove(address(conversionPaymentProxy), max); + return (rate, decimals); } /* @@ -292,6 +405,10 @@ contract BatchConversionPayments is BatchPaymentsPublic { conversionPaymentProxy = IERC20ConversionProxy(_paymentErc20ConversionFeeProxy); } + function setEthConversionPaymentProxy(address _paymentEthConversionFeeProxy) public onlyOwner { + conversionPaymentEthProxy = IEthConversionProxy(_paymentEthConversionFeeProxy); + } + /** * @notice Update the conversion path contract used to fetch conversions * @param _chainlinkConversionPathAddress address of the conversion path contract diff --git a/packages/smart-contracts/src/contracts/BatchPaymentsPublic.sol b/packages/smart-contracts/src/contracts/BatchPaymentsPublic.sol index bafcdffbd..3f8fb7a6a 100644 --- a/packages/smart-contracts/src/contracts/BatchPaymentsPublic.sol +++ b/packages/smart-contracts/src/contracts/BatchPaymentsPublic.sol @@ -6,7 +6,6 @@ import './lib/SafeERC20.sol'; import '@openzeppelin/contracts/access/Ownable.sol'; import './interfaces/ERC20FeeProxy.sol'; import './interfaces/EthereumFeeProxy.sol'; -import '@openzeppelin/contracts/security/ReentrancyGuard.sol'; /** * @title BatchPayments @@ -16,8 +15,9 @@ import '@openzeppelin/contracts/security/ReentrancyGuard.sol'; * - fees: ERC20 and ETH proxies fees are paid to the same address. * An additional batch fee is paid to the same address. * If one transaction of the batch fail, every transactions are reverted. + * @dev Please notify than fees are now divided by 10_000 instead of 1_000 in previous version */ -contract BatchPaymentsPublic is Ownable, ReentrancyGuard { +contract BatchPaymentsPublic is Ownable { using SafeERC20 for IERC20; IERC20FeeProxy public paymentErc20FeeProxy; @@ -70,7 +70,7 @@ contract BatchPaymentsPublic is Ownable, ReentrancyGuard { bytes[] calldata _paymentReferences, uint256[] calldata _feeAmounts, address payable _feeAddress - ) public payable nonReentrant { + ) public payable { require( _recipients.length == _amounts.length && _recipients.length == _paymentReferences.length && @@ -285,11 +285,15 @@ contract BatchPaymentsPublic is Ownable, ReentrancyGuard { } } + /* + * Helper functions + */ + /** * @notice Authorizes the proxy to spend a new request currency (ERC20). * @param _erc20Address Address of an ERC20 used as the request currency. */ - function approvePaymentProxyToSpend(address _erc20Address) public { + function approvePaymentProxyToSpend(address _erc20Address) private { IERC20 erc20 = IERC20(_erc20Address); uint256 max = 2**256 - 1; erc20.safeApprove(address(paymentErc20FeeProxy), max); diff --git a/packages/smart-contracts/src/contracts/interfaces/IEthConversionProxy.sol b/packages/smart-contracts/src/contracts/interfaces/IEthConversionProxy.sol new file mode 100644 index 000000000..beaee97af --- /dev/null +++ b/packages/smart-contracts/src/contracts/interfaces/IEthConversionProxy.sol @@ -0,0 +1,50 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +/** + * @title IEthConversionProxy + * @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 + */ +interface IEthConversionProxy { + // 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; +} diff --git a/packages/smart-contracts/src/lib/artifacts/BatchConversionPayments/0.1.0.json b/packages/smart-contracts/src/lib/artifacts/BatchConversionPayments/0.1.0.json index 766fb754c..840f1bd28 100644 --- a/packages/smart-contracts/src/lib/artifacts/BatchConversionPayments/0.1.0.json +++ b/packages/smart-contracts/src/lib/artifacts/BatchConversionPayments/0.1.0.json @@ -17,6 +17,11 @@ "name": "_paymentErc20ConversionFeeProxy", "type": "address" }, + { + "internalType": "address", + "name": "_paymentEthConversionFeeProxy", + "type": "address" + }, { "internalType": "address", "name": "_chainlinkConversionPathAddress", @@ -63,19 +68,6 @@ "stateMutability": "nonpayable", "type": "function" }, - { - "inputs": [ - { - "internalType": "address", - "name": "_erc20Address", - "type": "address" - } - ], - "name": "approvePaymentProxyToSpend", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, { "inputs": [], "name": "basicFee", @@ -152,7 +144,7 @@ "type": "address" } ], - "name": "batchERC20ConversionPaymentsMultiTokensEasy", + "name": "batchERC20ConversionPaymentsMultiTokens", "outputs": [], "stateMutability": "nonpayable", "type": "function" @@ -233,6 +225,61 @@ "stateMutability": "nonpayable", "type": "function" }, + { + "inputs": [ + { + "components": [ + { + "internalType": "address", + "name": "_recipient", + "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": "uint256", + "name": "_maxToSpend", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "_maxRateTimespan", + "type": "uint256" + } + ], + "internalType": "struct BatchConversionPayments.RequestInfo[]", + "name": "requestsInfo", + "type": "tuple[]" + }, + { + "internalType": "address payable", + "name": "_feeAddress", + "type": "address" + } + ], + "name": "batchEthConversionPaymentsWithReference", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, { "inputs": [ { @@ -375,7 +422,7 @@ ], "name": "batchRouter", "outputs": [], - "stateMutability": "nonpayable", + "stateMutability": "payable", "type": "function" }, { @@ -502,6 +549,19 @@ "stateMutability": "nonpayable", "type": "function" }, + { + "inputs": [ + { + "internalType": "address", + "name": "_paymentEthConversionFeeProxy", + "type": "address" + } + ], + "name": "setEthConversionPaymentProxy", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, { "inputs": [ { diff --git a/packages/smart-contracts/test/contracts/BatchERC20ConversionPayments.test.ts b/packages/smart-contracts/test/contracts/BatchConversionPayments.test.ts similarity index 69% rename from packages/smart-contracts/test/contracts/BatchERC20ConversionPayments.test.ts rename to packages/smart-contracts/test/contracts/BatchConversionPayments.test.ts index cbe0946dc..f1f8cc5fe 100644 --- a/packages/smart-contracts/test/contracts/BatchERC20ConversionPayments.test.ts +++ b/packages/smart-contracts/test/contracts/BatchConversionPayments.test.ts @@ -2,6 +2,7 @@ import { ethers, network } from 'hardhat'; import { ERC20FeeProxy__factory, Erc20ConversionProxy__factory, + EthConversionProxy__factory, BatchConversionPayments__factory, EthereumFeeProxy__factory, ERC20FeeProxy, @@ -9,6 +10,7 @@ import { ChainlinkConversionPath, TestERC20, Erc20ConversionProxy, + EthConversionProxy, TestERC20__factory, BatchConversionPayments, } from '../../src/types'; @@ -18,8 +20,14 @@ import { CurrencyManager } from '@requestnetwork/currency'; import { chainlinkConversionPath } from '../../src/lib'; import { localERC20AlphaArtifact } from './localArtifacts'; import Utils from '@requestnetwork/utils'; +import { HttpNetworkConfig } from 'hardhat/types'; + +const logGas = true; describe('contract: BatchErc20ConversionPayments', () => { + const networkConfig = network.config as HttpNetworkConfig; + const provider = new ethers.providers.JsonRpcProvider(networkConfig.url); + let from: string; let to: string; let feeAddress: string; @@ -42,6 +50,7 @@ describe('contract: BatchErc20ConversionPayments', () => { let fakeFAU_address: string; let testErc20ConversionProxy: Erc20ConversionProxy; + let testEthConversionProxy: EthConversionProxy; let testBatchConversionProxy: BatchConversionPayments; let testERC20: TestERC20; let testERC20b: TestERC20; @@ -93,9 +102,13 @@ describe('contract: BatchErc20ConversionPayments', () => { let emitOneTx: Function; /** * Function batch conversion, it can be the batchRouter function, used with conversion args, - * or directly batchERC20ConversionPaymentsMultiTokensEasy + * or directly batchERC20ConversionPaymentsMultiTokens * */ - let batchConvFunction: (args: any, feeAddress: string) => Promise; + let batchConvFunction: ( + args: any, + feeAddress: string, + optional?: any, + ) => Promise; /** Format arguments so they can be used by batchConvFunction */ let argTemplate: Function; @@ -111,10 +124,16 @@ describe('contract: BatchErc20ConversionPayments', () => { chainlinkPath.address, await signer.getAddress(), ); + testEthConversionProxy = await new EthConversionProxy__factory(signer).deploy( + ethereumFeeProxy.address, + chainlinkPath.address, + ETH_hash, + ); testBatchConversionProxy = await new BatchConversionPayments__factory(signer).deploy( erc20FeeProxy.address, ethereumFeeProxy.address, testErc20ConversionProxy.address, + testEthConversionProxy.address, chainlinkPath.address, await signer.getAddress(), ); @@ -204,7 +223,7 @@ describe('contract: BatchErc20ConversionPayments', () => { * It can also be used for batch IF batchFee == batchConvFee * The 3rd arg is used to calcul batch fees */ - const calculBalances = ( + const calculERC20Balances = ( _conversionToPay_result: BigNumber, _conversionFees_result: BigNumber, _conversionsToPay_results: BigNumber[], @@ -227,7 +246,7 @@ describe('contract: BatchErc20ConversionPayments', () => { feeDiffBalanceExpected = feeDiffBalanceExpected.add(batchFeeToPay(sumToPay)); }; - const transferOneTokenConv = async (path: string[], logGas = false) => { + const transferOneTokenConv = async (path: string[]) => { await initConvToPayAndRequestInfo(to, path, amountInFiat, feesAmountInFiat, 0, chainlinkPath); const result = batchConvFunction(argTemplate([requestInfo]), feeAddress); @@ -240,10 +259,10 @@ describe('contract: BatchErc20ConversionPayments', () => { await emitOneTx(expect(result), requestInfo, conversionToPay, conversionFees); } - calculBalances(conversionToPay.result, conversionFees.result, [conversionToPay.result]); + calculERC20Balances(conversionToPay.result, conversionFees.result, [conversionToPay.result]); }; - const twoTransferOneTokenConv = async (path2: string[], nTimes: number, logGas = false) => { + const twoTransferOneTokenConv = async (path2: string[], nTimes: number) => { const coef = 2; const amountInFiat2 = BigNumber.from(amountInFiat).mul(coef).toString(); const feesAmountInFiat2 = BigNumber.from(feesAmountInFiat).mul(coef).toString(); @@ -277,22 +296,22 @@ describe('contract: BatchErc20ConversionPayments', () => { requestInfo2._path[requestInfo2._path.length - 1] ) { for (let i = 0; i < nTimes - 1; i++) { - calculBalances(conversionToPay.result, conversionFees.result, []); - calculBalances(conversionToPay2.result, conversionFees2.result, []); + calculERC20Balances(conversionToPay.result, conversionFees.result, []); + calculERC20Balances(conversionToPay2.result, conversionFees2.result, []); } - calculBalances(conversionToPay.result, conversionFees.result, []); - calculBalances( + calculERC20Balances(conversionToPay.result, conversionFees.result, []); + calculERC20Balances( conversionToPay2.result, conversionFees2.result, conversionsToPay.map((ctp) => ctp.result), ); } else { for (let i = 0; i < nTimes - 1; i++) { - calculBalances(conversionToPay.result, conversionFees.result, []); + calculERC20Balances(conversionToPay.result, conversionFees.result, []); } const conversionsToPayBis = conversionsToPay.filter((_, i) => i % 2 === 0); - calculBalances( + calculERC20Balances( conversionToPay.result, conversionFees.result, conversionsToPayBis.map((ctp) => ctp.result), @@ -300,7 +319,7 @@ describe('contract: BatchErc20ConversionPayments', () => { } }; - const testSuite = (suiteName: string) => { + const ERC20TestSuite = (suiteName: string) => { emitOneTx = ( result: Chai.Assertion, requestInfo: RequestInfo, @@ -345,15 +364,15 @@ describe('contract: BatchErc20ConversionPayments', () => { ]; }; } - if (suiteName === 'batchERC20ConversionPaymentsMultiTokensEasy') { - batchConvFunction = testBatchConversionProxy.batchERC20ConversionPaymentsMultiTokensEasy; + if (suiteName === 'batchERC20ConversionPaymentsMultiTokens') { + batchConvFunction = testBatchConversionProxy.batchERC20ConversionPaymentsMultiTokens; argTemplate = (requestInfos: RequestInfo[]) => { return requestInfos; }; } }); describe(suiteName, () => { - describe('batchERC20ConversionPaymentsMultiTokensEasy with DAI', async () => { + describe('batchERC20ConversionPaymentsMultiTokens with DAI', async () => { it('allows to transfer DAI tokens for USD payment', async () => { await transferOneTokenConv(path); }); @@ -366,20 +385,20 @@ describe('contract: BatchErc20ConversionPayments', () => { }); it('allows to transfer DAI tokens for EUR payment - GAS', async () => { path = [EUR_hash, USD_hash, DAI_address]; - await transferOneTokenConv(path, true); + await transferOneTokenConv(path); }); it('allows to transfer 2 transactions DAI tokens for USD and EUR payment - GAS', async function () { const path2 = [EUR_hash, USD_hash, DAI_address]; - await twoTransferOneTokenConv(path2, 1, true); + await twoTransferOneTokenConv(path2, 1); }); it('TMP allows to transfer two kind of tokens for USD - GAS', async function () { const path2 = [USD_hash, fakeFAU_address]; - await twoTransferOneTokenConv(path2, 1, true); + await twoTransferOneTokenConv(path2, 1); }); }); }); - describe('batchERC20ConversionPaymentsMultiTokensEasy with errors', () => { + describe('batchERC20ConversionPaymentsMultiTokens with errors', () => { it('cannot transfer with invalid path', async function () { const wrongPath = [EUR_hash, ETH_hash, DAI_address]; requestInfo._path = wrongPath; @@ -468,7 +487,9 @@ describe('contract: BatchErc20ConversionPayments', () => { amount * (batchFee / 10_000), // batch fee amount = 200 * 1% ); - calculBalances(BigNumber.from(amount), BigNumber.from(feeAmount), [BigNumber.from(amount)]); + calculERC20Balances(BigNumber.from(amount), BigNumber.from(feeAmount), [ + BigNumber.from(amount), + ]); }; it('batchERC20PaymentsWithReference transfers token', async function () { await batchERC20Payments(false, 'batchERC20PaymentsWithReference'); @@ -485,6 +506,148 @@ describe('contract: BatchErc20ConversionPayments', () => { }); }); - testSuite('batchRouter'); - testSuite('batchERC20ConversionPaymentsMultiTokensEasy'); + const EthTestSuite = (suiteName: string) => { + describe(`Test ETH ${suiteName} functions`, () => { + let beforeEthBalanceTo: BigNumber; + let beforeEthBalanceFee: BigNumber; + let beforeEthBalance: BigNumber; + let feesToPay: ConvToPay; + let tx: ContractTransaction; + let amountToPayExpected: BigNumber; + let feeToPayExpected: BigNumber; + const amount = BigNumber.from(100000); // usually in USD + const feeAmount = amount.mul(basicFee).div(10000); // usually in USD + let inputs: Array; + const pathUsdEth = [USD_hash, ETH_hash]; + + const getInputs = (inputs: Array) => { + if (suiteName !== 'batchEthConversionPaymentsWithReference') { + return [ + { + paymentNetworkId: '3', + requestsInfo: inputs, + requestsInfoParent: requestsInfoParent1, // not used + }, + ]; + } + return inputs; + }; + + before(() => { + if (suiteName === 'batchEthConversionPaymentsWithReference') { + batchConvFunction = testBatchConversionProxy.batchEthConversionPaymentsWithReference; + } else { + batchConvFunction = testBatchConversionProxy.batchRouter; + } + + requestInfo = { + _recipient: to, + _requestAmount: amount, + _path: pathUsdEth, + _paymentReference: referenceExample, + _feeAmount: feeAmount, + _maxToSpend: BigNumber.from(0), + _maxRateTimespan: BigNumber.from(0), + }; + }); + + describe('success functions', () => { + beforeEach(async () => { + beforeEthBalanceTo = await provider.getBalance(to); + beforeEthBalanceFee = await provider.getBalance(feeAddress); + beforeEthBalance = await provider.getBalance(await signer.getAddress()); + requestInfo = { + _recipient: to, + _requestAmount: amount, + _path: pathUsdEth, + _paymentReference: referenceExample, + _feeAmount: feeAmount, + _maxToSpend: BigNumber.from(0), + _maxRateTimespan: BigNumber.from(0), + }; + + // basic setup: 1 payment + conversionToPay = await chainlinkPath.getConversion( + requestInfo._requestAmount, + requestInfo._path, + ); + feesToPay = await chainlinkPath.getConversion(requestInfo._feeAmount, requestInfo._path); + + amountToPayExpected = conversionToPay.result; + feeToPayExpected = feesToPay.result; + }); + + afterEach(async () => { + tx = await batchConvFunction(getInputs(inputs), feeAddress, { + value: BigNumber.from('100000000000000000'), + }); + const receipt = await tx.wait(); + + if (logGas) console.log('gas consumption: ', receipt.gasUsed.toString()); + + const afterEthBalance = await provider.getBalance(await signer.getAddress()); + const afterEthBalanceTo = await provider.getBalance(to); + const afterEthBalanceFee = await provider.getBalance(feeAddress); + + const proxyBalance = await provider.getBalance(testBatchConversionProxy.address); + const _diffBalance = beforeEthBalance.sub(afterEthBalance); + const _diffBalanceTo = afterEthBalanceTo.sub(beforeEthBalanceTo); + const _diffBalanceFee = afterEthBalanceFee.sub(beforeEthBalanceFee); + const _diffBalanceExpect = receipt.gasUsed + .mul(2 * 10 ** 10) + .add(_diffBalanceTo) + .add(_diffBalanceFee); + + expect(_diffBalance).to.equals(_diffBalanceExpect.toString(), 'DiffBalance'); + expect(_diffBalanceTo).to.equals(amountToPayExpected.toString(), 'diffBalanceTo'); + + expect(_diffBalanceFee.toString()).to.equals( + amountToPayExpected.mul(batchConvFee).div(10000).add(feeToPayExpected).toString(), + 'diffBalanceFee', + ); + expect(proxyBalance).to.equals('0', 'proxyBalance'); + }); + + it('batchEthConversionPaymentsWithReference transfer 1 payment in ethers denominated in USD', async function () { + inputs = [requestInfo]; + }); + + it('batchEthConversionPaymentsWithReference transfer 3 payment in ethers denominated in USD', async function () { + amountToPayExpected = amountToPayExpected.mul(3); + feeToPayExpected = feeToPayExpected.mul(3); + inputs = [requestInfo, requestInfo, requestInfo]; + }); + + it('batchEthConversionPaymentsWithReference transfer 3 payments in ethers denominated in USD and EUR', async function () { + const EurRequestInfo = Utils.deepCopy(requestInfo); + EurRequestInfo._path = [EUR_hash, USD_hash, ETH_hash]; + + const eurConversionToPay = await chainlinkPath.getConversion( + EurRequestInfo._requestAmount, + EurRequestInfo._path, + ); + const eurFeesToPay = await chainlinkPath.getConversion( + EurRequestInfo._feeAmount, + EurRequestInfo._path, + ); + + amountToPayExpected = eurConversionToPay.result.add(amountToPayExpected.mul(2)); + feeToPayExpected = eurFeesToPay.result.add(feeToPayExpected.mul(2)); + inputs = [requestInfo, EurRequestInfo, requestInfo]; + }); + }); + it('batchEthConversionPaymentsWithReference transfer FAIL: not enough funds', async function () { + await expect( + batchConvFunction(getInputs([requestInfo]), feeAddress, { + value: 10000, + }), + ).to.be.revertedWith('not enough funds'); + }); + }); + }; + + ERC20TestSuite('batchRouter'); + ERC20TestSuite('batchERC20ConversionPaymentsMultiTokens'); + EthTestSuite('batchRouter'); + EthTestSuite('batchEthConversionPaymentsWithReference'); }); From 0ebe8b00405cc7b7bcf601e3e32e5b6809fd3276 Mon Sep 17 00:00:00 2001 From: olivier7delf <55892112+olivier7delf@users.noreply.github.com> Date: Fri, 29 Jul 2022 16:49:27 +0200 Subject: [PATCH 006/105] refacto batch contracts approval functions --- .../src/contracts/BatchConversionPayments.sol | 20 +++++-------------- .../src/contracts/BatchPaymentsPublic.sol | 17 +++++++++------- .../contracts/BatchConversionPayments.test.ts | 3 ++- 3 files changed, 17 insertions(+), 23 deletions(-) diff --git a/packages/smart-contracts/src/contracts/BatchConversionPayments.sol b/packages/smart-contracts/src/contracts/BatchConversionPayments.sol index f71f67d24..ee45be2cb 100644 --- a/packages/smart-contracts/src/contracts/BatchConversionPayments.sol +++ b/packages/smart-contracts/src/contracts/BatchConversionPayments.sol @@ -13,8 +13,8 @@ import './BatchPaymentsPublic.sol'; * - ERC20 tokens: using Erc20ConversionProxy and ERC20FeeProxy * - Native token: as Eth, using EthConversionProxy and EthereumFeeProxy * - to: multiple addresses - * - fees: conversion proxy fees and additional batch conversion fee are paid to the same address. - * batchRouter is the main function to batch every kind of payment at once. + * - fees: conversion proxy fees and additional batch conversion fees are paid to the same address. + * batchRouter is the main function to batch every kind of payments at once. * If one transaction of the batch fail, every transactions are reverted. * @dev Please notify than fees are now divided by 10_000 instead of 1_000 in previous version * batchRouter is the main function, but others batch payment functions are "public" in order to do @@ -65,7 +65,7 @@ contract BatchConversionPayments is BatchPaymentsPublic { /** * @dev Used by batchRouter to hold information for any kind of request. - * - paymentNetworkId requests are group by paymentType to be paid with the appropriate function. + * - paymentNetworkId requests are grouped by paymentType to be paid with the appropriate function. * More details in batchRouter description. * - requestsInfo all informations required for conversion requests to be paid (=> paymentNetworkId equal 0 or 3) * - requestsInfoParent all informations required for None-conversion requests to be paid @@ -225,7 +225,7 @@ contract BatchConversionPayments is BatchPaymentsPublic { requestedToken.allowance(address(this), address(conversionPaymentProxy)) < uTokens[k].amountAndFee ) { - approveConversionPaymentProxyToSpend(uTokens[k].tokenAddress); + approvePaymentProxyToSpend(uTokens[k].tokenAddress, address(conversionPaymentProxy)); } } @@ -358,16 +358,6 @@ contract BatchConversionPayments is BatchPaymentsPublic { return (requestAmount * rate) / decimals + (requestFee * rate) / decimals; } - /** - * @notice Authorizes the conveersion proxy to spend a new request currency (ERC20). - * @param _erc20Address Address of an ERC20 used as the request currency. - */ - function approveConversionPaymentProxyToSpend(address _erc20Address) public { - IERC20 erc20 = IERC20(_erc20Address); - uint256 max = 2**256 - 1; - erc20.safeApprove(address(conversionPaymentProxy), max); - } - /** * @notice Get conversion rate and decimals from chainlink */ @@ -392,7 +382,7 @@ contract BatchConversionPayments is BatchPaymentsPublic { * Admin functions to edit the conversion proxies address */ - /** fees applied on a single request*/ + /** fees applied on a single request */ function setBasicFee(uint256 _basicFee) public onlyOwner { basicFee = _basicFee; } diff --git a/packages/smart-contracts/src/contracts/BatchPaymentsPublic.sol b/packages/smart-contracts/src/contracts/BatchPaymentsPublic.sol index 3f8fb7a6a..00a1c0f98 100644 --- a/packages/smart-contracts/src/contracts/BatchPaymentsPublic.sol +++ b/packages/smart-contracts/src/contracts/BatchPaymentsPublic.sol @@ -8,14 +8,16 @@ import './interfaces/ERC20FeeProxy.sol'; import './interfaces/EthereumFeeProxy.sol'; /** - * @title BatchPayments - * @notice This contract makes multiple payments with references, in one transaction: + * @title BatchPaymentsPublic + * @notice This contract makes multiple payments with references, in one transaction: * - on: ERC20 Payment Proxy and ETH Payment Proxy of the Request Network protocol * - to: multiple addresses * - fees: ERC20 and ETH proxies fees are paid to the same address. * An additional batch fee is paid to the same address. * If one transaction of the batch fail, every transactions are reverted. - * @dev Please notify than fees are now divided by 10_000 instead of 1_000 in previous version + * @dev It is a clone of BatchPayment.sol, with two main modifications: + * - fees are now divided by 10_000 instead of 1_000 in previous version + * - batch payment functions are now public, instead of external */ contract BatchPaymentsPublic is Ownable { using SafeERC20 for IERC20; @@ -155,7 +157,7 @@ contract BatchPaymentsPublic is Ownable { // Batch contract approve Erc20FeeProxy to spend the token if (requestedToken.allowance(address(this), address(paymentErc20FeeProxy)) < amount) { - approvePaymentProxyToSpend(address(requestedToken)); + approvePaymentProxyToSpend(address(requestedToken), address(paymentErc20FeeProxy)); } // Batch contract pays the requests using Erc20FeeProxy @@ -262,7 +264,7 @@ contract BatchPaymentsPublic is Ownable { requestedToken.allowance(address(this), address(paymentErc20FeeProxy)) < uniqueTokens[i].amountAndFee ) { - approvePaymentProxyToSpend(address(requestedToken)); + approvePaymentProxyToSpend(address(requestedToken), address(paymentErc20FeeProxy)); } // Payer pays batch fee amount @@ -292,11 +294,12 @@ contract BatchPaymentsPublic is Ownable { /** * @notice Authorizes the proxy to spend a new request currency (ERC20). * @param _erc20Address Address of an ERC20 used as the request currency. + * @param _paymentErc20Proxy Address of the proxy. */ - function approvePaymentProxyToSpend(address _erc20Address) private { + function approvePaymentProxyToSpend(address _erc20Address, address _paymentErc20Proxy) internal { IERC20 erc20 = IERC20(_erc20Address); uint256 max = 2**256 - 1; - erc20.safeApprove(address(paymentErc20FeeProxy), max); + erc20.safeApprove(address(_paymentErc20Proxy), max); } /** diff --git a/packages/smart-contracts/test/contracts/BatchConversionPayments.test.ts b/packages/smart-contracts/test/contracts/BatchConversionPayments.test.ts index f1f8cc5fe..872c2010e 100644 --- a/packages/smart-contracts/test/contracts/BatchConversionPayments.test.ts +++ b/packages/smart-contracts/test/contracts/BatchConversionPayments.test.ts @@ -22,7 +22,8 @@ import { localERC20AlphaArtifact } from './localArtifacts'; import Utils from '@requestnetwork/utils'; import { HttpNetworkConfig } from 'hardhat/types'; -const logGas = true; +// set to true to log batch payments's gas consumption +const logGas = false; describe('contract: BatchErc20ConversionPayments', () => { const networkConfig = network.config as HttpNetworkConfig; From 1818239645ed5751afe72fd2d6b50484c5b38325 Mon Sep 17 00:00:00 2001 From: olivier7delf <55892112+olivier7delf@users.noreply.github.com> Date: Wed, 3 Aug 2022 09:51:50 +0200 Subject: [PATCH 007/105] PR - update batch contact - function visibility and comments --- .../src/contracts/BatchConversionPayments.sol | 18 +++++++++++------- .../src/contracts/BatchPaymentsPublic.sol | 6 +++--- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/packages/smart-contracts/src/contracts/BatchConversionPayments.sol b/packages/smart-contracts/src/contracts/BatchConversionPayments.sol index ee45be2cb..53766e133 100644 --- a/packages/smart-contracts/src/contracts/BatchConversionPayments.sol +++ b/packages/smart-contracts/src/contracts/BatchConversionPayments.sol @@ -29,6 +29,7 @@ contract BatchConversionPayments is BatchPaymentsPublic { // Between 0 and 10000, i.e: batchFee = 100 represent 1% of fee uint256 public batchConversionFee; + // Between 0 and 10000,fees applied for basic invoice, 0.1% at Request Finance uint256 public basicFee; /** @@ -172,8 +173,11 @@ contract BatchConversionPayments is BatchPaymentsPublic { /** * @notice Transfers a batch of multiple ERC20 tokens with a reference with amount based on the request amount in fiat - * @param requestsInfo list of requestInfo, each one containing every informations of a request. + * @param requestsInfo list of requestInfo, each one containing every informations of a request * @param _feeAddress The fee recipient + * @dev amountAndFee is an approximation of the amount and the fee to be paid, in order to get enough tokens. + * The excess is sent back to the payer + * batchFeeAmount is an approximation for the same reason of amountAndFee */ function batchERC20ConversionPaymentsMultiTokens( RequestInfo[] calldata requestsInfo, @@ -255,7 +259,7 @@ contract BatchConversionPayments is BatchPaymentsPublic { requestedToken.safeTransfer(msg.sender, excessAmount); } - // Payer pays batch fees amount + // Payer pays the exact batch fees amount require( safeTransferFrom( uTokens[k].tokenAddress, @@ -288,7 +292,7 @@ contract BatchConversionPayments is BatchPaymentsPublic { Path[] memory rPaths = new Path[](requestsInfo.length); for (uint256 i = 0; i < requestsInfo.length; i++) { RequestInfo memory rI = requestsInfo[i]; - for (uint64 k = 0; k < requestsInfo.length; k++) { + for (uint256 k = 0; k < requestsInfo.length; k++) { // Check if the path is already known if (rPaths[k].rate > 0 && rPaths[k]._path[0] == rI._path[0]) { // use the already known rate and decimals from path already queried @@ -383,19 +387,19 @@ contract BatchConversionPayments is BatchPaymentsPublic { */ /** fees applied on a single request */ - function setBasicFee(uint256 _basicFee) public onlyOwner { + function setBasicFee(uint256 _basicFee) external onlyOwner { basicFee = _basicFee; } - function setBatchConversionFee(uint256 _batchConversionFee) public onlyOwner { + function setBatchConversionFee(uint256 _batchConversionFee) external onlyOwner { batchConversionFee = _batchConversionFee; } - function setConversionPaymentProxy(address _paymentErc20ConversionFeeProxy) public onlyOwner { + function setConversionPaymentProxy(address _paymentErc20ConversionFeeProxy) external onlyOwner { conversionPaymentProxy = IERC20ConversionProxy(_paymentErc20ConversionFeeProxy); } - function setEthConversionPaymentProxy(address _paymentEthConversionFeeProxy) public onlyOwner { + function setEthConversionPaymentProxy(address _paymentEthConversionFeeProxy) external onlyOwner { conversionPaymentEthProxy = IEthConversionProxy(_paymentEthConversionFeeProxy); } diff --git a/packages/smart-contracts/src/contracts/BatchPaymentsPublic.sol b/packages/smart-contracts/src/contracts/BatchPaymentsPublic.sol index 00a1c0f98..9d56851e5 100644 --- a/packages/smart-contracts/src/contracts/BatchPaymentsPublic.sol +++ b/packages/smart-contracts/src/contracts/BatchPaymentsPublic.sol @@ -352,15 +352,15 @@ contract BatchPaymentsPublic is Ownable { * Admin functions to edit the proxies address */ - function setBatchFee(uint256 _batchFee) public onlyOwner { + function setBatchFee(uint256 _batchFee) external onlyOwner { batchFee = _batchFee; } - function setPaymentErc20FeeProxy(address _paymentErc20FeeProxy) public onlyOwner { + function setPaymentErc20FeeProxy(address _paymentErc20FeeProxy) external onlyOwner { paymentErc20FeeProxy = IERC20FeeProxy(_paymentErc20FeeProxy); } - function setPaymentEthFeeProxy(address _paymentEthFeeProxy) public onlyOwner { + function setPaymentEthFeeProxy(address _paymentEthFeeProxy) external onlyOwner { paymentEthFeeProxy = IEthereumFeeProxy(_paymentEthFeeProxy); } } From 72c2a73da0cdd0d8e63f7a776083b7038c78a8eb Mon Sep 17 00:00:00 2001 From: olivier7delf <55892112+olivier7delf@users.noreply.github.com> Date: Wed, 3 Aug 2022 11:11:53 +0200 Subject: [PATCH 008/105] keep prefix underscore usage for function args --- .../src/contracts/BatchConversionPayments.sol | 104 +++++++++--------- 1 file changed, 52 insertions(+), 52 deletions(-) diff --git a/packages/smart-contracts/src/contracts/BatchConversionPayments.sol b/packages/smart-contracts/src/contracts/BatchConversionPayments.sol index 53766e133..9d76a121c 100644 --- a/packages/smart-contracts/src/contracts/BatchConversionPayments.sol +++ b/packages/smart-contracts/src/contracts/BatchConversionPayments.sol @@ -44,24 +44,24 @@ contract BatchConversionPayments is BatchPaymentsPublic { * _maxRateTimespan Max times span with the oldestrate, ignored if zero */ struct RequestInfo { - address _recipient; - uint256 _requestAmount; - address[] _path; - bytes _paymentReference; - uint256 _feeAmount; - uint256 _maxToSpend; - uint256 _maxRateTimespan; + address recipient; + uint256 requestAmount; + address[] path; + bytes paymentReference; + uint256 feeAmount; + uint256 maxToSpend; + uint256 maxRateTimespan; } /** * @dev It is the structure of the input for the function from contract BatchPaymentsPublic */ struct RequestsInfoParent { - address[] _tokenAddresses; - address[] _recipients; - uint256[] _amounts; - bytes[] _paymentReferences; - uint256[] _feeAmounts; + address[] tokenAddresses; + address[] recipients; + uint256[] amounts; + bytes[] paymentReferences; + uint256[] feeAmounts; } /** @@ -79,7 +79,7 @@ contract BatchConversionPayments is BatchPaymentsPublic { } struct Path { - address[] _path; + address[] path; uint256 rate; uint256 decimals; } @@ -136,20 +136,20 @@ contract BatchConversionPayments is BatchPaymentsPublic { batchERC20ConversionPaymentsMultiTokens(metaRequestsInfo.requestsInfo, _feeAddress); } else if (metaRequestsInfo.paymentNetworkId == 1) { batchERC20PaymentsWithReference( - metaRequestsInfo.requestsInfoParent._tokenAddresses[0], - metaRequestsInfo.requestsInfoParent._recipients, - metaRequestsInfo.requestsInfoParent._amounts, - metaRequestsInfo.requestsInfoParent._paymentReferences, - metaRequestsInfo.requestsInfoParent._feeAmounts, + metaRequestsInfo.requestsInfoParent.tokenAddresses[0], + metaRequestsInfo.requestsInfoParent.recipients, + metaRequestsInfo.requestsInfoParent.amounts, + metaRequestsInfo.requestsInfoParent.paymentReferences, + metaRequestsInfo.requestsInfoParent.feeAmounts, _feeAddress ); } else if (metaRequestsInfo.paymentNetworkId == 2) { batchERC20PaymentsMultiTokensWithReference( - metaRequestsInfo.requestsInfoParent._tokenAddresses, - metaRequestsInfo.requestsInfoParent._recipients, - metaRequestsInfo.requestsInfoParent._amounts, - metaRequestsInfo.requestsInfoParent._paymentReferences, - metaRequestsInfo.requestsInfoParent._feeAmounts, + metaRequestsInfo.requestsInfoParent.tokenAddresses, + metaRequestsInfo.requestsInfoParent.recipients, + metaRequestsInfo.requestsInfoParent.amounts, + metaRequestsInfo.requestsInfoParent.paymentReferences, + metaRequestsInfo.requestsInfoParent.feeAmounts, _feeAddress ); } else if (metaRequestsInfo.paymentNetworkId == 3) { @@ -159,10 +159,10 @@ contract BatchConversionPayments is BatchPaymentsPublic { ); } else if (metaRequestsInfo.paymentNetworkId == 4) { batchEthPaymentsWithReference( - metaRequestsInfo.requestsInfoParent._recipients, - metaRequestsInfo.requestsInfoParent._amounts, - metaRequestsInfo.requestsInfoParent._paymentReferences, - metaRequestsInfo.requestsInfoParent._feeAmounts, + metaRequestsInfo.requestsInfoParent.recipients, + metaRequestsInfo.requestsInfoParent.amounts, + metaRequestsInfo.requestsInfoParent.paymentReferences, + metaRequestsInfo.requestsInfoParent.feeAmounts, payable(_feeAddress) ); } else { @@ -188,15 +188,15 @@ contract BatchConversionPayments is BatchPaymentsPublic { for (uint256 i = 0; i < requestsInfo.length; i++) { for (uint256 k = 0; k < requestsInfo.length; k++) { // If the token is already in the existing uTokens list - if (uTokens[k].tokenAddress == requestsInfo[i]._path[requestsInfo[i]._path.length - 1]) { - uTokens[k].amountAndFee += requestsInfo[i]._maxToSpend; + if (uTokens[k].tokenAddress == requestsInfo[i].path[requestsInfo[i].path.length - 1]) { + uTokens[k].amountAndFee += requestsInfo[i].maxToSpend; break; } // If the token is not in the list (amountAndFee = 0) - else if (uTokens[k].amountAndFee == 0 && (requestsInfo[i]._maxToSpend) > 0) { - uTokens[k].tokenAddress = requestsInfo[i]._path[requestsInfo[i]._path.length - 1]; + else if (uTokens[k].amountAndFee == 0 && (requestsInfo[i].maxToSpend) > 0) { + uTokens[k].tokenAddress = requestsInfo[i].path[requestsInfo[i].path.length - 1]; // amountAndFee is used to store _maxToSpend, useful to send enough tokens to this contract - uTokens[k].amountAndFee = requestsInfo[i]._maxToSpend; + uTokens[k].amountAndFee = requestsInfo[i].maxToSpend; break; } } @@ -237,14 +237,14 @@ contract BatchConversionPayments is BatchPaymentsPublic { for (uint256 i = 0; i < requestsInfo.length; i++) { RequestInfo memory rI = requestsInfo[i]; conversionPaymentProxy.transferFromWithReferenceAndFee( - rI._recipient, - rI._requestAmount, - rI._path, - rI._paymentReference, - rI._feeAmount, + rI.recipient, + rI.requestAmount, + rI.path, + rI.paymentReference, + rI.feeAmount, _feeAddress, - rI._maxToSpend, - rI._maxRateTimespan + rI.maxToSpend, + rI.maxRateTimespan ); } @@ -294,22 +294,22 @@ contract BatchConversionPayments is BatchPaymentsPublic { RequestInfo memory rI = requestsInfo[i]; for (uint256 k = 0; k < requestsInfo.length; k++) { // Check if the path is already known - if (rPaths[k].rate > 0 && rPaths[k]._path[0] == rI._path[0]) { + if (rPaths[k].rate > 0 && rPaths[k].path[0] == rI.path[0]) { // use the already known rate and decimals from path already queried amountAndFeeToPay = amountAndFeeConversion( - rI._requestAmount, - rI._feeAmount, + rI.requestAmount, + rI.feeAmount, rPaths[k].rate, rPaths[k].decimals ); break; } else if (i == k) { // set the path, and get the associated rate and decimals - rPaths[i]._path = rI._path; - (rPaths[i].rate, rPaths[i].decimals) = getRate(rI._path, rI._maxRateTimespan); + rPaths[i].path = rI.path; + (rPaths[i].rate, rPaths[i].decimals) = getRate(rI.path, rI.maxRateTimespan); amountAndFeeToPay = amountAndFeeConversion( - rI._requestAmount, - rI._feeAmount, + rI.requestAmount, + rI.feeAmount, rPaths[i].rate, rPaths[i].decimals ); @@ -321,13 +321,13 @@ contract BatchConversionPayments is BatchPaymentsPublic { // Batch contract pays the requests through EthConversionProxy conversionPaymentEthProxy.transferWithReferenceAndFee{value: amountAndFeeToPay}( - payable(rI._recipient), - rI._requestAmount, - rI._path, - rI._paymentReference, - rI._feeAmount, + payable(rI.recipient), + rI.requestAmount, + rI.path, + rI.paymentReference, + rI.feeAmount, _feeAddress, - rI._maxRateTimespan + rI.maxRateTimespan ); } From 3ed489cfaa03063d4cfc0c336ff70c1d1cc4a30c Mon Sep 17 00:00:00 2001 From: olivier7delf <55892112+olivier7delf@users.noreply.github.com> Date: Wed, 3 Aug 2022 12:48:57 +0200 Subject: [PATCH 009/105] convention naming in batch contract and more comments --- .../src/contracts/BatchConversionPayments.sol | 83 +++++++----- .../src/contracts/BatchPaymentsPublic.sol | 51 +++++--- .../BatchConversionPayments/0.1.0.json | 91 ++++++------- .../contracts/BatchConversionPayments.test.ts | 120 +++++++++--------- 4 files changed, 178 insertions(+), 167 deletions(-) diff --git a/packages/smart-contracts/src/contracts/BatchConversionPayments.sol b/packages/smart-contracts/src/contracts/BatchConversionPayments.sol index 9d76a121c..dd3c96585 100644 --- a/packages/smart-contracts/src/contracts/BatchConversionPayments.sol +++ b/packages/smart-contracts/src/contracts/BatchConversionPayments.sol @@ -14,8 +14,8 @@ import './BatchPaymentsPublic.sol'; * - Native token: as Eth, using EthConversionProxy and EthereumFeeProxy * - to: multiple addresses * - fees: conversion proxy fees and additional batch conversion fees are paid to the same address. - * batchRouter is the main function to batch every kind of payments at once. - * If one transaction of the batch fail, every transactions are reverted. + * batchRouter is the main function to batch all kind of payments at once. + * If one transaction of the batch fail, all transactions are reverted. * @dev Please notify than fees are now divided by 10_000 instead of 1_000 in previous version * batchRouter is the main function, but others batch payment functions are "public" in order to do * gas optimization in some cases. @@ -23,17 +23,15 @@ import './BatchPaymentsPublic.sol'; contract BatchConversionPayments is BatchPaymentsPublic { using SafeERC20 for IERC20; - IERC20ConversionProxy conversionPaymentProxy; - IEthConversionProxy conversionPaymentEthProxy; + IERC20ConversionProxy paymentErc20ConversionProxy; + IEthConversionProxy paymentEthConversionProxy; ChainlinkConversionPath public chainlinkConversionPath; - // Between 0 and 10000, i.e: batchFee = 100 represent 1% of fee uint256 public batchConversionFee; - // Between 0 and 10000,fees applied for basic invoice, 0.1% at Request Finance uint256 public basicFee; /** - * @dev Every informations of a request, excepted the feeAddress + * @dev All the information of a request, excepted the feeAddress * _recipient Recipients address of the payement * _requestAmount Request amount in fiat * _path Conversion path @@ -85,26 +83,26 @@ contract BatchConversionPayments is BatchPaymentsPublic { } /** - * @param _paymentErc20FeeProxy The address to the ERC20 payment proxy to use. - * @param _paymentEthFeeProxy The address to the Ethereum payment proxy to use. - * @param _paymentErc20ConversionFeeProxy The address of the ERC20 Conversion payment proxy to use. - * @param _paymentEthConversionFeeProxy The address of the ETH Conversion payment proxy to use. + * @param _paymentErc20Proxy The address to the ERC20 fee payment proxy to use. + * @param _paymentEthProxy The address to the Ethereum fee payment proxy to use. + * @param _paymentErc20ConversionProxy The address of the ERC20 Conversion payment proxy to use. + * @param _paymentEthConversionFeeProxy The address of the Ethereum Conversion payment proxy to use. * @param _chainlinkConversionPathAddress The address of the conversion path contract * @param _owner Owner of the contract. */ constructor( - address _paymentErc20FeeProxy, - address _paymentEthFeeProxy, - address _paymentErc20ConversionFeeProxy, + address _paymentErc20Proxy, + address _paymentEthProxy, + address _paymentErc20ConversionProxy, address _paymentEthConversionFeeProxy, address _chainlinkConversionPathAddress, address _owner - ) BatchPaymentsPublic(_paymentErc20FeeProxy, _paymentEthFeeProxy, _owner) { - paymentErc20FeeProxy = IERC20FeeProxy(_paymentErc20FeeProxy); - paymentEthFeeProxy = IEthereumFeeProxy(_paymentEthFeeProxy); + ) BatchPaymentsPublic(_paymentErc20Proxy, _paymentEthProxy, _owner) { + paymentErc20Proxy = IERC20FeeProxy(_paymentErc20Proxy); + paymentEthProxy = IEthereumFeeProxy(_paymentEthProxy); - conversionPaymentProxy = IERC20ConversionProxy(_paymentErc20ConversionFeeProxy); - conversionPaymentEthProxy = IEthConversionProxy(_paymentEthConversionFeeProxy); + paymentErc20ConversionProxy = IERC20ConversionProxy(_paymentErc20ConversionProxy); + paymentEthConversionProxy = IEthConversionProxy(_paymentEthConversionFeeProxy); chainlinkConversionPath = ChainlinkConversionPath(_chainlinkConversionPathAddress); transferOwnership(_owner); @@ -173,7 +171,7 @@ contract BatchConversionPayments is BatchPaymentsPublic { /** * @notice Transfers a batch of multiple ERC20 tokens with a reference with amount based on the request amount in fiat - * @param requestsInfo list of requestInfo, each one containing every informations of a request + * @param requestsInfo list of requestInfo, each one containing all the information of a request * @param _feeAddress The fee recipient * @dev amountAndFee is an approximation of the amount and the fee to be paid, in order to get enough tokens. * The excess is sent back to the payer @@ -226,17 +224,17 @@ contract BatchConversionPayments is BatchPaymentsPublic { // Batch contract approves Erc20ConversionProxy to spend the token if ( - requestedToken.allowance(address(this), address(conversionPaymentProxy)) < + requestedToken.allowance(address(this), address(paymentErc20ConversionProxy)) < uTokens[k].amountAndFee ) { - approvePaymentProxyToSpend(uTokens[k].tokenAddress, address(conversionPaymentProxy)); + approvePaymentProxyToSpend(uTokens[k].tokenAddress, address(paymentErc20ConversionProxy)); } } // Batch pays the requests using Erc20ConversionFeeProxy for (uint256 i = 0; i < requestsInfo.length; i++) { RequestInfo memory rI = requestsInfo[i]; - conversionPaymentProxy.transferFromWithReferenceAndFee( + paymentErc20ConversionProxy.transferFromWithReferenceAndFee( rI.recipient, rI.requestAmount, rI.path, @@ -275,7 +273,7 @@ contract BatchConversionPayments is BatchPaymentsPublic { /** * @notice Send a batch of Eth conversion payments w/fees with paymentReferences to multiple accounts. * If one payment failed, the whole batch is reverted. - * @param requestsInfo List of requestInfos, each one containing every informations of a request. + * @param requestsInfo List of requestInfos, each one containing all the information of a request. * _maxToSpend is not used in this function. * @param _feeAddress The fee recipient. * @dev It uses EthereumConversionProxy to pay an invoice and fees. @@ -306,7 +304,7 @@ contract BatchConversionPayments is BatchPaymentsPublic { } else if (i == k) { // set the path, and get the associated rate and decimals rPaths[i].path = rI.path; - (rPaths[i].rate, rPaths[i].decimals) = getRate(rI.path, rI.maxRateTimespan); + (rPaths[i].rate, rPaths[i].decimals) = getRateAndDecimals(rI.path, rI.maxRateTimespan); amountAndFeeToPay = amountAndFeeConversion( rI.requestAmount, rI.feeAmount, @@ -320,7 +318,7 @@ contract BatchConversionPayments is BatchPaymentsPublic { require(address(this).balance >= amountAndFeeToPay, 'not enough funds'); // Batch contract pays the requests through EthConversionProxy - conversionPaymentEthProxy.transferWithReferenceAndFee{value: amountAndFeeToPay}( + paymentEthConversionProxy.transferWithReferenceAndFee{value: amountAndFeeToPay}( payable(rI.recipient), rI.requestAmount, rI.path, @@ -365,8 +363,8 @@ contract BatchConversionPayments is BatchPaymentsPublic { /** * @notice Get conversion rate and decimals from chainlink */ - function getRate(address[] memory _path, uint256 _maxRateTimespan) - internal + function getRateAndDecimals(address[] memory _path, uint256 _maxRateTimespan) + private view returns (uint256, uint256) { @@ -383,24 +381,41 @@ contract BatchConversionPayments is BatchPaymentsPublic { } /* - * Admin functions to edit the conversion proxies address + * Admin functions to edit the conversion proxies address and fees */ - /** fees applied on a single request */ + /** + * @notice Fees applied for basic invoice, 0.1% at Request Finance + * @param _basicFee Between 0 and 10000, e.i: basicFee = 10 represent 0.10% of fees + * Update it cautiously. + * e.i: Only if the Request Finance 'basicFee' has evolve, which should be exceptional + */ function setBasicFee(uint256 _basicFee) external onlyOwner { basicFee = _basicFee; } + /** + * @notice fees added when using Erc20/Eth conversion batch functions + * @param _batchConversionFee between 0 and 10000, i.e: batchFee = 50 represent 0.50% of fees + */ function setBatchConversionFee(uint256 _batchConversionFee) external onlyOwner { batchConversionFee = _batchConversionFee; } - function setConversionPaymentProxy(address _paymentErc20ConversionFeeProxy) external onlyOwner { - conversionPaymentProxy = IERC20ConversionProxy(_paymentErc20ConversionFeeProxy); + /** + * @param _paymentErc20ConversionProxy The address of the ERC20 Conversion payment proxy to use. + * Update cautiously, the proxy has to match the invoice proxy. + */ + function setPaymentErc20ConversionProxy(address _paymentErc20ConversionProxy) external onlyOwner { + paymentErc20ConversionProxy = IERC20ConversionProxy(_paymentErc20ConversionProxy); } - function setEthConversionPaymentProxy(address _paymentEthConversionFeeProxy) external onlyOwner { - conversionPaymentEthProxy = IEthConversionProxy(_paymentEthConversionFeeProxy); + /** + * @param _paymentEthConversionProxy The address of the Ethereum Conversion payment proxy to use. + * Update cautiously, the proxy has to match the invoice proxy. + */ + function setPaymentEthConversionProxy(address _paymentEthConversionProxy) external onlyOwner { + paymentEthConversionProxy = IEthConversionProxy(_paymentEthConversionProxy); } /** diff --git a/packages/smart-contracts/src/contracts/BatchPaymentsPublic.sol b/packages/smart-contracts/src/contracts/BatchPaymentsPublic.sol index 9d56851e5..8745e4276 100644 --- a/packages/smart-contracts/src/contracts/BatchPaymentsPublic.sol +++ b/packages/smart-contracts/src/contracts/BatchPaymentsPublic.sol @@ -22,10 +22,9 @@ import './interfaces/EthereumFeeProxy.sol'; contract BatchPaymentsPublic is Ownable { using SafeERC20 for IERC20; - IERC20FeeProxy public paymentErc20FeeProxy; - IEthereumFeeProxy public paymentEthFeeProxy; + IERC20FeeProxy public paymentErc20Proxy; + IEthereumFeeProxy public paymentEthProxy; - // @dev: Between 0 and 10000, i.e: batchFee = 100 represent 1% of fee uint256 public batchFee; struct Token { @@ -35,17 +34,17 @@ contract BatchPaymentsPublic is Ownable { } /** - * @param _paymentErc20FeeProxy The address to the ERC20 payment proxy to use. - * @param _paymentEthFeeProxy The address to the Ethereum payment proxy to use. + * @param _paymentErc20Proxy The address to the ERC20 fee payment proxy to use. + * @param _paymentEthProxy The address to the Ethereum fee payment proxy to use. * @param _owner Owner of the contract. */ constructor( - address _paymentErc20FeeProxy, - address _paymentEthFeeProxy, + address _paymentErc20Proxy, + address _paymentEthProxy, address _owner ) { - paymentErc20FeeProxy = IERC20FeeProxy(_paymentErc20FeeProxy); - paymentEthFeeProxy = IEthereumFeeProxy(_paymentEthFeeProxy); + paymentErc20Proxy = IERC20FeeProxy(_paymentErc20Proxy); + paymentEthProxy = IEthereumFeeProxy(_paymentEthProxy); transferOwnership(_owner); batchFee = 0; } @@ -88,7 +87,7 @@ contract BatchPaymentsPublic is Ownable { require(address(this).balance >= _amounts[i] + _feeAmounts[i], 'not enough funds'); amount += _amounts[i]; - paymentEthFeeProxy.transferWithReferenceAndFee{value: _amounts[i] + _feeAmounts[i]}( + paymentEthProxy.transferWithReferenceAndFee{value: _amounts[i] + _feeAmounts[i]}( payable(_recipients[i]), _paymentReferences[i], _feeAmounts[i], @@ -156,15 +155,15 @@ contract BatchPaymentsPublic is Ownable { ); // Batch contract approve Erc20FeeProxy to spend the token - if (requestedToken.allowance(address(this), address(paymentErc20FeeProxy)) < amount) { - approvePaymentProxyToSpend(address(requestedToken), address(paymentErc20FeeProxy)); + if (requestedToken.allowance(address(this), address(paymentErc20Proxy)) < amount) { + approvePaymentProxyToSpend(address(requestedToken), address(paymentErc20Proxy)); } // Batch contract pays the requests using Erc20FeeProxy for (uint256 i = 0; i < _recipients.length; i++) { // amount is updated to become the sum of amounts, to calculate batch fee amount amount -= _feeAmounts[i]; - paymentErc20FeeProxy.transferFromWithReferenceAndFee( + paymentErc20Proxy.transferFromWithReferenceAndFee( _tokenAddress, _recipients[i], _amounts[i], @@ -261,10 +260,10 @@ contract BatchPaymentsPublic is Ownable { // Batch contract approves Erc20FeeProxy to spend the token if ( - requestedToken.allowance(address(this), address(paymentErc20FeeProxy)) < + requestedToken.allowance(address(this), address(paymentErc20Proxy)) < uniqueTokens[i].amountAndFee ) { - approvePaymentProxyToSpend(address(requestedToken), address(paymentErc20FeeProxy)); + approvePaymentProxyToSpend(address(requestedToken), address(paymentErc20Proxy)); } // Payer pays batch fee amount @@ -276,7 +275,7 @@ contract BatchPaymentsPublic is Ownable { // Batch contract pays the requests using Erc20FeeProxy for (uint256 i = 0; i < _recipients.length; i++) { - paymentErc20FeeProxy.transferFromWithReferenceAndFee( + paymentErc20Proxy.transferFromWithReferenceAndFee( _tokenAddresses[i], _recipients[i], _amounts[i], @@ -349,18 +348,28 @@ contract BatchPaymentsPublic is Ownable { } /* - * Admin functions to edit the proxies address + * Admin functions to edit the proxies address and fees */ + /** + * @notice fees added when using Erc20/Eth batch functions + * @param _batchFee between 0 and 10000, i.e: batchFee = 50 represent 0.50% of fee + */ function setBatchFee(uint256 _batchFee) external onlyOwner { batchFee = _batchFee; } - function setPaymentErc20FeeProxy(address _paymentErc20FeeProxy) external onlyOwner { - paymentErc20FeeProxy = IERC20FeeProxy(_paymentErc20FeeProxy); + /** + * @param _paymentErc20Proxy The address to the Erc20 fee payment proxy to use. + */ + function setPaymentErc20Proxy(address _paymentErc20Proxy) external onlyOwner { + paymentErc20Proxy = IERC20FeeProxy(_paymentErc20Proxy); } - function setPaymentEthFeeProxy(address _paymentEthFeeProxy) external onlyOwner { - paymentEthFeeProxy = IEthereumFeeProxy(_paymentEthFeeProxy); + /** + * @param _paymentEthProxy The address to the Ethereum fee payment proxy to use. + */ + function setPaymentEthProxy(address _paymentEthProxy) external onlyOwner { + paymentEthProxy = IEthereumFeeProxy(_paymentEthProxy); } } diff --git a/packages/smart-contracts/src/lib/artifacts/BatchConversionPayments/0.1.0.json b/packages/smart-contracts/src/lib/artifacts/BatchConversionPayments/0.1.0.json index 840f1bd28..d0347a903 100644 --- a/packages/smart-contracts/src/lib/artifacts/BatchConversionPayments/0.1.0.json +++ b/packages/smart-contracts/src/lib/artifacts/BatchConversionPayments/0.1.0.json @@ -4,17 +4,17 @@ "inputs": [ { "internalType": "address", - "name": "_paymentErc20FeeProxy", + "name": "_paymentErc20Proxy", "type": "address" }, { "internalType": "address", - "name": "_paymentEthFeeProxy", + "name": "_paymentEthProxy", "type": "address" }, { "internalType": "address", - "name": "_paymentErc20ConversionFeeProxy", + "name": "_paymentErc20ConversionProxy", "type": "address" }, { @@ -55,19 +55,6 @@ "name": "OwnershipTransferred", "type": "event" }, - { - "inputs": [ - { - "internalType": "address", - "name": "_erc20Address", - "type": "address" - } - ], - "name": "approveConversionPaymentProxyToSpend", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, { "inputs": [], "name": "basicFee", @@ -100,37 +87,37 @@ "components": [ { "internalType": "address", - "name": "_recipient", + "name": "recipient", "type": "address" }, { "internalType": "uint256", - "name": "_requestAmount", + "name": "requestAmount", "type": "uint256" }, { "internalType": "address[]", - "name": "_path", + "name": "path", "type": "address[]" }, { "internalType": "bytes", - "name": "_paymentReference", + "name": "paymentReference", "type": "bytes" }, { "internalType": "uint256", - "name": "_feeAmount", + "name": "feeAmount", "type": "uint256" }, { "internalType": "uint256", - "name": "_maxToSpend", + "name": "maxToSpend", "type": "uint256" }, { "internalType": "uint256", - "name": "_maxRateTimespan", + "name": "maxRateTimespan", "type": "uint256" } ], @@ -231,37 +218,37 @@ "components": [ { "internalType": "address", - "name": "_recipient", + "name": "recipient", "type": "address" }, { "internalType": "uint256", - "name": "_requestAmount", + "name": "requestAmount", "type": "uint256" }, { "internalType": "address[]", - "name": "_path", + "name": "path", "type": "address[]" }, { "internalType": "bytes", - "name": "_paymentReference", + "name": "paymentReference", "type": "bytes" }, { "internalType": "uint256", - "name": "_feeAmount", + "name": "feeAmount", "type": "uint256" }, { "internalType": "uint256", - "name": "_maxToSpend", + "name": "maxToSpend", "type": "uint256" }, { "internalType": "uint256", - "name": "_maxRateTimespan", + "name": "maxRateTimespan", "type": "uint256" } ], @@ -339,37 +326,37 @@ "components": [ { "internalType": "address", - "name": "_recipient", + "name": "recipient", "type": "address" }, { "internalType": "uint256", - "name": "_requestAmount", + "name": "requestAmount", "type": "uint256" }, { "internalType": "address[]", - "name": "_path", + "name": "path", "type": "address[]" }, { "internalType": "bytes", - "name": "_paymentReference", + "name": "paymentReference", "type": "bytes" }, { "internalType": "uint256", - "name": "_feeAmount", + "name": "feeAmount", "type": "uint256" }, { "internalType": "uint256", - "name": "_maxToSpend", + "name": "maxToSpend", "type": "uint256" }, { "internalType": "uint256", - "name": "_maxRateTimespan", + "name": "maxRateTimespan", "type": "uint256" } ], @@ -381,27 +368,27 @@ "components": [ { "internalType": "address[]", - "name": "_tokenAddresses", + "name": "tokenAddresses", "type": "address[]" }, { "internalType": "address[]", - "name": "_recipients", + "name": "recipients", "type": "address[]" }, { "internalType": "uint256[]", - "name": "_amounts", + "name": "amounts", "type": "uint256[]" }, { "internalType": "bytes[]", - "name": "_paymentReferences", + "name": "paymentReferences", "type": "bytes[]" }, { "internalType": "uint256[]", - "name": "_feeAmounts", + "name": "feeAmounts", "type": "uint256[]" } ], @@ -453,7 +440,7 @@ }, { "inputs": [], - "name": "paymentErc20FeeProxy", + "name": "paymentErc20Proxy", "outputs": [ { "internalType": "contract IERC20FeeProxy", @@ -466,7 +453,7 @@ }, { "inputs": [], - "name": "paymentEthFeeProxy", + "name": "paymentEthProxy", "outputs": [ { "internalType": "contract IEthereumFeeProxy", @@ -540,11 +527,11 @@ "inputs": [ { "internalType": "address", - "name": "_paymentErc20ConversionFeeProxy", + "name": "_paymentErc20ConversionProxy", "type": "address" } ], - "name": "setConversionPaymentProxy", + "name": "setPaymentErc20ConversionProxy", "outputs": [], "stateMutability": "nonpayable", "type": "function" @@ -553,11 +540,11 @@ "inputs": [ { "internalType": "address", - "name": "_paymentEthConversionFeeProxy", + "name": "_paymentErc20Proxy", "type": "address" } ], - "name": "setEthConversionPaymentProxy", + "name": "setPaymentErc20Proxy", "outputs": [], "stateMutability": "nonpayable", "type": "function" @@ -566,11 +553,11 @@ "inputs": [ { "internalType": "address", - "name": "_paymentErc20FeeProxy", + "name": "_paymentEthConversionProxy", "type": "address" } ], - "name": "setPaymentErc20FeeProxy", + "name": "setPaymentEthConversionProxy", "outputs": [], "stateMutability": "nonpayable", "type": "function" @@ -579,11 +566,11 @@ "inputs": [ { "internalType": "address", - "name": "_paymentEthFeeProxy", + "name": "_paymentEthProxy", "type": "address" } ], - "name": "setPaymentEthFeeProxy", + "name": "setPaymentEthProxy", "outputs": [], "stateMutability": "nonpayable", "type": "function" diff --git a/packages/smart-contracts/test/contracts/BatchConversionPayments.test.ts b/packages/smart-contracts/test/contracts/BatchConversionPayments.test.ts index 872c2010e..1ee7828f9 100644 --- a/packages/smart-contracts/test/contracts/BatchConversionPayments.test.ts +++ b/packages/smart-contracts/test/contracts/BatchConversionPayments.test.ts @@ -82,22 +82,22 @@ describe('contract: BatchErc20ConversionPayments', () => { let feeDiffBalanceExpected: BigNumber; type RequestInfo = { - _recipient: string; - _requestAmount: BigNumberish; - _path: string[]; - _paymentReference: BytesLike; - _feeAmount: BigNumberish; - _maxToSpend: BigNumberish; - _maxRateTimespan: BigNumberish; + recipient: string; + requestAmount: BigNumberish; + path: string[]; + paymentReference: BytesLike; + feeAmount: BigNumberish; + maxToSpend: BigNumberish; + maxRateTimespan: BigNumberish; }; let requestInfo: RequestInfo; let requestsInfoParent1 = { - _tokenAddresses: [], - _recipients: [], - _amounts: [], - _paymentReferences: [], - _feeAmounts: [], + tokenAddresses: [], + recipients: [], + amounts: [], + paymentReferences: [], + feeAmounts: [], }; /** Function used to emit events of batch conversion proxy */ let emitOneTx: Function; @@ -166,13 +166,13 @@ describe('contract: BatchErc20ConversionPayments', () => { conversionToPay = await _chainlinkPath.getConversion(_requestAmount, _path); conversionFees = await _chainlinkPath.getConversion(_feeAmount, _path); requestInfo = { - _recipient: _recipient, - _requestAmount: _requestAmount, - _path: _path, - _paymentReference: referenceExample, - _feeAmount: _feeAmount, - _maxToSpend: conversionToPay.result.add(conversionFees.result).toString(), - _maxRateTimespan: _maxRateTimespan, + recipient: _recipient, + requestAmount: _requestAmount, + path: _path, + paymentReference: referenceExample, + feeAmount: _feeAmount, + maxToSpend: conversionToPay.result.add(conversionFees.result).toString(), + maxRateTimespan: _maxRateTimespan, }; }; @@ -273,10 +273,10 @@ describe('contract: BatchErc20ConversionPayments', () => { let requestInfo2 = Utils.deepCopy(requestInfo); - requestInfo2._path = path2; - requestInfo2._requestAmount = amountInFiat2; - requestInfo2._feeAmount = feesAmountInFiat2; - requestInfo2._maxToSpend = conversionToPay2.result.add(conversionFees2.result).toString(); + requestInfo2.path = path2; + requestInfo2.requestAmount = amountInFiat2; + requestInfo2.feeAmount = feesAmountInFiat2; + requestInfo2.maxToSpend = conversionToPay2.result.add(conversionFees2.result).toString(); let requestInfos: RequestInfo[] = []; let conversionsToPay: ConvToPay[] = []; @@ -293,8 +293,8 @@ describe('contract: BatchErc20ConversionPayments', () => { } if ( - requestInfo._path[requestInfo._path.length - 1] === - requestInfo2._path[requestInfo2._path.length - 1] + requestInfo.path[requestInfo.path.length - 1] === + requestInfo2.path[requestInfo2.path.length - 1] ) { for (let i = 0; i < nTimes - 1; i++) { calculERC20Balances(conversionToPay.result, conversionFees.result, []); @@ -331,16 +331,16 @@ describe('contract: BatchErc20ConversionPayments', () => { return result.to .emit(testErc20ConversionProxy, 'TransferWithConversionAndReference') .withArgs( - requestInfo._requestAmount, - ethers.utils.getAddress(requestInfo._path[0]), + requestInfo.requestAmount, + ethers.utils.getAddress(requestInfo.path[0]), ethers.utils.keccak256(referenceExample), - requestInfo._feeAmount, + requestInfo.feeAmount, '0', ) .to.emit(testErc20ConversionProxy, 'TransferWithReferenceAndFee') .withArgs( ethers.utils.getAddress(DAI_address), - ethers.utils.getAddress(requestInfo._recipient), + ethers.utils.getAddress(requestInfo.recipient), _conversionToPay.result, ethers.utils.keccak256(referenceExample), _conversionFees.result, @@ -402,14 +402,14 @@ describe('contract: BatchErc20ConversionPayments', () => { describe('batchERC20ConversionPaymentsMultiTokens with errors', () => { it('cannot transfer with invalid path', async function () { const wrongPath = [EUR_hash, ETH_hash, DAI_address]; - requestInfo._path = wrongPath; + requestInfo.path = wrongPath; await expect(batchConvFunction(argTemplate([requestInfo]), feeAddress)).to.be.revertedWith( 'revert No aggregator found', ); }); it('cannot transfer if max to spend too low', async function () { - requestInfo._maxToSpend = conversionToPay.result + requestInfo.maxToSpend = conversionToPay.result .add(conversionFees.result) .sub(1) .toString(); @@ -419,7 +419,7 @@ describe('contract: BatchErc20ConversionPayments', () => { }); it('cannot transfer if rate is too old', async function () { - requestInfo._maxRateTimespan = 10; + requestInfo.maxRateTimespan = 10; await expect(batchConvFunction(argTemplate([requestInfo]), feeAddress)).to.be.revertedWith( 'aggregator rate is outdated', @@ -444,11 +444,11 @@ describe('contract: BatchErc20ConversionPayments', () => { paymentNetworkId: subFunction === 'batchERC20PaymentsWithReference' ? 1 : 2, requestsInfo: [], requestsInfoParent: { - _tokenAddresses: [tokenAddress], - _recipients: [to], - _amounts: [amount], - _paymentReferences: [referenceExample], - _feeAmounts: [feeAmount], + tokenAddresses: [tokenAddress], + recipients: [to], + amounts: [amount], + paymentReferences: [referenceExample], + feeAmounts: [feeAmount], }, }, ], @@ -495,7 +495,7 @@ describe('contract: BatchErc20ConversionPayments', () => { it('batchERC20PaymentsWithReference transfers token', async function () { await batchERC20Payments(false, 'batchERC20PaymentsWithReference'); }); - it('with batchRouter, batchERC20PaymentsMultiTokensWithReference transfers token', async function () { + it('with batchRouter, batchERC20PaymentsWithReference transfers token', async function () { await batchERC20Payments(true, 'batchERC20PaymentsWithReference'); }); @@ -542,13 +542,13 @@ describe('contract: BatchErc20ConversionPayments', () => { } requestInfo = { - _recipient: to, - _requestAmount: amount, - _path: pathUsdEth, - _paymentReference: referenceExample, - _feeAmount: feeAmount, - _maxToSpend: BigNumber.from(0), - _maxRateTimespan: BigNumber.from(0), + recipient: to, + requestAmount: amount, + path: pathUsdEth, + paymentReference: referenceExample, + feeAmount: feeAmount, + maxToSpend: BigNumber.from(0), + maxRateTimespan: BigNumber.from(0), }; }); @@ -558,21 +558,21 @@ describe('contract: BatchErc20ConversionPayments', () => { beforeEthBalanceFee = await provider.getBalance(feeAddress); beforeEthBalance = await provider.getBalance(await signer.getAddress()); requestInfo = { - _recipient: to, - _requestAmount: amount, - _path: pathUsdEth, - _paymentReference: referenceExample, - _feeAmount: feeAmount, - _maxToSpend: BigNumber.from(0), - _maxRateTimespan: BigNumber.from(0), + recipient: to, + requestAmount: amount, + path: pathUsdEth, + paymentReference: referenceExample, + feeAmount: feeAmount, + maxToSpend: BigNumber.from(0), + maxRateTimespan: BigNumber.from(0), }; // basic setup: 1 payment conversionToPay = await chainlinkPath.getConversion( - requestInfo._requestAmount, - requestInfo._path, + requestInfo.requestAmount, + requestInfo.path, ); - feesToPay = await chainlinkPath.getConversion(requestInfo._feeAmount, requestInfo._path); + feesToPay = await chainlinkPath.getConversion(requestInfo.feeAmount, requestInfo.path); amountToPayExpected = conversionToPay.result; feeToPayExpected = feesToPay.result; @@ -621,15 +621,15 @@ describe('contract: BatchErc20ConversionPayments', () => { it('batchEthConversionPaymentsWithReference transfer 3 payments in ethers denominated in USD and EUR', async function () { const EurRequestInfo = Utils.deepCopy(requestInfo); - EurRequestInfo._path = [EUR_hash, USD_hash, ETH_hash]; + EurRequestInfo.path = [EUR_hash, USD_hash, ETH_hash]; const eurConversionToPay = await chainlinkPath.getConversion( - EurRequestInfo._requestAmount, - EurRequestInfo._path, + EurRequestInfo.requestAmount, + EurRequestInfo.path, ); const eurFeesToPay = await chainlinkPath.getConversion( - EurRequestInfo._feeAmount, - EurRequestInfo._path, + EurRequestInfo.feeAmount, + EurRequestInfo.path, ); amountToPayExpected = eurConversionToPay.result.add(amountToPayExpected.mul(2)); From 180063213384b3339d1175f28bb28307db657e17 Mon Sep 17 00:00:00 2001 From: olivier7delf <55892112+olivier7delf@users.noreply.github.com> Date: Thu, 4 Aug 2022 10:37:22 +0200 Subject: [PATCH 010/105] batchConv - delete chainlink implementation --- .../src/contracts/BatchConversionPayments.sol | 106 ++++-------------- .../src/contracts/BatchPaymentsPublic.sol | 8 +- .../contracts/BatchConversionPayments.test.ts | 5 +- 3 files changed, 25 insertions(+), 94 deletions(-) diff --git a/packages/smart-contracts/src/contracts/BatchConversionPayments.sol b/packages/smart-contracts/src/contracts/BatchConversionPayments.sol index dd3c96585..cd51f2399 100644 --- a/packages/smart-contracts/src/contracts/BatchConversionPayments.sol +++ b/packages/smart-contracts/src/contracts/BatchConversionPayments.sol @@ -76,12 +76,6 @@ contract BatchConversionPayments is BatchPaymentsPublic { RequestsInfoParent requestsInfoParent; } - struct Path { - address[] path; - uint256 rate; - uint256 decimals; - } - /** * @param _paymentErc20Proxy The address to the ERC20 fee payment proxy to use. * @param _paymentEthProxy The address to the Ethereum fee payment proxy to use. @@ -111,6 +105,12 @@ contract BatchConversionPayments is BatchPaymentsPublic { batchConversionFee = 0; } + // batch Eth requires batch contract to receive funds from ethFeeProxy with a value = 0 + // and also from paymentEthConversionProxy with a value > 0 + receive() external payable { + require(address(msg.sender) == address(paymentEthConversionProxy) || msg.value == 0, 'Non-payable'); + } + /** * @notice Batch payments on different payment networks at once. * - batchERC20ConversionPaymentsMultiTokens, paymentNetworks: 0 @@ -277,55 +277,27 @@ contract BatchConversionPayments is BatchPaymentsPublic { * _maxToSpend is not used in this function. * @param _feeAddress The fee recipient. * @dev It uses EthereumConversionProxy to pay an invoice and fees. + * Please: + * Notice that if there is not enough ether sent to the contract, + * it emit the follow error: "revert paymentProxy transferExactEthWithReferenceAndFee failed" + * This choice reduces the gas significantly, otherwise, it would be necessary to make multiple calls to chainlink.. */ function batchEthConversionPaymentsWithReference( RequestInfo[] calldata requestsInfo, address payable _feeAddress ) public payable { uint256 contractBalance = address(this).balance; - // amountAndFeeToPay in native token (as ETH), is updated at each payment - uint256 amountAndFeeToPay; - // rPaths stores _path, rate, and decimals only once by path - Path[] memory rPaths = new Path[](requestsInfo.length); + // Batch contract pays the requests through EthConversionProxy for (uint256 i = 0; i < requestsInfo.length; i++) { - RequestInfo memory rI = requestsInfo[i]; - for (uint256 k = 0; k < requestsInfo.length; k++) { - // Check if the path is already known - if (rPaths[k].rate > 0 && rPaths[k].path[0] == rI.path[0]) { - // use the already known rate and decimals from path already queried - amountAndFeeToPay = amountAndFeeConversion( - rI.requestAmount, - rI.feeAmount, - rPaths[k].rate, - rPaths[k].decimals - ); - break; - } else if (i == k) { - // set the path, and get the associated rate and decimals - rPaths[i].path = rI.path; - (rPaths[i].rate, rPaths[i].decimals) = getRateAndDecimals(rI.path, rI.maxRateTimespan); - amountAndFeeToPay = amountAndFeeConversion( - rI.requestAmount, - rI.feeAmount, - rPaths[i].rate, - rPaths[i].decimals - ); - break; - } - } - - require(address(this).balance >= amountAndFeeToPay, 'not enough funds'); - - // Batch contract pays the requests through EthConversionProxy - paymentEthConversionProxy.transferWithReferenceAndFee{value: amountAndFeeToPay}( - payable(rI.recipient), - rI.requestAmount, - rI.path, - rI.paymentReference, - rI.feeAmount, + paymentEthConversionProxy.transferWithReferenceAndFee{value: address(this).balance}( + payable(requestsInfo[i].recipient), + requestsInfo[i].requestAmount, + requestsInfo[i].path, + requestsInfo[i].paymentReference, + requestsInfo[i].feeAmount, _feeAddress, - rI.maxRateTimespan + requestsInfo[i].maxRateTimespan ); } @@ -338,46 +310,8 @@ contract BatchConversionPayments is BatchPaymentsPublic { _feeAddress.transfer(amountBatchFees); // Batch contract transfers the remaining ethers to the payer - if (address(this).balance > 0) { - (bool sendBackSuccess, ) = payable(msg.sender).call{value: address(this).balance}(''); - require(sendBackSuccess, 'Could not send remaining funds to the payer'); - } - } - - /* - * Helper functions - */ - - /** - * @notice Calculate the amount of the conversion - */ - function amountAndFeeConversion( - uint256 requestAmount, - uint256 requestFee, - uint256 rate, - uint256 decimals - ) private pure returns (uint256) { - return (requestAmount * rate) / decimals + (requestFee * rate) / decimals; - } - - /** - * @notice Get conversion rate and decimals from chainlink - */ - function getRateAndDecimals(address[] memory _path, uint256 _maxRateTimespan) - private - view - returns (uint256, uint256) - { - (uint256 rate, uint256 oldestTimestampRate, uint256 decimals) = chainlinkConversionPath.getRate( - _path - ); - - // Check rate timespan - require( - _maxRateTimespan == 0 || block.timestamp - oldestTimestampRate <= _maxRateTimespan, - 'aggregator rate is outdated' - ); - return (rate, decimals); + (bool sendBackSuccess, ) = payable(msg.sender).call{value: address(this).balance}(''); + require(sendBackSuccess, 'Could not send remaining funds to the payer'); } /* diff --git a/packages/smart-contracts/src/contracts/BatchPaymentsPublic.sol b/packages/smart-contracts/src/contracts/BatchPaymentsPublic.sol index 8745e4276..d50eb280a 100644 --- a/packages/smart-contracts/src/contracts/BatchPaymentsPublic.sol +++ b/packages/smart-contracts/src/contracts/BatchPaymentsPublic.sol @@ -15,7 +15,8 @@ import './interfaces/EthereumFeeProxy.sol'; * - fees: ERC20 and ETH proxies fees are paid to the same address. * An additional batch fee is paid to the same address. * If one transaction of the batch fail, every transactions are reverted. - * @dev It is a clone of BatchPayment.sol, with two main modifications: + * @dev It is a clone of BatchPayment.sol, with three main modifications: + * - function "receive" is not implemented * - fees are now divided by 10_000 instead of 1_000 in previous version * - batch payment functions are now public, instead of external */ @@ -49,11 +50,6 @@ contract BatchPaymentsPublic is Ownable { batchFee = 0; } - // batch Eth requires batch contract to receive funds from ethFeeProxy - receive() external payable { - require(msg.value == 0, 'Non-payable'); - } - /** * @notice Send a batch of Eth payments w/fees with paymentReferences to multiple accounts. * If one payment failed, the whole batch is reverted diff --git a/packages/smart-contracts/test/contracts/BatchConversionPayments.test.ts b/packages/smart-contracts/test/contracts/BatchConversionPayments.test.ts index 1ee7828f9..fc67d73bc 100644 --- a/packages/smart-contracts/test/contracts/BatchConversionPayments.test.ts +++ b/packages/smart-contracts/test/contracts/BatchConversionPayments.test.ts @@ -23,7 +23,7 @@ import Utils from '@requestnetwork/utils'; import { HttpNetworkConfig } from 'hardhat/types'; // set to true to log batch payments's gas consumption -const logGas = false; +const logGas = true; describe('contract: BatchErc20ConversionPayments', () => { const networkConfig = network.config as HttpNetworkConfig; @@ -125,6 +125,7 @@ describe('contract: BatchErc20ConversionPayments', () => { chainlinkPath.address, await signer.getAddress(), ); + console.log('testErc20ConversionProxy', testErc20ConversionProxy.address); testEthConversionProxy = await new EthConversionProxy__factory(signer).deploy( ethereumFeeProxy.address, chainlinkPath.address, @@ -642,7 +643,7 @@ describe('contract: BatchErc20ConversionPayments', () => { batchConvFunction(getInputs([requestInfo]), feeAddress, { value: 10000, }), - ).to.be.revertedWith('not enough funds'); + ).to.be.revertedWith('paymentProxy transferExactEthWithReferenceAndFee failed'); }); }); }; From 1eb224d3e3cc98894157a0d05754f94b30a0c1b2 Mon Sep 17 00:00:00 2001 From: olivier7delf <55892112+olivier7delf@users.noreply.github.com> Date: Thu, 4 Aug 2022 11:11:14 +0200 Subject: [PATCH 011/105] batchConv erc20 - delete a require - add error tests --- .../src/contracts/BatchConversionPayments.sol | 3 +- .../contracts/BatchConversionPayments.test.ts | 49 ++++++++++++++++--- 2 files changed, 44 insertions(+), 8 deletions(-) diff --git a/packages/smart-contracts/src/contracts/BatchConversionPayments.sol b/packages/smart-contracts/src/contracts/BatchConversionPayments.sol index cd51f2399..5932d1a3a 100644 --- a/packages/smart-contracts/src/contracts/BatchConversionPayments.sol +++ b/packages/smart-contracts/src/contracts/BatchConversionPayments.sol @@ -210,10 +210,9 @@ contract BatchConversionPayments is BatchPaymentsPublic { requestedToken.allowance(msg.sender, address(this)) >= uTokens[k].amountAndFee, 'Not sufficient allowance for batch to pay' ); - require(requestedToken.balanceOf(msg.sender) >= uTokens[k].amountAndFee, 'not enough funds'); require( requestedToken.balanceOf(msg.sender) >= uTokens[k].amountAndFee + uTokens[k].batchFeeAmount, - 'not enough funds to pay approximated batchConversionFee' + 'not enough funds, including fees' ); // Transfer the amount and fee required for the token on the batch conversion contract diff --git a/packages/smart-contracts/test/contracts/BatchConversionPayments.test.ts b/packages/smart-contracts/test/contracts/BatchConversionPayments.test.ts index fc67d73bc..1cb123982 100644 --- a/packages/smart-contracts/test/contracts/BatchConversionPayments.test.ts +++ b/packages/smart-contracts/test/contracts/BatchConversionPayments.test.ts @@ -34,6 +34,7 @@ describe('contract: BatchErc20ConversionPayments', () => { let feeAddress: string; let batchAddress: string; let signer: Signer; + let toSigner: Signer; const basicFee = 10; const batchFee = 100; const batchConvFee = 100; @@ -114,9 +115,10 @@ describe('contract: BatchErc20ConversionPayments', () => { let argTemplate: Function; before(async () => { + let _; [from, to, feeAddress] = (await ethers.getSigners()).map((s) => s.address); - [signer] = await ethers.getSigners(); - + [signer, _, _, toSigner] = await ethers.getSigners(); + _; chainlinkPath = chainlinkConversionPath.connect(network.name, signer); erc20FeeProxy = await new ERC20FeeProxy__factory(signer).deploy(); ethereumFeeProxy = await new EthereumFeeProxy__factory(signer).deploy(); @@ -353,9 +355,10 @@ describe('contract: BatchErc20ConversionPayments', () => { path = [USD_hash, DAI_address]; initConvToPayAndRequestInfo(to, path, amountInFiat, feesAmountInFiat, 0, chainlinkPath); }); - before(() => { + + const setBatchConvFunction = async (_signer: Signer) => { if (suiteName === 'batchRouter') { - batchConvFunction = testBatchConversionProxy.batchRouter; + batchConvFunction = testBatchConversionProxy.connect(_signer).batchRouter; argTemplate = (requestInfos: RequestInfo[]) => { return [ { @@ -367,11 +370,15 @@ describe('contract: BatchErc20ConversionPayments', () => { }; } if (suiteName === 'batchERC20ConversionPaymentsMultiTokens') { - batchConvFunction = testBatchConversionProxy.batchERC20ConversionPaymentsMultiTokens; + batchConvFunction = + testBatchConversionProxy.connect(_signer).batchERC20ConversionPaymentsMultiTokens; argTemplate = (requestInfos: RequestInfo[]) => { return requestInfos; }; } + }; + before(() => { + setBatchConvFunction(signer); }); describe(suiteName, () => { describe('batchERC20ConversionPaymentsMultiTokens with DAI', async () => { @@ -393,7 +400,7 @@ describe('contract: BatchErc20ConversionPayments', () => { const path2 = [EUR_hash, USD_hash, DAI_address]; await twoTransferOneTokenConv(path2, 1); }); - it('TMP allows to transfer two kind of tokens for USD - GAS', async function () { + it('allows to transfer two kinds of tokens for USD - GAS', async function () { const path2 = [USD_hash, fakeFAU_address]; await twoTransferOneTokenConv(path2, 1); }); @@ -426,6 +433,36 @@ describe('contract: BatchErc20ConversionPayments', () => { 'aggregator rate is outdated', ); }); + + it('Not enough allowance', async function () { + // toSigner connect to the batch function + setBatchConvFunction(toSigner); + await expect(batchConvFunction(argTemplate([requestInfo]), feeAddress)).to.be.revertedWith( + 'Not sufficient allowance for batch to pay', + ); + // reset: signer connect to the batch function + setBatchConvFunction(signer); + }); + + it('Not enough funds', async function () { + // increase toSigner allowance + await testERC20 + .connect(toSigner) + .approve(testBatchConversionProxy.address, thousandWith18Decimal); + // toSigner connect to the batch function + setBatchConvFunction(toSigner); + + await expect(batchConvFunction(argTemplate([requestInfo]), feeAddress)).to.be.revertedWith( + 'not enough funds, including fees', + ); + + // reset: + // - decrease toSigner allowance + // - connect with signer account + await testERC20.connect(toSigner).approve(testBatchConversionProxy.address, '0'); + testERC20.connect(signer); + setBatchConvFunction(signer); + }); }); }; From f8c274564c89ef0a67e54958b236f45e1a698b80 Mon Sep 17 00:00:00 2001 From: olivier7delf <55892112+olivier7delf@users.noreply.github.com> Date: Thu, 4 Aug 2022 11:13:38 +0200 Subject: [PATCH 012/105] prettier contract --- .../src/contracts/BatchConversionPayments.sol | 5 ++++- .../test/contracts/BatchConversionPayments.test.ts | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/smart-contracts/src/contracts/BatchConversionPayments.sol b/packages/smart-contracts/src/contracts/BatchConversionPayments.sol index 5932d1a3a..fd747be38 100644 --- a/packages/smart-contracts/src/contracts/BatchConversionPayments.sol +++ b/packages/smart-contracts/src/contracts/BatchConversionPayments.sol @@ -108,7 +108,10 @@ contract BatchConversionPayments is BatchPaymentsPublic { // batch Eth requires batch contract to receive funds from ethFeeProxy with a value = 0 // and also from paymentEthConversionProxy with a value > 0 receive() external payable { - require(address(msg.sender) == address(paymentEthConversionProxy) || msg.value == 0, 'Non-payable'); + require( + address(msg.sender) == address(paymentEthConversionProxy) || msg.value == 0, + 'Non-payable' + ); } /** diff --git a/packages/smart-contracts/test/contracts/BatchConversionPayments.test.ts b/packages/smart-contracts/test/contracts/BatchConversionPayments.test.ts index 1cb123982..1b98a5d47 100644 --- a/packages/smart-contracts/test/contracts/BatchConversionPayments.test.ts +++ b/packages/smart-contracts/test/contracts/BatchConversionPayments.test.ts @@ -23,7 +23,7 @@ import Utils from '@requestnetwork/utils'; import { HttpNetworkConfig } from 'hardhat/types'; // set to true to log batch payments's gas consumption -const logGas = true; +const logGas = false; describe('contract: BatchErc20ConversionPayments', () => { const networkConfig = network.config as HttpNetworkConfig; From 84216f7687dfb0e32d085309384c633e98930831 Mon Sep 17 00:00:00 2001 From: Romain <45540622+rom1trt@users.noreply.github.com> Date: Thu, 28 Jul 2022 15:41:16 +0200 Subject: [PATCH 013/105] doc: modify command to create request (#880) * refactor: command to create request * fix: escrow audit fix 2 (#878) * fix(smart-contracts): update batch fees (#873) update batch fees from 1% to .3% * feat: add cancel stream function (#884) * refactor: contract setup compression fix (#888) * fix: escrow audit fix 2 (#878) * fix(smart-contracts): update batch fees (#873) update batch fees from 1% to .3% * feat: add cancel stream function (#884) * refactor: contract setup compression fix (#888) * feat: goerli storage (#890) feat: squash commits goerli storage * fix: delete ETHConversionProxy Co-authored-by: Darko Kolev Co-authored-by: olivier7delf <55892112+olivier7delf@users.noreply.github.com> Co-authored-by: Bertrand Juglas --- packages/toolbox/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/toolbox/README.md b/packages/toolbox/README.md index f86129022..072f779b0 100644 --- a/packages/toolbox/README.md +++ b/packages/toolbox/README.md @@ -27,8 +27,8 @@ CreateRequest.createTestRequest(12); #### In the CLI ```bash -yarn run:create -yarn run:create 12 +yarn request-toolbox request create +yarn request-toolbox request create 12 ``` ### Conversion paths From 73cb822142781d1329ad6ff14eff93c8b7287ff3 Mon Sep 17 00:00:00 2001 From: Darko Kolev Date: Fri, 29 Jul 2022 10:34:03 +0200 Subject: [PATCH 014/105] chore: update escrow addresses (#886) --- .../lib/artifacts/ERC20EscrowToPay/index.ts | 40 +++++-------------- .../src/lib/artifacts/ERC20FeeProxy/index.ts | 4 ++ 2 files changed, 14 insertions(+), 30 deletions(-) diff --git a/packages/smart-contracts/src/lib/artifacts/ERC20EscrowToPay/index.ts b/packages/smart-contracts/src/lib/artifacts/ERC20EscrowToPay/index.ts index c53b7cdb1..7afbf33c0 100644 --- a/packages/smart-contracts/src/lib/artifacts/ERC20EscrowToPay/index.ts +++ b/packages/smart-contracts/src/lib/artifacts/ERC20EscrowToPay/index.ts @@ -14,44 +14,24 @@ export const erc20EscrowToPayArtifact = new ContractArtifact( creationBlockNumber: 0, }, mainnet: { - address: '0xa015c141C02996EcE6410646DA3D07d70091c577', - creationBlockNumber: 14884007, + address: '0x7DfD5955a1Ed6Bf74ccF8e24FF53E0a9A7e9F477', + creationBlockNumber: 15146972, }, rinkeby: { - address: '0xEbe28A2B7336670Ba752bfEad4a121D2c4FF2464', - creationBlockNumber: 10461945, + address: '0x2b487A3251aCC34ae95E4f5aA7fdcD2C7447B42e', + creationBlockNumber: 11028247, }, goerli: { - address: '0xEbe28A2B7336670Ba752bfEad4a121D2c4FF2464', - creationBlockNumber: 10461945, + address: '0xd2777001fD7D89331D8E87eC439f78079179322b', + creationBlockNumber: 7230322, }, matic: { - address: '0xc7f471F5A8f8b33F131049b1e9A43941CbE31792', - creationBlockNumber: 29821569, + address: '0x937Db37ffb67083242fbC6AdD472146bF10E01ec', + creationBlockNumber: 30751595, }, fuse: { - address: '0xa015c141C02996EcE6410646DA3D07d70091c577', - creationBlockNumber: 17328459, - }, - celo: { - address: '0xa015c141C02996EcE6410646DA3D07d70091c577', - creationBlockNumber: 13299808, - }, - xdai: { - address: '0xa015c141C02996EcE6410646DA3D07d70091c577', - creationBlockNumber: 22438806, - }, - 'arbitrum-one': { - address: '0xa015c141C02996EcE6410646DA3D07d70091c577', - creationBlockNumber: 13417262, - }, - fantom: { - address: '0xa015c141C02996EcE6410646DA3D07d70091c577', - creationBlockNumber: 39534777, - }, - bsc: { - address: '0xc7f471F5A8f8b33F131049b1e9A43941CbE31792', - creationBlockNumber: 18877277, + address: '0x4BA012eae4d64da79Bd6bcdBa366803fCe701A4C', + creationBlockNumber: 18086337, }, }, }, diff --git a/packages/smart-contracts/src/lib/artifacts/ERC20FeeProxy/index.ts b/packages/smart-contracts/src/lib/artifacts/ERC20FeeProxy/index.ts index dd64ee33e..dbe994340 100644 --- a/packages/smart-contracts/src/lib/artifacts/ERC20FeeProxy/index.ts +++ b/packages/smart-contracts/src/lib/artifacts/ERC20FeeProxy/index.ts @@ -67,6 +67,10 @@ export const erc20FeeProxyArtifact = new ContractArtifact( address: '0xda46309973bFfDdD5a10cE12c44d2EE266f45A44', creationBlockNumber: 7118080, }, + goerli: { + address: '0x399F5EE127ce7432E4921a61b8CF52b0af52cbfE', + creationBlockNumber: 7091472, + }, matic: { address: '0x0DfbEe143b42B41eFC5A6F87bFD1fFC78c2f0aC9', creationBlockNumber: 17427742, From 3e8763d988f185b43081e695d303262e92f6717a Mon Sep 17 00:00:00 2001 From: Romain <45540622+rom1trt@users.noreply.github.com> Date: Mon, 1 Aug 2022 16:08:21 +0200 Subject: [PATCH 015/105] feat: goerli payment (#892) --- packages/currency/src/aggregators/goerli.json | 8 + .../currency/src/erc20/networks/goerli.ts | 11 ++ .../src/erc20/address-based.ts | 2 +- .../any/any-to-erc20-proxy-contract.test.ts | 68 ++++--- .../test/erc20/address-based.test.ts | 2 +- .../test/erc20/fee-proxy-contract.test.ts | 4 +- .../test/erc20/proxy-contract.test.ts | 4 +- .../payment-detection/test/erc777/mocks.ts | 2 +- .../test/erc777/superfluid-retriever.test.ts | 176 +++++++++--------- .../test/eth/info-retriever.test.ts | 3 +- .../test/eth/input-data.test.ts | 4 +- .../payment-detection/test/provider.test.ts | 33 +++- .../payment-processor/src/payment/utils.ts | 2 +- .../test/payment/utils.test.ts | 9 + .../scripts-create2/compute-one-address.ts | 2 +- .../contract-setup/setupETHConversionProxy.ts | 2 +- .../scripts-create2/contract-setup/setups.ts | 145 +-------------- .../smart-contracts/scripts-create2/deploy.ts | 2 +- .../smart-contracts/scripts-create2/utils.ts | 4 +- .../smart-contracts/scripts-create2/verify.ts | 2 +- .../scripts/conversion-proxy.ts | 2 +- .../scripts/deploy-payments.ts | 14 +- .../scripts/test-deploy_chainlink_contract.ts | 4 +- .../interfaces/IERC20ConversionProxy.sol | 1 + .../ChainlinkConversionPath/index.ts | 4 + .../src/lib/artifacts/ERC20SwapToPay/index.ts | 4 + .../lib/artifacts/EthConversionProxy/index.ts | 4 + .../lib/artifacts/EthereumFeeProxy/index.ts | 4 + .../src/lib/artifacts/EthereumProxy/index.ts | 4 + 29 files changed, 245 insertions(+), 281 deletions(-) create mode 100644 packages/currency/src/aggregators/goerli.json create mode 100644 packages/currency/src/erc20/networks/goerli.ts diff --git a/packages/currency/src/aggregators/goerli.json b/packages/currency/src/aggregators/goerli.json new file mode 100644 index 000000000..93d71cbd2 --- /dev/null +++ b/packages/currency/src/aggregators/goerli.json @@ -0,0 +1,8 @@ +{ + "0xba62bcfcaafc6622853cca2be6ac7d845bc0f2dc": { + "0x775eb53d00dd0acd3ec1696472105d579b9b386b": 1 + }, + "0x775eb53d00dd0acd3ec1696472105d579b9b386b": { + "0xba62bcfcaafc6622853cca2be6ac7d845bc0f2dc": 1 + } +} diff --git a/packages/currency/src/erc20/networks/goerli.ts b/packages/currency/src/erc20/networks/goerli.ts new file mode 100644 index 000000000..badb77bd5 --- /dev/null +++ b/packages/currency/src/erc20/networks/goerli.ts @@ -0,0 +1,11 @@ +import { TokenMap } from './types'; + +// List of the supported goerli ERC20 tokens +export const supportedGoerliERC20: TokenMap = { + // Faucet Token on goerli network. Easy to use on tests. + '0xBA62BCfcAaFc6622853cca2BE6Ac7d845BC0f2Dc': { + decimals: 18, + name: 'Faucet Token', + symbol: 'FAU-goerli', + }, +}; diff --git a/packages/payment-detection/src/erc20/address-based.ts b/packages/payment-detection/src/erc20/address-based.ts index 5bd11e632..4664b3164 100644 --- a/packages/payment-detection/src/erc20/address-based.ts +++ b/packages/payment-detection/src/erc20/address-based.ts @@ -8,7 +8,7 @@ import { BalanceError } from '../balance-error'; import Erc20InfoRetriever from './address-based-info-retriever'; import { PaymentDetectorBase } from '../payment-detector-base'; -const supportedNetworks = ['mainnet', 'rinkeby', 'private']; +const supportedNetworks = ['mainnet', 'rinkeby', 'goerli', 'private']; /** * Handle payment networks with ERC20 based address extension diff --git a/packages/payment-detection/test/any/any-to-erc20-proxy-contract.test.ts b/packages/payment-detection/test/any/any-to-erc20-proxy-contract.test.ts index 1be024d55..f770a7415 100644 --- a/packages/payment-detection/test/any/any-to-erc20-proxy-contract.test.ts +++ b/packages/payment-detection/test/any/any-to-erc20-proxy-contract.test.ts @@ -31,7 +31,7 @@ const mockAdvancedLogic: AdvancedLogicTypes.IAdvancedLogic = { }, extensions: { anyToErc20Proxy: { - supportedNetworks: ['mainnet', 'rinkeby', 'private'], + supportedNetworks: ['mainnet', 'rinkeby', 'goerli', 'private'], createAddPaymentAddressAction, createAddRefundAddressAction, createCreationAction, @@ -56,35 +56,61 @@ describe('api/any/conversion-fee-proxy-contract', () => { jest.clearAllMocks(); }); - it('can createExtensionsDataForCreation', async () => { - await anyToErc20Proxy.createExtensionsDataForCreation({ - paymentAddress: 'ethereum address', - salt: 'ea3bc7caf64110ca', - acceptedTokens: ['ethereum address2'], - network: 'rinkeby', - maxRateTimespan: 1000, + const testSuite = (network: string) => { + it(`can createExtensionsDataForCreation on ${network}`, async () => { + await anyToErc20Proxy.createExtensionsDataForCreation({ + paymentAddress: 'ethereum address', + salt: 'ea3bc7caf64110ca', + acceptedTokens: ['ethereum address2'], + network: network, + maxRateTimespan: 1000, + }); + + expect(createCreationAction).toHaveBeenCalledWith({ + feeAddress: undefined, + feeAmount: undefined, + paymentAddress: 'ethereum address', + refundAddress: undefined, + salt: 'ea3bc7caf64110ca', + acceptedTokens: ['ethereum address2'], + network: network, + maxRateTimespan: 1000, + }); }); - expect(createCreationAction).toHaveBeenCalledWith({ - feeAddress: undefined, - feeAmount: undefined, - paymentAddress: 'ethereum address', - refundAddress: undefined, - salt: 'ea3bc7caf64110ca', - acceptedTokens: ['ethereum address2'], - network: 'rinkeby', - maxRateTimespan: 1000, + it(`can createExtensionsDataForCreation with fee amount and address ${network}`, async () => { + await anyToErc20Proxy.createExtensionsDataForCreation({ + feeAddress: 'fee address', + feeAmount: '2000', + paymentAddress: 'ethereum address', + salt: 'ea3bc7caf64110ca', + acceptedTokens: ['ethereum address2'], + network: network, + }); + + expect(createCreationAction).toHaveBeenCalledWith({ + feeAddress: 'fee address', + feeAmount: '2000', + paymentAddress: 'ethereum address', + refundAddress: undefined, + salt: 'ea3bc7caf64110ca', + acceptedTokens: ['ethereum address2'], + network: network, + }); }); - }); + }; + + testSuite('rinkeby'); + testSuite('goerli'); - it('can createExtensionsDataForCreation with fee amount and address', async () => { + it('can createExtensionsDataForCreation with fee amount and address (Goerli)', async () => { await anyToErc20Proxy.createExtensionsDataForCreation({ feeAddress: 'fee address', feeAmount: '2000', paymentAddress: 'ethereum address', salt: 'ea3bc7caf64110ca', acceptedTokens: ['ethereum address2'], - network: 'rinkeby', + network: 'goerli', }); expect(createCreationAction).toHaveBeenCalledWith({ @@ -94,7 +120,7 @@ describe('api/any/conversion-fee-proxy-contract', () => { refundAddress: undefined, salt: 'ea3bc7caf64110ca', acceptedTokens: ['ethereum address2'], - network: 'rinkeby', + network: 'goerli', }); }); diff --git a/packages/payment-detection/test/erc20/address-based.test.ts b/packages/payment-detection/test/erc20/address-based.test.ts index f75629d33..93fa53302 100644 --- a/packages/payment-detection/test/erc20/address-based.test.ts +++ b/packages/payment-detection/test/erc20/address-based.test.ts @@ -126,7 +126,7 @@ describe('api/erc20/address-based', () => { error: { code: PaymentTypes.BALANCE_ERROR_CODE.NETWORK_NOT_SUPPORTED, message: - 'Payment network wrong not supported by ERC20 payment detection. Supported networks: mainnet, rinkeby, private', + 'Payment network wrong not supported by ERC20 payment detection. Supported networks: mainnet, rinkeby, goerli, private', }, events: [], }); diff --git a/packages/payment-detection/test/erc20/fee-proxy-contract.test.ts b/packages/payment-detection/test/erc20/fee-proxy-contract.test.ts index f7504c9b8..c0d924c21 100644 --- a/packages/payment-detection/test/erc20/fee-proxy-contract.test.ts +++ b/packages/payment-detection/test/erc20/fee-proxy-contract.test.ts @@ -23,7 +23,7 @@ const mockAdvancedLogic: AdvancedLogicTypes.IAdvancedLogic = { }, extensions: { feeProxyContractErc20: { - supportedNetworks: ['mainnet', 'private', 'rinkeby'], + supportedNetworks: ['mainnet', 'private', 'rinkeby', 'goerli'], createAddPaymentAddressAction, createAddRefundAddressAction, createCreationAction, @@ -283,7 +283,7 @@ describe('api/erc20/fee-proxy-contract', () => { ).toBe('7'); }); - it('should have gasFee & gasUsed in the payment eventl', async () => { + it('should have gasFee & gasUsed in the payment event', async () => { const mockRequest: RequestLogicTypes.IRequest = { creator: { type: IdentityTypes.TYPE.ETHEREUM_ADDRESS, value: '0x2' }, currency: { diff --git a/packages/payment-detection/test/erc20/proxy-contract.test.ts b/packages/payment-detection/test/erc20/proxy-contract.test.ts index 5d89bb797..f814c0ae5 100644 --- a/packages/payment-detection/test/erc20/proxy-contract.test.ts +++ b/packages/payment-detection/test/erc20/proxy-contract.test.ts @@ -25,7 +25,7 @@ const mockAdvancedLogic: AdvancedLogicTypes.IAdvancedLogic = { }, extensions: { proxyContractErc20: { - supportedNetworks: ['mainnet', 'rinkeby'], + supportedNetworks: ['mainnet', 'rinkeby', 'goerli'], createAddPaymentAddressAction, createAddRefundAddressAction, createCreationAction, @@ -160,7 +160,7 @@ describe('api/erc20/proxy-contract', () => { error: { code: PaymentTypes.BALANCE_ERROR_CODE.NETWORK_NOT_SUPPORTED, message: - 'Payment network WRONG not supported by pn-erc20-proxy-contract payment detection. Supported networks: mainnet, rinkeby', + 'Payment network WRONG not supported by pn-erc20-proxy-contract payment detection. Supported networks: mainnet, rinkeby, goerli', }, events: [], }); diff --git a/packages/payment-detection/test/erc777/mocks.ts b/packages/payment-detection/test/erc777/mocks.ts index 69565f307..8cfdc6eb5 100644 --- a/packages/payment-detection/test/erc777/mocks.ts +++ b/packages/payment-detection/test/erc777/mocks.ts @@ -76,7 +76,7 @@ const mockFlows = [ }, { transactionHash: '0xe472ca1b52751b058fbdaeaffebd98c0cc43b45aa31794b3eb06834ede19f7be', - blockNumber: '9945543', + blockNumber: 9945543, timestamp: '1641495767', sender: '0x9c040e2d6fd83a8b35069aa7154b69674961e0f7', flowRate: '0', diff --git a/packages/payment-detection/test/erc777/superfluid-retriever.test.ts b/packages/payment-detection/test/erc777/superfluid-retriever.test.ts index 9ea56eb5e..3c72a469a 100644 --- a/packages/payment-detection/test/erc777/superfluid-retriever.test.ts +++ b/packages/payment-detection/test/erc777/superfluid-retriever.test.ts @@ -7,106 +7,108 @@ import { mockSuperfluidSubgraph } from './mocks'; jest.mock('graphql-request'); const graphql = mocked(GraphQLClient.prototype); +const fDAIxTokenRinkeby = '0x745861aed1eee363b4aaa5f1994be40b1e05ff90'; +const fUSDCxTokenRinkeby = '0x0f1d7c55a2b133e000ea10eec03c774e0d6796e8'; +const fDAIxTokenGoerli = '0x2bf02814ea0b2b155ed47b7cede18caa752940e6'; +const fUSDCxTokenGoerli = '0x2bf02814ea0b2b155ed47b7cede18caa752940e6'; -describe('api/erc777/superfluid-info-retriever', () => { - describe('on untagged requests', () => { - it('should get payment events from SuperFluid via subgraph with 1 request', async () => { - const paymentData = { - reference: '0xbeefaccc470c7dbd54de69', - txHash: '0xe472ca1b52751b058fbdaeaffebd98c0cc43b45aa31794b3eb06834ede19f7be', - from: '0x9c040e2d6fd83a8b35069aa7154b69674961e0f7', - to: '0x52e5bcfa46393894afcfe6cd98a6761fa692c594', - network: 'rinkeby', - salt: '0ee84db293a752c6', - amount: '92592592592592000', - requestId: '0188791633ff0ec72a7dbdefb886d2db6cccfa98287320839c2f173c7a4e3ce7e1', - block: 9945543, - token: '0x745861aed1eee363b4aaa5f1994be40b1e05ff90', //fDAIx - }; - graphql.request.mockResolvedValue(mockSuperfluidSubgraph[0]); +const testSuiteWithDaix = (network: string, fDAIxToken: string) => { + describe('api/erc777/superfluid-info-retriever', () => { + describe('on untagged requests', () => { + it(`should get payment events from SuperFluid via subgraph with 1 request on ${network}`, async () => { + const paymentData = { + reference: '0xbeefaccc470c7dbd54de69', + txHash: '0xe472ca1b52751b058fbdaeaffebd98c0cc43b45aa31794b3eb06834ede19f7be', + from: '0x9c040e2d6fd83a8b35069aa7154b69674961e0f7', + to: '0x52e5bcfa46393894afcfe6cd98a6761fa692c594', + network: network, + salt: '0ee84db293a752c6', + amount: '92592592592592000', + requestId: '0188791633ff0ec72a7dbdefb886d2db6cccfa98287320839c2f173c7a4e3ce7e1', + block: 9945543, + token: fDAIxToken, + }; + graphql.request.mockResolvedValue(mockSuperfluidSubgraph[0]); - const paymentReference = PaymentReferenceCalculator.calculate( - paymentData.requestId, - paymentData.salt, - paymentData.to, - ); - const subgraphReference = `0xbeefac${paymentReference}`; - expect(subgraphReference).toEqual(paymentData.reference); + const paymentReference = PaymentReferenceCalculator.calculate( + paymentData.requestId, + paymentData.salt, + paymentData.to, + ); + const subgraphReference = `0xbeefac${paymentReference}`; + expect(subgraphReference).toEqual(paymentData.reference); - const graphRetriever = new SuperFluidInfoRetriever( - paymentReference, - paymentData.token, - paymentData.to, - PaymentTypes.EVENTS_NAMES.PAYMENT, - paymentData.network, - ); - const transferEvents = await graphRetriever.getTransferEvents(); - expect(transferEvents).toHaveLength(5); - expect(transferEvents[0].amount).toEqual(paymentData.amount); - expect(transferEvents[0].name).toEqual('payment'); - expect(transferEvents[0].parameters?.to).toEqual(paymentData.to); - expect(transferEvents[1].amount).toEqual('34722222222222000'); - expect(transferEvents[2].amount).toEqual('40509259259259000'); - expect(transferEvents[0].parameters?.txHash).toEqual(paymentData.txHash); - expect(transferEvents[0].parameters?.block).toEqual(paymentData.block); + const graphRetriever = new SuperFluidInfoRetriever( + paymentReference, + paymentData.token, + paymentData.to, + PaymentTypes.EVENTS_NAMES.PAYMENT, + paymentData.network, + ); + const transferEvents = await graphRetriever.getTransferEvents(); + expect(transferEvents).toHaveLength(5); + expect(transferEvents[0].amount).toEqual(paymentData.amount); + expect(transferEvents[0].name).toEqual('payment'); + expect(transferEvents[0].parameters?.to).toEqual(paymentData.to); + expect(transferEvents[1].amount).toEqual('34722222222222000'); + expect(transferEvents[2].amount).toEqual('40509259259259000'); + expect(transferEvents[0].parameters?.txHash).toEqual(paymentData.txHash); + expect(transferEvents[0].parameters?.block).toEqual(paymentData.block); + }); }); - }); - describe('on 2 nested requests', () => { - it('should get payment event from SuperFluid via subgraph with 2 requests', async () => { - const paymentData = { - reference: '0xbeefac9474ad7670909da5', - from: '0x9c040e2d6fd83a8b35069aa7154b69674961e0f7', - to: '0x52e5bcfa46393894afcfe6cd98a6761fa692c594', - network: 'rinkeby', - salt: '0ee84db293a752c6', - amount: '320833333333331260', - // = (1642693617 - 1642692777 = 840 sec) x (385802469135800 - 3858024691358 = 381944444444442 Wei DAIx / sec) - requestId: '0288792633ff0ec72a7dbdefb886d2db6cccfa98287320839c2f273c7a4e3ce7e2', - token: '0x745861aed1eee363b4aaa5f1994be40b1e05ff90', //fDAIx - block: 10024811, - txHash: '0x0fefa02d90be46eb51a82f02b7a787084c35a895bd833a7c9f0560e315bb4061', - }; - graphql.request.mockResolvedValue(mockSuperfluidSubgraph[1]); + describe('on 2 nested requests', () => { + it(`should get payment event from SuperFluid via subgraph with 2 requests on ${network}`, async () => { + const paymentData = { + reference: '0xbeefac9474ad7670909da5', + from: '0x9c040e2d6fd83a8b35069aa7154b69674961e0f7', + to: '0x52e5bcfa46393894afcfe6cd98a6761fa692c594', + network: network, + salt: '0ee84db293a752c6', + amount: '320833333333331260', + // = (1642693617 - 1642692777 = 840 sec) x (385802469135800 - 3858024691358 = 381944444444442 Wei DAIx / sec) + requestId: '0288792633ff0ec72a7dbdefb886d2db6cccfa98287320839c2f273c7a4e3ce7e2', + token: fDAIxToken, + }; + graphql.request.mockResolvedValue(mockSuperfluidSubgraph[1]); - const paymentReference = PaymentReferenceCalculator.calculate( - paymentData.requestId, - paymentData.salt, - paymentData.to, - ); - const subgraphReference = `0xbeefac${paymentReference}`; - expect(subgraphReference).toEqual(paymentData.reference); - const graphRetriever = new SuperFluidInfoRetriever( - paymentReference, - paymentData.token, - paymentData.to, - PaymentTypes.EVENTS_NAMES.PAYMENT, - paymentData.network, - ); - const transferEvents = await graphRetriever.getTransferEvents(); - expect(transferEvents).toHaveLength(1); - expect(transferEvents[0].amount).toEqual(paymentData.amount); - expect(transferEvents[0].name).toEqual('payment'); - expect(transferEvents[0].parameters?.to).toEqual(paymentData.to); - expect(transferEvents[0].parameters?.txHash).toEqual(paymentData.txHash); - expect(transferEvents[0].parameters?.block).toEqual(paymentData.block); + const paymentReference = PaymentReferenceCalculator.calculate( + paymentData.requestId, + paymentData.salt, + paymentData.to, + ); + const subgraphReference = `0xbeefac${paymentReference}`; + expect(subgraphReference).toEqual(paymentData.reference); + const graphRetriever = new SuperFluidInfoRetriever( + paymentReference, + paymentData.token, + paymentData.to, + PaymentTypes.EVENTS_NAMES.PAYMENT, + paymentData.network, + ); + const transferEvents = await graphRetriever.getTransferEvents(); + expect(transferEvents).toHaveLength(1); + expect(transferEvents[0].amount).toEqual(paymentData.amount); + expect(transferEvents[0].name).toEqual('payment'); + expect(transferEvents[0].parameters?.to).toEqual(paymentData.to); + }); }); }); +}; +const testSuiteWithUSDCx = (network: string, fUSDCxToken: string) => { describe('on ongoing request', () => { - it('should get payment event from SuperFluid via subgraph with ongoing request', async () => { + it(`should get payment event from SuperFluid via subgraph with ongoing request on ${network}`, async () => { const paymentData = { reference: '0xbeefac0e87b43bf1e99c82', from: '0x165a26628ac843e97f657e648b004226fbb7f7c5', to: '0xe7e6431f08db273d915b49888f0c67ef61802e05', - network: 'rinkeby', + network: network, salt: '0ee84db293a752c6', amount: '1', requestId: '0688792633ff0ec72a7dbdefb886d2db6cccfa98287320839c2f273c7a4e3ce7e2', - token: '0x0f1d7c55a2b133e000ea10eec03c774e0d6796e8', //fUSDCx + token: fUSDCxToken, timestamp: 1643041225, - block: 10047970, - txHash: '0xdb44f35aa1490d2ddc8bbe7b82e0e3a370f3bf171a55da7a8a5886996e9c468d', }; graphql.request.mockResolvedValue(mockSuperfluidSubgraph[2]); @@ -131,8 +133,12 @@ describe('api/erc777/superfluid-info-retriever', () => { expect(transferEvents[0].amount).toEqual(timestamp.toString()); expect(transferEvents[0].name).toEqual('payment'); expect(transferEvents[0].parameters?.to).toEqual(paymentData.to); - expect(transferEvents[0].parameters?.txHash).toEqual(paymentData.txHash); - expect(transferEvents[0].parameters?.block).toEqual(paymentData.block); }); }); -}); +}; + +testSuiteWithDaix('rinkeby', fDAIxTokenRinkeby); +testSuiteWithDaix('goerli', fDAIxTokenGoerli); + +testSuiteWithUSDCx('rinkeby', fUSDCxTokenRinkeby); +testSuiteWithUSDCx('goerli', fUSDCxTokenGoerli); diff --git a/packages/payment-detection/test/eth/info-retriever.test.ts b/packages/payment-detection/test/eth/info-retriever.test.ts index 0a314c0c7..de5f76798 100644 --- a/packages/payment-detection/test/eth/info-retriever.test.ts +++ b/packages/payment-detection/test/eth/info-retriever.test.ts @@ -46,11 +46,12 @@ describe('api/eth/info-retriever', () => { }); describe('Multichain', () => { - // TODO temporary disable xDAI, CELO and Sokol + // TODO temporary disable xDAI, CELO, Sokol, and Goerli // FIXME: API-based checks should run nightly and be mocked for CI [ 'mainnet', 'rinkeby', + // 'goerli', // 'xdai', // 'sokol', 'fuse', diff --git a/packages/payment-detection/test/eth/input-data.test.ts b/packages/payment-detection/test/eth/input-data.test.ts index 3be6bfd6d..054c189c1 100644 --- a/packages/payment-detection/test/eth/input-data.test.ts +++ b/packages/payment-detection/test/eth/input-data.test.ts @@ -23,7 +23,7 @@ const mockAdvancedLogic: AdvancedLogicTypes.IAdvancedLogic = { createAddPaymentAddressAction, createAddRefundAddressAction, createCreationAction, - supportedNetworks: ['mainnet', 'rinkeby'], + supportedNetworks: ['mainnet', 'rinkeby', 'goerli'], // inherited from declarative createAddPaymentInstructionAction, createAddRefundInstructionAction, @@ -160,7 +160,7 @@ describe('api/eth/input-data', () => { error: { code: PaymentTypes.BALANCE_ERROR_CODE.NETWORK_NOT_SUPPORTED, message: - /Payment network wrong not supported by ETH payment detection\. Supported networks: mainnet, rinkeby, private.*/, + /Payment network wrong not supported by ETH payment detection\. Supported networks: mainnet, rinkeby, goerli, private.*/, }, events: [], }); diff --git a/packages/payment-detection/test/provider.test.ts b/packages/payment-detection/test/provider.test.ts index 1a9fdaf5f..c7aed7ef3 100644 --- a/packages/payment-detection/test/provider.test.ts +++ b/packages/payment-detection/test/provider.test.ts @@ -13,12 +13,23 @@ describe('getDefaultProvider', () => { expect(provider).toBeInstanceOf(providers.InfuraProvider); await expect(provider.getNetwork()).resolves.toMatchObject({ chainId: 1 }); }); + const testSuite = (network: string, chainId: number) => { + it(`Can take a standard network ${network}`, async () => { + const provider = getDefaultProvider(network); - it('Can take a standard network', async () => { - const provider = getDefaultProvider('rinkeby'); + expect(provider).toBeInstanceOf(providers.InfuraProvider); + await expect(provider.getNetwork()).resolves.toMatchObject({ chainId: chainId }); + }); + }; + + testSuite('rinkeby', 4); + testSuite('goerli', 5); + + it('Can take a standard network (Goerli)', async () => { + const provider = getDefaultProvider('goerli'); expect(provider).toBeInstanceOf(providers.InfuraProvider); - await expect(provider.getNetwork()).resolves.toMatchObject({ chainId: 4 }); + await expect(provider.getNetwork()).resolves.toMatchObject({ chainId: 5 }); }); it('Can take a private network', async () => { @@ -71,12 +82,16 @@ describe('getDefaultProvider', () => { ); }); - it('Can override the api key for a standard provider', async () => { - initPaymentDetectionApiKeys({ - infura: 'foo-bar', - }); + expect((getDefaultProvider('goerli') as providers.JsonRpcProvider).connection.url).toMatch( + /https:\/\/goerli\.infura.*/, + ); +}); - const provider = getDefaultProvider() as providers.InfuraProvider; - expect(provider.connection.url).toEqual('https://mainnet.infura.io/v3/foo-bar'); +it('Can override the api key for a standard provider', async () => { + initPaymentDetectionApiKeys({ + infura: 'foo-bar', }); + + const provider = getDefaultProvider() as providers.InfuraProvider; + expect(provider.connection.url).toEqual('https://mainnet.infura.io/v3/foo-bar'); }); diff --git a/packages/payment-processor/src/payment/utils.ts b/packages/payment-processor/src/payment/utils.ts index 355665d17..a0dc0b72d 100644 --- a/packages/payment-processor/src/payment/utils.ts +++ b/packages/payment-processor/src/payment/utils.ts @@ -30,7 +30,7 @@ export function getProvider(): providers.Web3Provider { /** * Utility to get a network provider, depending on the request's currency network. - * Will throw an error if the network isn't mainnet or rinkeby + * Will throw an error if the network isn't mainnet, rinkeby, or goerli * * @param request */ diff --git a/packages/payment-processor/test/payment/utils.test.ts b/packages/payment-processor/test/payment/utils.test.ts index 7f06a01d1..16edf997d 100644 --- a/packages/payment-processor/test/payment/utils.test.ts +++ b/packages/payment-processor/test/payment/utils.test.ts @@ -108,6 +108,15 @@ describe('getNetworkProvider', () => { expect(getNetworkProvider(request)).toBeInstanceOf(providers.Provider); }); + it('returns a provider for goerli', () => { + const request: any = { + currencyInfo: { + network: 'goerli', + }, + }; + expect(getNetworkProvider(request)).toBeInstanceOf(providers.Provider); + }); + it('fails for other network', () => { const request: any = { currencyInfo: { diff --git a/packages/smart-contracts/scripts-create2/compute-one-address.ts b/packages/smart-contracts/scripts-create2/compute-one-address.ts index 7e8c4809a..f2803616e 100644 --- a/packages/smart-contracts/scripts-create2/compute-one-address.ts +++ b/packages/smart-contracts/scripts-create2/compute-one-address.ts @@ -52,7 +52,7 @@ export const computeCreate2DeploymentAddressesFromList = async ( switch (contract) { case 'EthereumProxy': case 'EthereumFeeProxy': - case 'ETHConversionProxy': + case 'EthConversionProxy': case 'ERC20FeeProxy': case 'Erc20ConversionProxy': case 'ERC20EscrowToPay': diff --git a/packages/smart-contracts/scripts-create2/contract-setup/setupETHConversionProxy.ts b/packages/smart-contracts/scripts-create2/contract-setup/setupETHConversionProxy.ts index 28d1c974d..b0c0cbfee 100644 --- a/packages/smart-contracts/scripts-create2/contract-setup/setupETHConversionProxy.ts +++ b/packages/smart-contracts/scripts-create2/contract-setup/setupETHConversionProxy.ts @@ -8,7 +8,7 @@ import { updateChainlinkConversionPath, updatePaymentErc20FeeProxy } from './adm * @param contractAddress address of the BatchPayments Proxy * @param hre Hardhat runtime environment */ -export const setupEthConversionProxy = async ( +export const setupETHConversionProxy = async ( contractAddress: string, hre: HardhatRuntimeEnvironmentExtended, ): Promise => { diff --git a/packages/smart-contracts/scripts-create2/contract-setup/setups.ts b/packages/smart-contracts/scripts-create2/contract-setup/setups.ts index 88f85e030..cda45affa 100644 --- a/packages/smart-contracts/scripts-create2/contract-setup/setups.ts +++ b/packages/smart-contracts/scripts-create2/contract-setup/setups.ts @@ -1,143 +1,10 @@ import { HardhatRuntimeEnvironmentExtended } from '../types'; -import { erc20SwapConversionArtifact } from '../../src/lib'; -import { batchPaymentsArtifact } from '../../src/lib'; -import utils from '@requestnetwork/utils'; -import { - updateBatchPaymentFees, - updatePaymentErc20FeeProxy, - updatePaymentEthFeeProxy, - updateChainlinkConversionPath, - updateRequestSwapFees, - updateSwapRouter, -} from './adminTasks'; +import { setupETHConversionProxy } from './setupETHConversionProxy'; +import { setupBatchPayments } from './setupBatchPayments'; +import { setupERC20SwapToConversion } from './setupERC20SwapToConversion'; /** - * Updates the values of the batch fees of the BatchPayments contract, if needed - * @param contractAddress address of the BatchPayments Proxy - * @param hre Hardhat runtime environment - */ -const setupBatchPayments = async ( - contractAddress: string, - hre: HardhatRuntimeEnvironmentExtended, -): Promise => { - // Setup contract parameters - const batchPaymentContract = new hre.ethers.Contract( - contractAddress, - batchPaymentsArtifact.getContractAbi(), - ); - await Promise.all( - hre.config.xdeploy.networks.map(async (network) => { - let provider; - if (network === 'celo') { - provider = utils.getCeloProvider(); - } else { - provider = utils.getDefaultProvider(network); - } - const wallet = new hre.ethers.Wallet(hre.config.xdeploy.signer, provider); - const signer = wallet.connect(provider); - const batchPaymentConnected = await batchPaymentContract.connect(signer); - const adminNonce = await signer.getTransactionCount(); - const gasPrice = await provider.getGasPrice(); - - // start from the adminNonce, increase gasPrice if needed - await Promise.all([ - updateBatchPaymentFees(batchPaymentConnected, adminNonce, gasPrice.mul(2)), - updatePaymentErc20FeeProxy(batchPaymentConnected, network, adminNonce + 1, gasPrice.mul(2)), - updatePaymentEthFeeProxy(batchPaymentConnected, network, adminNonce + 2, gasPrice.mul(2)), - ]); - }), - ); - console.log('Setup for setupBatchPayment successful'); -}; - -/** - * Updates the values of the chainlinkConversionPath and swap router of the ERC20SwapToConversion contract, if needed - * @param contractAddress address of the ERC20SwapToConversion Proxy - * @param hre Hardhat runtime environment - */ -const setupERC20SwapToConversion = async ( - contractAddress: string, - hre: HardhatRuntimeEnvironmentExtended, -): Promise => { - // Setup contract parameters - const ERC20SwapToConversionContract = new hre.ethers.Contract( - contractAddress, - erc20SwapConversionArtifact.getContractAbi(), - ); - await Promise.all( - hre.config.xdeploy.networks.map(async (network) => { - let provider; - if (network === 'celo') { - provider = utils.getCeloProvider(); - } else { - provider = utils.getDefaultProvider(network); - } - const wallet = new hre.ethers.Wallet(hre.config.xdeploy.signer, provider); - const signer = wallet.connect(provider); - const ERC20SwapToConversionConnected = await ERC20SwapToConversionContract.connect(signer); - const adminNonce = await signer.getTransactionCount(); - const gasPrice = await provider.getGasPrice(); - - await Promise.all([ - updateChainlinkConversionPath( - ERC20SwapToConversionConnected, - network, - adminNonce, - gasPrice, - ), - updateSwapRouter(ERC20SwapToConversionConnected, network, adminNonce + 1, gasPrice), - updateRequestSwapFees(ERC20SwapToConversionConnected, adminNonce + 2, gasPrice), - ]); - }), - ); - console.log('Setup for ERC20SwapToConversion successfull'); -}; - -/** - * Updates the values of the EthConversionProxy contract, if needed - * @param contractAddress address of the EthConversion Proxy - * @param hre Hardhat runtime environment - */ -const setupEthConversionProxy = async ( - contractAddress: string, - hre: HardhatRuntimeEnvironmentExtended, -): Promise => { - // Setup contract parameters - const EthConversionProxyContract = new hre.ethers.Contract( - contractAddress, - batchPaymentsArtifact.getContractAbi(), - ); - await Promise.all( - hre.config.xdeploy.networks.map(async (network) => { - let provider; - if (network === 'celo') { - provider = utils.getCeloProvider(); - } else { - provider = utils.getDefaultProvider(network); - } - const wallet = new hre.ethers.Wallet(hre.config.xdeploy.signer, provider); - const signer = wallet.connect(provider); - const EthConversionProxyConnected = await EthConversionProxyContract.connect(signer); - const adminNonce = await signer.getTransactionCount(); - const gasPrice = await provider.getGasPrice(); - - // start from the adminNonce, increase gasPrice if needed - await Promise.all([ - updatePaymentErc20FeeProxy(EthConversionProxyConnected, network, adminNonce, gasPrice), - updateChainlinkConversionPath( - EthConversionProxyConnected, - network, - adminNonce + 1, - gasPrice, - ), - ]); - }), - ); - console.log('Setup for EthConversionProxy successful'); -}; - -/** - * Updates the values of either BatchPayments, EthConversionProxy, or ERC20SwapToConversion contract, if needed + * Updates the values of either BatchPayments, ETHConversionProxy, or ERC20SwapToConversion contract, if needed * @param contractAddress address of the proxy * @param hre Hardhat runtime environment * @param contractName name of the contract @@ -148,8 +15,8 @@ export const setupContract = async ( contractName: string, ): Promise => { switch (contractName) { - case 'EthConversionProxy': { - await setupEthConversionProxy(contractAddress, hre); + case 'ETHConversionProxy': { + await setupETHConversionProxy(contractAddress, hre); break; } case 'ERC20SwapToConversion': { diff --git a/packages/smart-contracts/scripts-create2/deploy.ts b/packages/smart-contracts/scripts-create2/deploy.ts index ec15644fa..23dbfc931 100644 --- a/packages/smart-contracts/scripts-create2/deploy.ts +++ b/packages/smart-contracts/scripts-create2/deploy.ts @@ -53,7 +53,7 @@ export const deployWithCreate2FromList = async ( switch (contract) { case 'EthereumProxy': case 'EthereumFeeProxy': - case 'ETHConversionProxy': + case 'EthConversionProxy': case 'ERC20FeeProxy': case 'Erc20ConversionProxy': { const constructorArgs = getConstructorArgs(contract); diff --git a/packages/smart-contracts/scripts-create2/utils.ts b/packages/smart-contracts/scripts-create2/utils.ts index 421bba148..6956b562a 100644 --- a/packages/smart-contracts/scripts-create2/utils.ts +++ b/packages/smart-contracts/scripts-create2/utils.ts @@ -9,7 +9,7 @@ import * as artifacts from '../src/lib'; export const create2ContractDeploymentList = [ 'EthereumProxy', 'EthereumFeeProxy', - 'ETHConversionProxy', + 'EthConversionProxy', 'ERC20FeeProxy', 'Erc20ConversionProxy', 'ERC20SwapToConversion', @@ -39,7 +39,7 @@ export const getArtifact = (contract: string): artifacts.ContractArtifact { const NONCE_BATCH_4 = 10; @@ -170,7 +170,7 @@ export async function deployAllPaymentContracts( }); // Deploy ETH Conversion - const ethConversionResult = await deployETHConversionProxy( + const ethConversionResult = await deployEthConversionProxy( { ...args, chainlinkConversionPathAddress, @@ -265,12 +265,12 @@ export async function deployAllPaymentContracts( const ethConversionAdminNonce = NONCE_BATCH_5 + 3; await jumpToNonce(args, hre, ethConversionAdminNonce); - // 5.d ETHConversion.transferOwnership + // 5.d EthConversion.transferOwnership if (await nonceReady(ethConversionAdminNonce)) { if (ethConversionResultInstance) { if (!process.env.ADMIN_WALLET_ADDRESS) { throw new Error( - 'ADMIN_WALLET_ADDRESS missing, cannot addWhitelistAdmin on ETHConversion.', + 'ADMIN_WALLET_ADDRESS missing, cannot addWhitelistAdmin on EthConversion.', ); } if (args.simulate === false) { @@ -280,13 +280,13 @@ export async function deployAllPaymentContracts( await tx.wait(1); } else { console.log( - `[i] Simulating addWhitelistAdmin to ETHConversion at ${ethConversionResultInstance.address}`, + `[i] Simulating addWhitelistAdmin to EthConversion at ${ethConversionResultInstance.address}`, ); } } else { if (!ethConversionResultInstance) { console.warn( - `Warning: the ETHConversion contract instance is not ready for ETHConversion update, consider retrying.`, + `Warning: the EthConversion contract instance is not ready for EthConversion update, consider retrying.`, ); switchToSimulation(); } diff --git a/packages/smart-contracts/scripts/test-deploy_chainlink_contract.ts b/packages/smart-contracts/scripts/test-deploy_chainlink_contract.ts index 88fa62a25..099be843a 100644 --- a/packages/smart-contracts/scripts/test-deploy_chainlink_contract.ts +++ b/packages/smart-contracts/scripts/test-deploy_chainlink_contract.ts @@ -1,7 +1,7 @@ import '@nomiclabs/hardhat-ethers'; import { CurrencyManager } from '@requestnetwork/currency'; import { HardhatRuntimeEnvironment } from 'hardhat/types'; -import { deployERC20ConversionProxy, deployETHConversionProxy } from './conversion-proxy'; +import { deployERC20ConversionProxy, deployEthConversionProxy } from './conversion-proxy'; import { deploySwapConversion } from './erc20-swap-to-conversion'; import { deployOne } from './deploy-one'; @@ -93,7 +93,7 @@ export default async function deploy( // EthConversion const ethConversionProxyAddress = ( - await deployETHConversionProxy( + await deployEthConversionProxy( { ...args, chainlinkConversionPathAddress: conversionPathInstance.address, diff --git a/packages/smart-contracts/src/contracts/interfaces/IERC20ConversionProxy.sol b/packages/smart-contracts/src/contracts/interfaces/IERC20ConversionProxy.sol index 946d17069..3a7b8b8f6 100644 --- a/packages/smart-contracts/src/contracts/interfaces/IERC20ConversionProxy.sol +++ b/packages/smart-contracts/src/contracts/interfaces/IERC20ConversionProxy.sol @@ -1,3 +1,4 @@ +// SPDX-License-Identifier: MIT pragma solidity ^0.8.0; interface IERC20ConversionProxy { diff --git a/packages/smart-contracts/src/lib/artifacts/ChainlinkConversionPath/index.ts b/packages/smart-contracts/src/lib/artifacts/ChainlinkConversionPath/index.ts index ee0761fde..817dd671e 100644 --- a/packages/smart-contracts/src/lib/artifacts/ChainlinkConversionPath/index.ts +++ b/packages/smart-contracts/src/lib/artifacts/ChainlinkConversionPath/index.ts @@ -72,6 +72,10 @@ export const chainlinkConversionPath = new ContractArtifact( address: '0x1B5077Ca852d39CDDeDaF45FAF1235841854420b', creationBlockNumber: 7408086, }, + goerli: { + address: '0x0Ef49176A87Adcc88bD5125126C6a6c23a28303C', + creationBlockNumber: 7109102, + }, bsc: { address: '0x75740D9b5cA3BCCb356CA7f0D0dB71aBE427a835', creationBlockNumber: 16165020, diff --git a/packages/smart-contracts/src/lib/artifacts/EthConversionProxy/index.ts b/packages/smart-contracts/src/lib/artifacts/EthConversionProxy/index.ts index 01ced6caa..c9720fa6d 100644 --- a/packages/smart-contracts/src/lib/artifacts/EthConversionProxy/index.ts +++ b/packages/smart-contracts/src/lib/artifacts/EthConversionProxy/index.ts @@ -55,6 +55,10 @@ export const ethConversionArtifact = new ContractArtifact( address: '0x7Ebf48a26253810629C191b56C3212Fd0D211c26', creationBlockNumber: 10023415, }, + goerli: { + address: '0xED250D9219EB93098Bb67aEbc992963172B9c8DA', + creationBlockNumber: 7108896, + }, fantom: { address: '0x7Ebf48a26253810629C191b56C3212Fd0D211c26', creationBlockNumber: 28552915, diff --git a/packages/smart-contracts/src/lib/artifacts/EthereumFeeProxy/index.ts b/packages/smart-contracts/src/lib/artifacts/EthereumFeeProxy/index.ts index 06dd43676..48bfbf6b2 100644 --- a/packages/smart-contracts/src/lib/artifacts/EthereumFeeProxy/index.ts +++ b/packages/smart-contracts/src/lib/artifacts/EthereumFeeProxy/index.ts @@ -66,6 +66,10 @@ export const ethereumFeeProxyArtifact = new ContractArtifact( address: '0xfCFBcfc4f5A421089e3Df45455F7f4985FE2D6a8', creationBlockNumber: 10307582, }, + goerli: { + address: '0xe11BF2fDA23bF0A98365e1A4c04A87C9339e8687', + creationBlockNumber: 7091386, + }, fantom: { address: '0xfCFBcfc4f5A421089e3Df45455F7f4985FE2D6a8', creationBlockNumber: 33495801, diff --git a/packages/smart-contracts/src/lib/artifacts/EthereumProxy/index.ts b/packages/smart-contracts/src/lib/artifacts/EthereumProxy/index.ts index 527cedbb7..2602944bd 100644 --- a/packages/smart-contracts/src/lib/artifacts/EthereumProxy/index.ts +++ b/packages/smart-contracts/src/lib/artifacts/EthereumProxy/index.ts @@ -136,6 +136,10 @@ export const ethereumProxyArtifact = new ContractArtifact( address: '0x322F0037d272E980984F89E94Aae43BD0FC065E6', creationBlockNumber: 10307566, }, + goerli: { + address: '0x171Ee0881407d4c0C11eA1a2FB7D5b4cdED71e6e', + creationBlockNumber: 7069045, + }, fantom: { address: '0x322F0037d272E980984F89E94Aae43BD0FC065E6', creationBlockNumber: 33496209, From 8b51b333d7303533b3e2d078eb078b3db27a53a7 Mon Sep 17 00:00:00 2001 From: Romain <45540622+rom1trt@users.noreply.github.com> Date: Wed, 3 Aug 2022 10:13:18 +0200 Subject: [PATCH 016/105] refactor: factorize goerli tests (#893) * refactor: factorize goerli tests * refactor: testSuite to testProvider * fix: delete goerli aggregator * feat: add void goerli chainlinkAggregator --- packages/currency/src/aggregators/goerli.json | 8 ------ .../src/chainlink-path-aggregators.ts | 1 + .../any/any-to-erc20-proxy-contract.test.ts | 23 +-------------- .../payment-detection/test/provider.test.ts | 7 ----- .../test/payment/utils.test.ts | 28 ++++++++----------- 5 files changed, 14 insertions(+), 53 deletions(-) delete mode 100644 packages/currency/src/aggregators/goerli.json diff --git a/packages/currency/src/aggregators/goerli.json b/packages/currency/src/aggregators/goerli.json deleted file mode 100644 index 93d71cbd2..000000000 --- a/packages/currency/src/aggregators/goerli.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "0xba62bcfcaafc6622853cca2be6ac7d845bc0f2dc": { - "0x775eb53d00dd0acd3ec1696472105d579b9b386b": 1 - }, - "0x775eb53d00dd0acd3ec1696472105d579b9b386b": { - "0xba62bcfcaafc6622853cca2be6ac7d845bc0f2dc": 1 - } -} diff --git a/packages/currency/src/chainlink-path-aggregators.ts b/packages/currency/src/chainlink-path-aggregators.ts index e81a0f504..e59e35e5c 100644 --- a/packages/currency/src/chainlink-path-aggregators.ts +++ b/packages/currency/src/chainlink-path-aggregators.ts @@ -14,6 +14,7 @@ export type CurrencyPairs = Record>; export const chainlinkCurrencyPairs: Record = { private: privateAggregator, rinkeby: rinkebyAggregator, + goerli: {}, mainnet: mainnetAggregator, matic: maticAggregator, fantom: fantomAggregator, diff --git a/packages/payment-detection/test/any/any-to-erc20-proxy-contract.test.ts b/packages/payment-detection/test/any/any-to-erc20-proxy-contract.test.ts index f770a7415..927d075fc 100644 --- a/packages/payment-detection/test/any/any-to-erc20-proxy-contract.test.ts +++ b/packages/payment-detection/test/any/any-to-erc20-proxy-contract.test.ts @@ -78,7 +78,7 @@ describe('api/any/conversion-fee-proxy-contract', () => { }); }); - it(`can createExtensionsDataForCreation with fee amount and address ${network}`, async () => { + it(`can createExtensionsDataForCreation with fee amount and address on ${network}`, async () => { await anyToErc20Proxy.createExtensionsDataForCreation({ feeAddress: 'fee address', feeAmount: '2000', @@ -103,27 +103,6 @@ describe('api/any/conversion-fee-proxy-contract', () => { testSuite('rinkeby'); testSuite('goerli'); - it('can createExtensionsDataForCreation with fee amount and address (Goerli)', async () => { - await anyToErc20Proxy.createExtensionsDataForCreation({ - feeAddress: 'fee address', - feeAmount: '2000', - paymentAddress: 'ethereum address', - salt: 'ea3bc7caf64110ca', - acceptedTokens: ['ethereum address2'], - network: 'goerli', - }); - - expect(createCreationAction).toHaveBeenCalledWith({ - feeAddress: 'fee address', - feeAmount: '2000', - paymentAddress: 'ethereum address', - refundAddress: undefined, - salt: 'ea3bc7caf64110ca', - acceptedTokens: ['ethereum address2'], - network: 'goerli', - }); - }); - it('can createExtensionsDataForCreation without salt', async () => { await anyToErc20Proxy.createExtensionsDataForCreation({ paymentAddress: 'ethereum address', diff --git a/packages/payment-detection/test/provider.test.ts b/packages/payment-detection/test/provider.test.ts index c7aed7ef3..2f04e83b5 100644 --- a/packages/payment-detection/test/provider.test.ts +++ b/packages/payment-detection/test/provider.test.ts @@ -25,13 +25,6 @@ describe('getDefaultProvider', () => { testSuite('rinkeby', 4); testSuite('goerli', 5); - it('Can take a standard network (Goerli)', async () => { - const provider = getDefaultProvider('goerli'); - - expect(provider).toBeInstanceOf(providers.InfuraProvider); - await expect(provider.getNetwork()).resolves.toMatchObject({ chainId: 5 }); - }); - it('Can take a private network', async () => { const provider = getDefaultProvider('private') as providers.JsonRpcProvider; diff --git a/packages/payment-processor/test/payment/utils.test.ts b/packages/payment-processor/test/payment/utils.test.ts index 16edf997d..90ac9ae08 100644 --- a/packages/payment-processor/test/payment/utils.test.ts +++ b/packages/payment-processor/test/payment/utils.test.ts @@ -99,23 +99,19 @@ describe('getNetworkProvider', () => { expect(getNetworkProvider(request)).toBeInstanceOf(providers.Provider); }); - it('returns a provider for rinkeby', () => { - const request: any = { - currencyInfo: { - network: 'rinkeby', - }, - }; - expect(getNetworkProvider(request)).toBeInstanceOf(providers.Provider); - }); + const testProvider = (network: string) => { + it(`returns a provider for ${network}`, () => { + const request: any = { + currencyInfo: { + network: network, + }, + }; + expect(getNetworkProvider(request)).toBeInstanceOf(providers.Provider); + }); + }; - it('returns a provider for goerli', () => { - const request: any = { - currencyInfo: { - network: 'goerli', - }, - }; - expect(getNetworkProvider(request)).toBeInstanceOf(providers.Provider); - }); + testProvider('rinkeby'); + testProvider('goerli'); it('fails for other network', () => { const request: any = { From 0f20179c8388a1c17fa5645800c885da89aa1857 Mon Sep 17 00:00:00 2001 From: Romain <45540622+rom1trt@users.noreply.github.com> Date: Wed, 3 Aug 2022 13:45:58 +0200 Subject: [PATCH 017/105] feat: add goerli support (advanced-logic) (#895) feat: add goerli support --- .../extensions/payment-network/erc20/address-based.ts | 2 +- .../payment-network/erc20/fee-proxy-contract.ts | 1 + .../extensions/payment-network/erc20/proxy-contract.ts | 2 +- .../src/extensions/payment-network/erc777/stream.ts | 9 ++++++++- .../payment-network/ethereum/fee-proxy-contract.ts | 2 +- .../extensions/payment-network/ethereum/input-data.ts | 1 + 6 files changed, 13 insertions(+), 4 deletions(-) diff --git a/packages/advanced-logic/src/extensions/payment-network/erc20/address-based.ts b/packages/advanced-logic/src/extensions/payment-network/erc20/address-based.ts index e7cb7905f..e987247e0 100644 --- a/packages/advanced-logic/src/extensions/payment-network/erc20/address-based.ts +++ b/packages/advanced-logic/src/extensions/payment-network/erc20/address-based.ts @@ -3,7 +3,7 @@ import AddressBasedPaymentNetwork from '../address-based'; const CURRENT_VERSION = '0.1.0'; -const supportedNetworks = ['mainnet', 'rinkeby', 'private']; +const supportedNetworks = ['mainnet', 'rinkeby', 'private', 'goerli']; /** * Implementation of the payment network to pay in ERC20 tokens based on an Ethereum address diff --git a/packages/advanced-logic/src/extensions/payment-network/erc20/fee-proxy-contract.ts b/packages/advanced-logic/src/extensions/payment-network/erc20/fee-proxy-contract.ts index 3ef3dbf7b..8dae61feb 100644 --- a/packages/advanced-logic/src/extensions/payment-network/erc20/fee-proxy-contract.ts +++ b/packages/advanced-logic/src/extensions/payment-network/erc20/fee-proxy-contract.ts @@ -15,6 +15,7 @@ export default class Erc20FeeProxyPaymentNetwork< public supportedNetworks: string[] = [ 'mainnet', 'rinkeby', + 'goerli', 'private', 'matic', 'mumbai', diff --git a/packages/advanced-logic/src/extensions/payment-network/erc20/proxy-contract.ts b/packages/advanced-logic/src/extensions/payment-network/erc20/proxy-contract.ts index d5eda8823..6841a39fe 100644 --- a/packages/advanced-logic/src/extensions/payment-network/erc20/proxy-contract.ts +++ b/packages/advanced-logic/src/extensions/payment-network/erc20/proxy-contract.ts @@ -12,7 +12,7 @@ export default class Erc20ProxyPaymentNetwork< public constructor( public extensionId: ExtensionTypes.ID = ExtensionTypes.ID.PAYMENT_NETWORK_ERC20_PROXY_CONTRACT, public currentVersion: string = CURRENT_VERSION, - public supportedNetworks: string[] = ['mainnet', 'rinkeby', 'private'], + public supportedNetworks: string[] = ['mainnet', 'rinkeby', 'goerli', 'private'], public supportedCurrencyType: RequestLogicTypes.CURRENCY = RequestLogicTypes.CURRENCY.ERC20, ) { super(extensionId, currentVersion, supportedNetworks, supportedCurrencyType); diff --git a/packages/advanced-logic/src/extensions/payment-network/erc777/stream.ts b/packages/advanced-logic/src/extensions/payment-network/erc777/stream.ts index 73cd4ff67..2493a86d9 100644 --- a/packages/advanced-logic/src/extensions/payment-network/erc777/stream.ts +++ b/packages/advanced-logic/src/extensions/payment-network/erc777/stream.ts @@ -13,7 +13,14 @@ export default class Erc777StreamPaymentNetwork< public constructor( extensionId: ExtensionTypes.ID = ExtensionTypes.ID.PAYMENT_NETWORK_ERC777_STREAM, currentVersion: string = CURRENT_VERSION, - public supportedNetworks: string[] = ['matic', 'xdai', 'mumbai', 'rinkeby', 'arbitrum-rinkeby'], + public supportedNetworks: string[] = [ + 'matic', + 'xdai', + 'mumbai', + 'rinkeby', + 'goerli', + 'arbitrum-rinkeby', + ], public supportedCurrencyType: RequestLogicTypes.CURRENCY = RequestLogicTypes.CURRENCY.ERC777, ) { super(extensionId, currentVersion, supportedNetworks, supportedCurrencyType); diff --git a/packages/advanced-logic/src/extensions/payment-network/ethereum/fee-proxy-contract.ts b/packages/advanced-logic/src/extensions/payment-network/ethereum/fee-proxy-contract.ts index 4bd15b240..fa5cab775 100644 --- a/packages/advanced-logic/src/extensions/payment-network/ethereum/fee-proxy-contract.ts +++ b/packages/advanced-logic/src/extensions/payment-network/ethereum/fee-proxy-contract.ts @@ -12,7 +12,7 @@ export default class EthereumFeeProxyPaymentNetwork< public constructor( extensionId: ExtensionTypes.ID = ExtensionTypes.ID.PAYMENT_NETWORK_ETH_FEE_PROXY_CONTRACT, currentVersion: string = CURRENT_VERSION, - public supportedNetworks: string[] = ['mainnet', 'rinkeby', 'private'], + public supportedNetworks: string[] = ['mainnet', 'rinkeby', 'goerli', 'private'], ) { super(extensionId, currentVersion, supportedNetworks, RequestLogicTypes.CURRENCY.ETH); } diff --git a/packages/advanced-logic/src/extensions/payment-network/ethereum/input-data.ts b/packages/advanced-logic/src/extensions/payment-network/ethereum/input-data.ts index ee7ce6199..3921a7385 100644 --- a/packages/advanced-logic/src/extensions/payment-network/ethereum/input-data.ts +++ b/packages/advanced-logic/src/extensions/payment-network/ethereum/input-data.ts @@ -5,6 +5,7 @@ const CURRENT_VERSION = '0.3.0'; const supportedNetworks = [ 'mainnet', 'rinkeby', + 'goerli', 'xdai', 'sokol', 'fuse', From 3c1db982754fd8c96e459842b8dcb22fe9b0d401 Mon Sep 17 00:00:00 2001 From: olivier7delf <55892112+olivier7delf@users.noreply.github.com> Date: Tue, 2 Aug 2022 17:18:58 +0200 Subject: [PATCH 018/105] tmp --- .../src/payment/batch-proxy-conv.ts | 370 ++++++++++++++++++ 1 file changed, 370 insertions(+) create mode 100644 packages/payment-processor/src/payment/batch-proxy-conv.ts diff --git a/packages/payment-processor/src/payment/batch-proxy-conv.ts b/packages/payment-processor/src/payment/batch-proxy-conv.ts new file mode 100644 index 000000000..1f7417ba9 --- /dev/null +++ b/packages/payment-processor/src/payment/batch-proxy-conv.ts @@ -0,0 +1,370 @@ +import { ContractTransaction, Signer, providers, constants, BigNumber, BigNumberish } from 'ethers'; +import { batchPaymentsArtifact } from '@requestnetwork/smart-contracts'; +import { BatchPayments__factory } from '@requestnetwork/smart-contracts/types'; +import { ClientTypes, PaymentTypes } from '@requestnetwork/types'; +import { ITransactionOverrides } from './transaction-overrides'; +import { + comparePnTypeAndVersion, + getAmountToPay, + getPaymentNetworkExtension, + getProvider, + getRequestPaymentValues, + getSigner, + validateErc20FeeProxyRequest, +} from './utils'; +import { validateEthFeeProxyRequest } from './eth-fee-proxy'; +import { IPreparedTransaction } from './prepared-transaction'; +import { checkErc20Allowance, encodeApproveAnyErc20 } from './erc20'; +import { IConversionPaymentSettings } from './index'; + +/** TODO UPDATE + * ERC20 Batch Proxy payment details: + * batch of request with the same payment network type: ERC20 + * batch of request with the same payment network version + * 2 modes available: single token or multi tokens + * It requires batch proxy's approval + * + * Eth Batch Proxy payment details: + * batch of request with the same payment network type + * batch of request with the same payment network version + * -> Eth batch proxy accepts requests with 2 id: ethProxy and ethFeeProxy + * but only call ethFeeProxy. It can impact payment detection + */ + +type metaRequests = { + requests: ClientTypes.IRequestData[]; + paymentSettings?: IConversionPaymentSettings[]; + amount?: BigNumberish[]; + feeAmount?: BigNumberish[]; +}; + +/** + * Processes a transaction to pay a batch of requests with an ERC20 or ETH currency that is different from the request currency (eg. fiat). + * The payment is made by the ERC20, or ETH fee proxy contract. + * @param requests List of requests + * @param version version of the batch proxy, which can be different from request pn version + * @param signerOrProvider the Web3 provider, or signer. Defaults to window.ethereum. + * @param batchFee Only for batch ETH: additional fee applied to a batch, between 0 and 1000, default value = 10 + * @param overrides optionally, override default transaction values, like gas. + */ +export async function payBatchConvProxyRequest( + requests: metaRequests, + version: string, + signerOrProvider: providers.Provider | Signer = getProvider(), + batchFee: number, + overrides?: ITransactionOverrides, +): Promise { + const { data, to, value } = prepareBatchPaymentTransaction(requests, version, batchFee); + const signer = getSigner(signerOrProvider); + return signer.sendTransaction({ data, to, value, ...overrides }); +} + +/** + * Processes a transaction to pay a batch of ETH Requests with fees. + * Requests paymentType must be "ETH" or "ERC20" + * @param requests List of requests + * @param version version of the batch proxy, which can be different from request pn version + * @param signerOrProvider the Web3 provider, or signer. Defaults to window.ethereum. + * @param batchFee Only for batch ETH: additional fee applied to a batch, between 0 and 1000, default value = 10 + * @param overrides optionally, override default transaction values, like gas. + */ +export async function payBatchProxyRequest( + requests: ClientTypes.IRequestData[], + version: string, + signerOrProvider: providers.Provider | Signer = getProvider(), + batchFee: number, + overrides?: ITransactionOverrides, +): Promise { + const { data, to, value } = prepareBatchPaymentTransaction(requests, version, batchFee); + const signer = getSigner(signerOrProvider); + return signer.sendTransaction({ data, to, value, ...overrides }); +} + +/** + * Prepate the transaction to pay a batch of requests through the batch proxy contract, can be used with a Multisig contract. + * Requests paymentType must be "ETH" or "ERC20" + * @param requests list of ETH requests to pay + * @param version version of the batch proxy, which can be different from request pn version + * @param batchFee additional fee applied to a batch + */ +export function prepareBatchPaymentTransaction( + requests: ClientTypes.IRequestData[], + version: string, + batchFee: number, +): IPreparedTransaction { + const encodedTx = encodePayBatchRequest(requests); + const proxyAddress = getBatchProxyAddress(requests[0], version); + let totalAmount = 0; + + if (requests[0].currencyInfo.type === 'ETH') { + const { amountsToPay, feesToPay } = getBatchArgs(requests); + + const amountToPay = amountsToPay.reduce((sum, current) => sum.add(current), BigNumber.from(0)); + const batchFeeToPay = BigNumber.from(amountToPay).mul(batchFee).div(1000); + const feeToPay = feesToPay.reduce( + (sum, current) => sum.add(current), + BigNumber.from(batchFeeToPay), + ); + totalAmount = amountToPay.add(feeToPay).toNumber(); + } + + return { + data: encodedTx, + to: proxyAddress, + value: totalAmount, + }; +} + +/** + * Encodes the call to pay a batch of requests through the ERC20Bacth or ETHBatch proxy contract, + * can be used with a Multisig contract. + * @param requests list of ECR20 requests to pay + * @dev pn version of the requests is checked to avoid paying with two differents proxies (e.g: erc20proxy v1 and v2) + */ +export function encodePayBatchRequest(requests: ClientTypes.IRequestData[]): string { + const { + tokenAddresses, + paymentAddresses, + amountsToPay, + paymentReferences, + feesToPay, + feeAddressUsed, + } = getBatchArgs(requests); + + const proxyContract = BatchPayments__factory.createInterface(); + + if (requests[0].currencyInfo.type === 'ERC20') { + let isMultiTokens = false; + for (let i = 0; tokenAddresses.length; i++) { + if (tokenAddresses[0] !== tokenAddresses[i]) { + isMultiTokens = true; + break; + } + } + + const pn = getPaymentNetworkExtension(requests[0]); + for (let i = 0; i < requests.length; i++) { + validateErc20FeeProxyRequest(requests[i]); + if (!comparePnTypeAndVersion(pn, requests[i])) { + throw new Error(`Every payment network type and version must be identical`); + } + } + + if (isMultiTokens) { + return proxyContract.encodeFunctionData('batchERC20PaymentsMultiTokensWithReference', [ + tokenAddresses, + paymentAddresses, + amountsToPay, + paymentReferences, + feesToPay, + feeAddressUsed, + ]); + } else { + return proxyContract.encodeFunctionData('batchERC20PaymentsWithReference', [ + tokenAddresses[0], + paymentAddresses, + amountsToPay, + paymentReferences, + feesToPay, + feeAddressUsed, + ]); + } + } else { + tokenAddresses; + return proxyContract.encodeFunctionData('batchEthPaymentsWithReference', [ + paymentAddresses, + amountsToPay, + paymentReferences, + feesToPay, + feeAddressUsed, + ]); + } +} + +/** + * Get batch arguments + * @param requests List of requests + * @returns List with the args required by batch Eth and Erc20 functions, + * @dev tokenAddresses returned is for batch Erc20 functions + */ +function getBatchArgs( + requests: ClientTypes.IRequestData[], +): { + tokenAddresses: Array; + paymentAddresses: Array; + amountsToPay: Array; + paymentReferences: Array; + feesToPay: Array; + feeAddressUsed: string; +} { + const tokenAddresses: Array = []; + const paymentAddresses: Array = []; + const amountsToPay: Array = []; + const paymentReferences: Array = []; + const feesToPay: Array = []; + let feeAddressUsed = constants.AddressZero; + + const paymentType = requests[0].currencyInfo.type; + for (let i = 0; i < requests.length; i++) { + if (paymentType === 'ETH') { + validateEthFeeProxyRequest(requests[i]); + } else if (paymentType === 'ERC20') { + validateErc20FeeProxyRequest(requests[i]); + } else { + throw new Error(`paymentType ${paymentType} is not supported for batch payment`); + } + + const tokenAddress = requests[i].currencyInfo.value; + const { paymentReference, paymentAddress, feeAddress, feeAmount } = getRequestPaymentValues( + requests[i], + ); + + tokenAddresses.push(tokenAddress); + paymentAddresses.push(paymentAddress); + amountsToPay.push(getAmountToPay(requests[i])); + paymentReferences.push(`0x${paymentReference}`); + feesToPay.push(BigNumber.from(feeAmount || 0)); + feeAddressUsed = feeAddress || constants.AddressZero; + } + + return { + tokenAddresses, + paymentAddresses, + amountsToPay, + paymentReferences, + feesToPay, + feeAddressUsed, + }; +} + +/** + * Get Batch contract Address + * @param request + * @param version version of the batch proxy, which can be different from request pn version + */ +export function getBatchProxyAddress(request: ClientTypes.IRequestData, version: string): string { + const pn = getPaymentNetworkExtension(request); + const pnId = (pn?.id as unknown) as PaymentTypes.PAYMENT_NETWORK_ID; + if (!pnId) { + throw new Error('No payment network Id'); + } + + const proxyAddress = batchPaymentsArtifact.getAddress(request.currencyInfo.network!, version); + + if (!proxyAddress) { + throw new Error(`No deployment found for network ${pn}, version ${pn?.version}`); + } + return proxyAddress; +} + +/** + * ERC20 Batch proxy approvals methods + */ + +/** + * Processes the approval transaction of the targeted ERC20 with batch proxy. + * @param request request to pay + * @param account account that will be used to pay the request + * @param version version of the batch proxy, which can be different from request pn version + * @param signerOrProvider the Web3 provider, or signer. Defaults to window.ethereum. + * @param overrides optionally, override default transaction values, like gas. + */ +export async function approveErc20BatchIfNeeded( + request: ClientTypes.IRequestData, + account: string, + version: string, + signerOrProvider: providers.Provider | Signer = getProvider(), + overrides?: ITransactionOverrides, +): Promise { + if (!(await hasErc20BatchApproval(request, account, version, signerOrProvider))) { + return approveErc20Batch(request, version, getSigner(signerOrProvider), overrides); + } +} + +/** + * Checks if the batch proxy has the necessary allowance from a given account + * to pay a given request with ERC20 batch + * @param request request to pay + * @param account account that will be used to pay the request + * @param version version of the batch proxy, which can be different from request pn version + * @param signerOrProvider the Web3 provider, or signer. Defaults to window.ethereum. + */ +export async function hasErc20BatchApproval( + request: ClientTypes.IRequestData, + account: string, + version: string, + signerOrProvider: providers.Provider | Signer = getProvider(), +): Promise { + return checkErc20Allowance( + account, + getBatchProxyAddress(request, version), + signerOrProvider, + request.currencyInfo.value, + request.expectedAmount, + ); +} + +/** + * Processes the transaction to approve the batch proxy to spend signer's tokens to pay + * the request in its payment currency. Can be used with a Multisig contract. + * @param request request to pay + * @param version version of the batch proxy, which can be different from request pn version + * @param signerOrProvider the Web3 provider, or signer. Defaults to window.ethereum. + * @param overrides optionally, override default transaction values, like gas. + */ +export async function approveErc20Batch( + request: ClientTypes.IRequestData, + version: string, + signerOrProvider: providers.Provider | Signer = getProvider(), + overrides?: ITransactionOverrides, +): Promise { + const preparedTx = prepareApproveErc20Batch(request, version, signerOrProvider, overrides); + const signer = getSigner(signerOrProvider); + const tx = await signer.sendTransaction(preparedTx); + return tx; +} + +/** + * Prepare the transaction to approve the proxy to spend signer's tokens to pay + * the request in its payment currency. Can be used with a Multisig contract. + * @param request request to pay + * @param version version of the batch proxy, which can be different from request pn version + * @param signerOrProvider the Web3 provider, or signer. Defaults to window.ethereum. + * @param overrides optionally, override default transaction values, like gas. + */ +export function prepareApproveErc20Batch( + request: ClientTypes.IRequestData, + version: string, + signerOrProvider: providers.Provider | Signer = getProvider(), + overrides?: ITransactionOverrides, +): IPreparedTransaction { + const encodedTx = encodeApproveErc20Batch(request, version, signerOrProvider); + const tokenAddress = request.currencyInfo.value; + return { + data: encodedTx, + to: tokenAddress, + value: 0, + ...overrides, + }; +} + +/** + * Encodes the transaction to approve the batch proxy to spend signer's tokens to pay + * the request in its payment currency. Can be used with a Multisig contract. + * @param request request to pay + * @param version version of the batch proxy, which can be different from request pn version + * @param signerOrProvider the Web3 provider, or signer. Defaults to window.ethereum. + */ +export function encodeApproveErc20Batch( + request: ClientTypes.IRequestData, + version: string, + signerOrProvider: providers.Provider | Signer = getProvider(), +): string { + const proxyAddress = getBatchProxyAddress(request, version); + + return encodeApproveAnyErc20( + request.currencyInfo.value, + proxyAddress, + getSigner(signerOrProvider), + ); +} From 31cf53f240f804ff3936ad66250abba63d143d4a Mon Sep 17 00:00:00 2001 From: olivier7delf <55892112+olivier7delf@users.noreply.github.com> Date: Thu, 4 Aug 2022 12:33:16 +0200 Subject: [PATCH 019/105] any-to-erc20-proxy payment - correct documention on proxy used --- packages/payment-processor/src/payment/any-to-erc20-proxy.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/payment-processor/src/payment/any-to-erc20-proxy.ts b/packages/payment-processor/src/payment/any-to-erc20-proxy.ts index a557f826d..3c2b65665 100644 --- a/packages/payment-processor/src/payment/any-to-erc20-proxy.ts +++ b/packages/payment-processor/src/payment/any-to-erc20-proxy.ts @@ -20,7 +20,7 @@ import { IConversionPaymentSettings } from './index'; /** * Processes a transaction to pay a request with an ERC20 currency that is different from the request currency (eg. fiat). - * The payment is made by the ERC20 fee proxy contract. + * The payment is made by the ERC20 Conversion fee proxy contract. * @param request the request to pay * @param signerOrProvider the Web3 provider, or signer. Defaults to window.ethereum. * @param paymentSettings payment settings @@ -47,7 +47,8 @@ export async function payAnyToErc20ProxyRequest( } /** - * Encodes the call to pay a request with an ERC20 currency that is different from the request currency (eg. fiat). The payment is made by the ERC20 fee proxy contract. + * Encodes the call to pay a request with an ERC20 currency that is different from the request currency (eg. fiat). + * The payment is made by the ERC20 Conversion fee proxy contract. * @param request request to pay * @param signerOrProvider the Web3 provider, or signer. Defaults to window.ethereum. * @param paymentSettings payment settings From f3ff08876d8fa586c12ed6ce5c48e02f8ff14b9d Mon Sep 17 00:00:00 2001 From: olivier7delf <55892112+olivier7delf@users.noreply.github.com> Date: Thu, 4 Aug 2022 14:39:41 +0200 Subject: [PATCH 020/105] wip --- .../src/payment/batch-proxy-conv.ts | 59 ++++++++----------- 1 file changed, 23 insertions(+), 36 deletions(-) diff --git a/packages/payment-processor/src/payment/batch-proxy-conv.ts b/packages/payment-processor/src/payment/batch-proxy-conv.ts index 1f7417ba9..29b36582e 100644 --- a/packages/payment-processor/src/payment/batch-proxy-conv.ts +++ b/packages/payment-processor/src/payment/batch-proxy-conv.ts @@ -31,51 +31,29 @@ import { IConversionPaymentSettings } from './index'; * but only call ethFeeProxy. It can impact payment detection */ -type metaRequests = { +type MetaRequest = { + paymentNetworkId: number; // ref in batchConversionPayment.sol requests: ClientTypes.IRequestData[]; paymentSettings?: IConversionPaymentSettings[]; amount?: BigNumberish[]; feeAmount?: BigNumberish[]; + version?: string; + batchFee?: number; }; /** * Processes a transaction to pay a batch of requests with an ERC20 or ETH currency that is different from the request currency (eg. fiat). * The payment is made by the ERC20, or ETH fee proxy contract. - * @param requests List of requests - * @param version version of the batch proxy, which can be different from request pn version + * @param metaRequests List of MetaRequest * @param signerOrProvider the Web3 provider, or signer. Defaults to window.ethereum. - * @param batchFee Only for batch ETH: additional fee applied to a batch, between 0 and 1000, default value = 10 * @param overrides optionally, override default transaction values, like gas. */ export async function payBatchConvProxyRequest( - requests: metaRequests, - version: string, - signerOrProvider: providers.Provider | Signer = getProvider(), - batchFee: number, - overrides?: ITransactionOverrides, -): Promise { - const { data, to, value } = prepareBatchPaymentTransaction(requests, version, batchFee); - const signer = getSigner(signerOrProvider); - return signer.sendTransaction({ data, to, value, ...overrides }); -} - -/** - * Processes a transaction to pay a batch of ETH Requests with fees. - * Requests paymentType must be "ETH" or "ERC20" - * @param requests List of requests - * @param version version of the batch proxy, which can be different from request pn version - * @param signerOrProvider the Web3 provider, or signer. Defaults to window.ethereum. - * @param batchFee Only for batch ETH: additional fee applied to a batch, between 0 and 1000, default value = 10 - * @param overrides optionally, override default transaction values, like gas. - */ -export async function payBatchProxyRequest( - requests: ClientTypes.IRequestData[], - version: string, + metaRequests: MetaRequest[], signerOrProvider: providers.Provider | Signer = getProvider(), - batchFee: number, overrides?: ITransactionOverrides, ): Promise { - const { data, to, value } = prepareBatchPaymentTransaction(requests, version, batchFee); + const { data, to, value } = prepareBatchConvPaymentTransaction(metaRequests); const signer = getSigner(signerOrProvider); return signer.sendTransaction({ data, to, value, ...overrides }); } @@ -83,15 +61,24 @@ export async function payBatchProxyRequest( /** * Prepate the transaction to pay a batch of requests through the batch proxy contract, can be used with a Multisig contract. * Requests paymentType must be "ETH" or "ERC20" - * @param requests list of ETH requests to pay - * @param version version of the batch proxy, which can be different from request pn version - * @param batchFee additional fee applied to a batch */ -export function prepareBatchPaymentTransaction( - requests: ClientTypes.IRequestData[], - version: string, - batchFee: number, +export function prepareBatchConvPaymentTransaction( + metaRequests: MetaRequest[], ): IPreparedTransaction { + // we only implement batchRouter + // later, to do gas optimizaton, we will implement the others batch functions, + // at this moment, paymentNetworkId will be useful + + const metaRequest = metaRequests[0]; + + for (let i = 0; i < metaRequests.length; i++) { + if (metaRequests[i].paymentNetworkId === 0) { + } else if (metaRequests[i].paymentNetworkId === 1) { + } else if (metaRequests[i].paymentNetworkId === 2) { + } else if (metaRequests[i].paymentNetworkId === 3) { + } else if (metaRequests[i].paymentNetworkId === 4) { + } + } const encodedTx = encodePayBatchRequest(requests); const proxyAddress = getBatchProxyAddress(requests[0], version); let totalAmount = 0; From ebd3ff45a79f430ce2a4f050d5141d4c4f362b0b Mon Sep 17 00:00:00 2001 From: olivier7delf <55892112+olivier7delf@users.noreply.github.com> Date: Thu, 4 Aug 2022 17:05:27 +0200 Subject: [PATCH 021/105] refacto receive function - add comments to test functions --- .../src/contracts/BatchConversionPayments.sol | 11 +-- .../src/contracts/BatchPaymentsPublic.sol | 8 ++ .../contracts/BatchConversionPayments.test.ts | 95 ++++++++++++------- 3 files changed, 72 insertions(+), 42 deletions(-) diff --git a/packages/smart-contracts/src/contracts/BatchConversionPayments.sol b/packages/smart-contracts/src/contracts/BatchConversionPayments.sol index fd747be38..aea3cb131 100644 --- a/packages/smart-contracts/src/contracts/BatchConversionPayments.sol +++ b/packages/smart-contracts/src/contracts/BatchConversionPayments.sol @@ -105,15 +105,6 @@ contract BatchConversionPayments is BatchPaymentsPublic { batchConversionFee = 0; } - // batch Eth requires batch contract to receive funds from ethFeeProxy with a value = 0 - // and also from paymentEthConversionProxy with a value > 0 - receive() external payable { - require( - address(msg.sender) == address(paymentEthConversionProxy) || msg.value == 0, - 'Non-payable' - ); - } - /** * @notice Batch payments on different payment networks at once. * - batchERC20ConversionPaymentsMultiTokens, paymentNetworks: 0 @@ -289,6 +280,7 @@ contract BatchConversionPayments is BatchPaymentsPublic { address payable _feeAddress ) public payable { uint256 contractBalance = address(this).balance; + payerAuthorized = true; // Batch contract pays the requests through EthConversionProxy for (uint256 i = 0; i < requestsInfo.length; i++) { @@ -314,6 +306,7 @@ contract BatchConversionPayments is BatchPaymentsPublic { // Batch contract transfers the remaining ethers to the payer (bool sendBackSuccess, ) = payable(msg.sender).call{value: address(this).balance}(''); require(sendBackSuccess, 'Could not send remaining funds to the payer'); + payerAuthorized = false; } /* diff --git a/packages/smart-contracts/src/contracts/BatchPaymentsPublic.sol b/packages/smart-contracts/src/contracts/BatchPaymentsPublic.sol index d50eb280a..dcf6c30f4 100644 --- a/packages/smart-contracts/src/contracts/BatchPaymentsPublic.sol +++ b/packages/smart-contracts/src/contracts/BatchPaymentsPublic.sol @@ -27,6 +27,8 @@ contract BatchPaymentsPublic is Ownable { IEthereumFeeProxy public paymentEthProxy; uint256 public batchFee; + // payerAuthorized is set to true only when needed for batch Eth conversion + bool internal payerAuthorized; struct Token { address tokenAddress; @@ -50,6 +52,12 @@ contract BatchPaymentsPublic is Ownable { batchFee = 0; } + // batch Eth requires batch contract to receive funds from ethFeeProxy with a value = 0 + // and also from paymentEthConversionProxy with a value > 0 + receive() external payable { + require (payerAuthorized || msg.value == 0, 'Non-payable'); + } + /** * @notice Send a batch of Eth payments w/fees with paymentReferences to multiple accounts. * If one payment failed, the whole batch is reverted diff --git a/packages/smart-contracts/test/contracts/BatchConversionPayments.test.ts b/packages/smart-contracts/test/contracts/BatchConversionPayments.test.ts index 1b98a5d47..19b2aebcb 100644 --- a/packages/smart-contracts/test/contracts/BatchConversionPayments.test.ts +++ b/packages/smart-contracts/test/contracts/BatchConversionPayments.test.ts @@ -34,7 +34,7 @@ describe('contract: BatchErc20ConversionPayments', () => { let feeAddress: string; let batchAddress: string; let signer: Signer; - let toSigner: Signer; + let xSigner: Signer; const basicFee = 10; const batchFee = 100; const batchConvFee = 100; @@ -103,7 +103,7 @@ describe('contract: BatchErc20ConversionPayments', () => { /** Function used to emit events of batch conversion proxy */ let emitOneTx: Function; /** - * Function batch conversion, it can be the batchRouter function, used with conversion args, + * @notice Function batch conversion, it can be the batchRouter function, used with conversion args, * or directly batchERC20ConversionPaymentsMultiTokens * */ let batchConvFunction: ( @@ -115,10 +115,8 @@ describe('contract: BatchErc20ConversionPayments', () => { let argTemplate: Function; before(async () => { - let _; [from, to, feeAddress] = (await ethers.getSigners()).map((s) => s.address); - [signer, _, _, toSigner] = await ethers.getSigners(); - _; + [signer, xSigner, xSigner, xSigner] = await ethers.getSigners(); chainlinkPath = chainlinkConversionPath.connect(network.name, signer); erc20FeeProxy = await new ERC20FeeProxy__factory(signer).deploy(); ethereumFeeProxy = await new EthereumFeeProxy__factory(signer).deploy(); @@ -127,7 +125,6 @@ describe('contract: BatchErc20ConversionPayments', () => { chainlinkPath.address, await signer.getAddress(), ); - console.log('testErc20ConversionProxy', testErc20ConversionProxy.address); testEthConversionProxy = await new EthConversionProxy__factory(signer).deploy( ethereumFeeProxy.address, chainlinkPath.address, @@ -158,6 +155,9 @@ describe('contract: BatchErc20ConversionPayments', () => { return conversionAmountToPay.mul(batchConvFee).div(10000); }; + /** + * @notice it gets the conversions including fees to be paid, and it set the requestInfo + */ const initConvToPayAndRequestInfo = async ( _recipient: string, _path: string[], @@ -223,9 +223,10 @@ describe('contract: BatchErc20ConversionPayments', () => { expect(batchDiffBalance).to.equals('0', 'batchDiffBalance'); }); - /** Function used to calcul the expected new balance for batch conversion. + /** + * @notice Used to calcul the expected new ERC20 balance for batch conversion. * It can also be used for batch IF batchFee == batchConvFee - * The 3rd arg is used to calcul batch fees + * @param _conversionsToPay_results is used to calcul batch fees, in case of multiple payments */ const calculERC20Balances = ( _conversionToPay_result: BigNumber, @@ -238,10 +239,17 @@ describe('contract: BatchErc20ConversionPayments', () => { toDiffBalanceExpected = toDiffBalanceExpected.add(_conversionToPay_result); feeDiffBalanceExpected = feeDiffBalanceExpected.add(_conversionFees_result); - if (_conversionsToPay_results.length > 0) calculBatchFeeBalances(_conversionsToPay_results); + if (_conversionsToPay_results.length > 0) + calculERC20BatchFeeBalances(_conversionsToPay_results); }; - const calculBatchFeeBalances = (_conversionsToPay_results: BigNumber[]) => { + /** + * @notice Used to calcul the expected new ERC20 fee batch balance for batch conversion. + * @param _conversionsToPay_results is used to calcul batch fees, it case of payments multiple + * @dev in case of payments multiple, we sum the amount paid, and then, we calcul the fees amount + * because the sum(batchFeeToPay(amountPay[i])) != batchFeeToPay(sum(amountPay[i])) + */ + const calculERC20BatchFeeBalances = (_conversionsToPay_results: BigNumber[]) => { let sumToPay = BigNumber.from(0); for (let i = 0; i < _conversionsToPay_results.length; i++) { sumToPay = sumToPay.add(_conversionsToPay_results[i]); @@ -250,6 +258,10 @@ describe('contract: BatchErc20ConversionPayments', () => { feeDiffBalanceExpected = feeDiffBalanceExpected.add(batchFeeToPay(sumToPay)); }; + /** + * @notice update requestInfo, do an ERC20 conv batch payment and calcul the balances + * @param path to update the resquestInfo + */ const transferOneTokenConv = async (path: string[]) => { await initConvToPayAndRequestInfo(to, path, amountInFiat, feesAmountInFiat, 0, chainlinkPath); @@ -266,7 +278,12 @@ describe('contract: BatchErc20ConversionPayments', () => { calculERC20Balances(conversionToPay.result, conversionFees.result, [conversionToPay.result]); }; - const twoTransferOneTokenConv = async (path2: string[], nTimes: number) => { + /** + * @notice generate nTimes 2 requestInfos, do an ERC20 conv batch payment with theses 2*nTimes requests + * and calcul the balances + * @param path2 to update the second resquestInfo + */ + const transferTokensConv = async (path2: string[], nTimes: number) => { const coef = 2; const amountInFiat2 = BigNumber.from(amountInFiat).mul(coef).toString(); const feesAmountInFiat2 = BigNumber.from(feesAmountInFiat).mul(coef).toString(); @@ -323,7 +340,11 @@ describe('contract: BatchErc20ConversionPayments', () => { } }; - const ERC20TestSuite = (suiteName: string) => { + /** + * @notice it contains all the tests related to the ERC20 batch payment, and its context required + * @param erc20Function is the batch function name tested: "batchRouter" or "batchERC20ConversionPaymentsMultiTokens" + */ + const ERC20TestSuite = (erc20Function: string) => { emitOneTx = ( result: Chai.Assertion, requestInfo: RequestInfo, @@ -357,7 +378,7 @@ describe('contract: BatchErc20ConversionPayments', () => { }); const setBatchConvFunction = async (_signer: Signer) => { - if (suiteName === 'batchRouter') { + if (erc20Function === 'batchRouter') { batchConvFunction = testBatchConversionProxy.connect(_signer).batchRouter; argTemplate = (requestInfos: RequestInfo[]) => { return [ @@ -369,7 +390,7 @@ describe('contract: BatchErc20ConversionPayments', () => { ]; }; } - if (suiteName === 'batchERC20ConversionPaymentsMultiTokens') { + if (erc20Function === 'batchERC20ConversionPaymentsMultiTokens') { batchConvFunction = testBatchConversionProxy.connect(_signer).batchERC20ConversionPaymentsMultiTokens; argTemplate = (requestInfos: RequestInfo[]) => { @@ -380,7 +401,7 @@ describe('contract: BatchErc20ConversionPayments', () => { before(() => { setBatchConvFunction(signer); }); - describe(suiteName, () => { + describe(erc20Function, () => { describe('batchERC20ConversionPaymentsMultiTokens with DAI', async () => { it('allows to transfer DAI tokens for USD payment', async () => { await transferOneTokenConv(path); @@ -390,19 +411,19 @@ describe('contract: BatchErc20ConversionPayments', () => { await transferOneTokenConv(path); }); it('allows to transfer 2 transactions DAI tokens for USD payment', async function () { - await twoTransferOneTokenConv(path, 1); + await transferTokensConv(path, 1); }); - it('allows to transfer DAI tokens for EUR payment - GAS', async () => { + it('allows to transfer DAI tokens for EUR payment', async () => { path = [EUR_hash, USD_hash, DAI_address]; await transferOneTokenConv(path); }); - it('allows to transfer 2 transactions DAI tokens for USD and EUR payment - GAS', async function () { + it('allows to transfer 2 transactions DAI tokens for USD and EUR payments', async function () { const path2 = [EUR_hash, USD_hash, DAI_address]; - await twoTransferOneTokenConv(path2, 1); + await transferTokensConv(path2, 1); }); - it('allows to transfer two kinds of tokens for USD - GAS', async function () { + it('allows to transfer two kinds of tokens for USD', async function () { const path2 = [USD_hash, fakeFAU_address]; - await twoTransferOneTokenConv(path2, 1); + await transferTokensConv(path2, 1); }); }); }); @@ -435,8 +456,8 @@ describe('contract: BatchErc20ConversionPayments', () => { }); it('Not enough allowance', async function () { - // toSigner connect to the batch function - setBatchConvFunction(toSigner); + // xSigner connect to the batch function + setBatchConvFunction(xSigner); await expect(batchConvFunction(argTemplate([requestInfo]), feeAddress)).to.be.revertedWith( 'Not sufficient allowance for batch to pay', ); @@ -445,21 +466,21 @@ describe('contract: BatchErc20ConversionPayments', () => { }); it('Not enough funds', async function () { - // increase toSigner allowance + // increase xSigner allowance await testERC20 - .connect(toSigner) + .connect(xSigner) .approve(testBatchConversionProxy.address, thousandWith18Decimal); - // toSigner connect to the batch function - setBatchConvFunction(toSigner); + // xSigner connect to the batch function + setBatchConvFunction(xSigner); await expect(batchConvFunction(argTemplate([requestInfo]), feeAddress)).to.be.revertedWith( 'not enough funds, including fees', ); // reset: - // - decrease toSigner allowance + // - decrease xSigner allowance // - connect with signer account - await testERC20.connect(toSigner).approve(testBatchConversionProxy.address, '0'); + await testERC20.connect(xSigner).approve(testBatchConversionProxy.address, '0'); testERC20.connect(signer); setBatchConvFunction(signer); }); @@ -545,8 +566,12 @@ describe('contract: BatchErc20ConversionPayments', () => { }); }); - const EthTestSuite = (suiteName: string) => { - describe(`Test ETH ${suiteName} functions`, () => { + /** + * @notice it contains all the tests related to the Eth batch payment, and its context required + * @param ethFunction is the batch function name tested: "batchRouter" or "batchEthConversionPaymentsWithReference" + */ + const EthTestSuite = (ethFunction: string) => { + describe(`Test ETH ${ethFunction} functions`, () => { let beforeEthBalanceTo: BigNumber; let beforeEthBalanceFee: BigNumber; let beforeEthBalance: BigNumber; @@ -559,8 +584,12 @@ describe('contract: BatchErc20ConversionPayments', () => { let inputs: Array; const pathUsdEth = [USD_hash, ETH_hash]; + /** + * @notice it modify the Eth batch inputs if needed, depending of the function used: ethFunction + * @param inputs a list of requestInfo + */ const getInputs = (inputs: Array) => { - if (suiteName !== 'batchEthConversionPaymentsWithReference') { + if (ethFunction !== 'batchEthConversionPaymentsWithReference') { return [ { paymentNetworkId: '3', @@ -573,7 +602,7 @@ describe('contract: BatchErc20ConversionPayments', () => { }; before(() => { - if (suiteName === 'batchEthConversionPaymentsWithReference') { + if (ethFunction === 'batchEthConversionPaymentsWithReference') { batchConvFunction = testBatchConversionProxy.batchEthConversionPaymentsWithReference; } else { batchConvFunction = testBatchConversionProxy.batchRouter; From 1c07569df6ca905fc08be43c7d9dbe32ee1135ae Mon Sep 17 00:00:00 2001 From: olivier7delf <55892112+olivier7delf@users.noreply.github.com> Date: Thu, 4 Aug 2022 17:28:13 +0200 Subject: [PATCH 022/105] comments about unique token in smart contract --- .../smart-contracts/src/contracts/BatchConversionPayments.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/smart-contracts/src/contracts/BatchConversionPayments.sol b/packages/smart-contracts/src/contracts/BatchConversionPayments.sol index aea3cb131..967827b3c 100644 --- a/packages/smart-contracts/src/contracts/BatchConversionPayments.sol +++ b/packages/smart-contracts/src/contracts/BatchConversionPayments.sol @@ -175,7 +175,7 @@ contract BatchConversionPayments is BatchPaymentsPublic { RequestInfo[] calldata requestsInfo, address _feeAddress ) public { - // Aggregate _maxToSpend by token + // a list of unique tokens, with the sum of maxToSpend by token Token[] memory uTokens = new Token[](requestsInfo.length); for (uint256 i = 0; i < requestsInfo.length; i++) { for (uint256 k = 0; k < requestsInfo.length; k++) { From 2357f601ea5e16e237af7cd2ce2329e04aaf7ed9 Mon Sep 17 00:00:00 2001 From: olivier7delf <55892112+olivier7delf@users.noreply.github.com> Date: Thu, 4 Aug 2022 17:29:45 +0200 Subject: [PATCH 023/105] prettier --- packages/smart-contracts/src/contracts/BatchPaymentsPublic.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/smart-contracts/src/contracts/BatchPaymentsPublic.sol b/packages/smart-contracts/src/contracts/BatchPaymentsPublic.sol index dcf6c30f4..8f165e589 100644 --- a/packages/smart-contracts/src/contracts/BatchPaymentsPublic.sol +++ b/packages/smart-contracts/src/contracts/BatchPaymentsPublic.sol @@ -55,7 +55,7 @@ contract BatchPaymentsPublic is Ownable { // batch Eth requires batch contract to receive funds from ethFeeProxy with a value = 0 // and also from paymentEthConversionProxy with a value > 0 receive() external payable { - require (payerAuthorized || msg.value == 0, 'Non-payable'); + require(payerAuthorized || msg.value == 0, 'Non-payable'); } /** From b721ef4a3df2304b23a564699c9a146cc60c8e0d Mon Sep 17 00:00:00 2001 From: olivier7delf <55892112+olivier7delf@users.noreply.github.com> Date: Thu, 4 Aug 2022 17:33:23 +0200 Subject: [PATCH 024/105] Update packages/smart-contracts/src/contracts/BatchPaymentsPublic.sol Co-authored-by: Yo <56731761+yomarion@users.noreply.github.com> --- .../src/contracts/BatchPaymentsPublic.sol | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/smart-contracts/src/contracts/BatchPaymentsPublic.sol b/packages/smart-contracts/src/contracts/BatchPaymentsPublic.sol index 8f165e589..43cc054c2 100644 --- a/packages/smart-contracts/src/contracts/BatchPaymentsPublic.sol +++ b/packages/smart-contracts/src/contracts/BatchPaymentsPublic.sol @@ -59,14 +59,14 @@ contract BatchPaymentsPublic is Ownable { } /** - * @notice Send a batch of Eth payments w/fees with paymentReferences to multiple accounts. - * If one payment failed, the whole batch is reverted - * @param _recipients List of recipients accounts. - * @param _amounts List of amounts, corresponding to recipients[]. - * @param _paymentReferences List of paymentRefs, corr. to the recipients[]. - * @param _feeAmounts List of amounts of the payment fee, corr. to the recipients[]. + * @notice Send a batch of ETH (or EVM native token) payments with fees and paymentReferences to multiple accounts. + * If one payment fails, the whole batch reverts. + * @param _recipients List of recipient accounts. + * @param _amounts List of amounts, matching recipients[]. + * @param _paymentReferences List of paymentRefs, matching recipients[]. + * @param _feeAmounts List fee amounts, matching recipients[]. * @param _feeAddress The fee recipient. - * @dev It uses EthereumFeeProxy to pay an invoice and fees, with a payment reference. + * @dev It uses EthereumFeeProxy to pay an invoice and fees with a payment reference. * Make sure: msg.value >= sum(_amouts)+sum(_feeAmounts)+sumBatchFeeAmount */ function batchEthPaymentsWithReference( From 83398eabdf63890194c5b7bb14967ccd0ac864e9 Mon Sep 17 00:00:00 2001 From: olivier7delf <55892112+olivier7delf@users.noreply.github.com> Date: Thu, 4 Aug 2022 17:35:13 +0200 Subject: [PATCH 025/105] Update packages/smart-contracts/src/contracts/BatchPaymentsPublic.sol Co-authored-by: Yo <56731761+yomarion@users.noreply.github.com> --- .../src/contracts/BatchPaymentsPublic.sol | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/smart-contracts/src/contracts/BatchPaymentsPublic.sol b/packages/smart-contracts/src/contracts/BatchPaymentsPublic.sol index 43cc054c2..00af40b73 100644 --- a/packages/smart-contracts/src/contracts/BatchPaymentsPublic.sol +++ b/packages/smart-contracts/src/contracts/BatchPaymentsPublic.sol @@ -114,16 +114,16 @@ contract BatchPaymentsPublic is Ownable { } /** - * @notice Send a batch of erc20 payments w/fees with paymentReferences to multiple accounts. - * @param _tokenAddress Token to transact with. - * @param _recipients List of recipients accounts. - * @param _amounts List of amounts, corresponding to recipients[]. - * @param _paymentReferences List of paymentRefs, corr. to the recipients[] and . - * @param _feeAmounts List of amounts of the payment fee, corr. to the recipients[]. + * @notice Send a batch of ERC20 payments with fees and paymentReferences to multiple accounts. + * @param _tokenAddress Token used for all the payments. + * @param _recipients List of recipient accounts. + * @param _amounts List of amounts, matching recipients[]. + * @param _paymentReferences List of paymentRefs, matching recipients[]. + * @param _feeAmounts List of payment fee amounts, matching recipients[]. * @param _feeAddress The fee recipient. * @dev Uses ERC20FeeProxy to pay an invoice and fees, with a payment reference. - * Make sure the contract has allowance to spend the payer token. - * Make sure the payer has enough tokens to pay the amount, the fee, the batch fee + * Make sure this contract has enough allowance to spend the payer's token. + * Make sure the payer has enough tokens to pay the amount, the fee, and the batch fee. */ function batchERC20PaymentsWithReference( address _tokenAddress, From d5c11ec7219fd1ce0b023e5158e5b06a6e29335f Mon Sep 17 00:00:00 2001 From: olivier7delf <55892112+olivier7delf@users.noreply.github.com> Date: Thu, 4 Aug 2022 17:35:31 +0200 Subject: [PATCH 026/105] Update packages/smart-contracts/src/contracts/BatchPaymentsPublic.sol Co-authored-by: Yo <56731761+yomarion@users.noreply.github.com> --- packages/smart-contracts/src/contracts/BatchPaymentsPublic.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/smart-contracts/src/contracts/BatchPaymentsPublic.sol b/packages/smart-contracts/src/contracts/BatchPaymentsPublic.sol index 00af40b73..941840d41 100644 --- a/packages/smart-contracts/src/contracts/BatchPaymentsPublic.sol +++ b/packages/smart-contracts/src/contracts/BatchPaymentsPublic.sol @@ -150,7 +150,7 @@ contract BatchPaymentsPublic is Ownable { IERC20 requestedToken = IERC20(_tokenAddress); require( requestedToken.allowance(msg.sender, address(this)) >= amount, - 'Not sufficient allowance for batch to pay' + 'Insufficient allowance for batch to pay' ); require(requestedToken.balanceOf(msg.sender) >= amount, 'not enough funds'); require( From e704f4eae3c98a2464ee9964ad82b3b9ad629256 Mon Sep 17 00:00:00 2001 From: olivier7delf <55892112+olivier7delf@users.noreply.github.com> Date: Thu, 4 Aug 2022 17:35:42 +0200 Subject: [PATCH 027/105] Update packages/smart-contracts/src/contracts/BatchPaymentsPublic.sol Co-authored-by: Yo <56731761+yomarion@users.noreply.github.com> --- .../src/contracts/BatchPaymentsPublic.sol | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/smart-contracts/src/contracts/BatchPaymentsPublic.sol b/packages/smart-contracts/src/contracts/BatchPaymentsPublic.sol index 941840d41..be24081ed 100644 --- a/packages/smart-contracts/src/contracts/BatchPaymentsPublic.sol +++ b/packages/smart-contracts/src/contracts/BatchPaymentsPublic.sol @@ -190,16 +190,16 @@ contract BatchPaymentsPublic is Ownable { } /** - * @notice Send a batch of erc20 payments on multiple tokens w/fees with paymentReferences to multiple accounts. + * @notice Send a batch of ERC20 payments with fees and paymentReferences to multiple accounts, with multiple tokens. * @param _tokenAddresses List of tokens to transact with. - * @param _recipients List of recipients accounts. - * @param _amounts List of amounts, corresponding to recipients[]. - * @param _paymentReferences List of paymentRefs, corr. to the recipients[]. - * @param _feeAmounts List of amounts of the payment fee, corr. to the recipients[]. + * @param _recipients List of recipient accounts. + * @param _amounts List of amounts, matching recipients[]. + * @param _paymentReferences List of paymentRefs, matching recipients[]. + * @param _feeAmounts List of amounts of the payment fee, matching recipients[]. * @param _feeAddress The fee recipient. * @dev It uses ERC20FeeProxy to pay an invoice and fees, with a payment reference. - * Make sure the contract has allowance to spend the payer token. - * Make sure the payer has enough tokens to pay the amount, the fee, the batch fee + * Make sure this contract has enough allowance to spend the payer's token. + * Make sure the payer has enough tokens to pay the amount, the fee, and the batch fee. */ function batchERC20PaymentsMultiTokensWithReference( address[] calldata _tokenAddresses, From 978b701b7487c2a04caf6777498698cd608da134 Mon Sep 17 00:00:00 2001 From: olivier7delf <55892112+olivier7delf@users.noreply.github.com> Date: Thu, 4 Aug 2022 17:36:08 +0200 Subject: [PATCH 028/105] Update packages/smart-contracts/src/contracts/BatchConversionPayments.sol Co-authored-by: Yo <56731761+yomarion@users.noreply.github.com> --- .../src/contracts/BatchConversionPayments.sol | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/smart-contracts/src/contracts/BatchConversionPayments.sol b/packages/smart-contracts/src/contracts/BatchConversionPayments.sol index 967827b3c..0adcc191f 100644 --- a/packages/smart-contracts/src/contracts/BatchConversionPayments.sol +++ b/packages/smart-contracts/src/contracts/BatchConversionPayments.sol @@ -264,16 +264,16 @@ contract BatchConversionPayments is BatchPaymentsPublic { } /** - * @notice Send a batch of Eth conversion payments w/fees with paymentReferences to multiple accounts. - * If one payment failed, the whole batch is reverted. + * @notice Send a batch of ETH conversion payments with fees and paymentReferences to multiple accounts. + * If one payment fails, the whole batch is reverted. * @param requestsInfo List of requestInfos, each one containing all the information of a request. * _maxToSpend is not used in this function. * @param _feeAddress The fee recipient. * @dev It uses EthereumConversionProxy to pay an invoice and fees. * Please: - * Notice that if there is not enough ether sent to the contract, - * it emit the follow error: "revert paymentProxy transferExactEthWithReferenceAndFee failed" - * This choice reduces the gas significantly, otherwise, it would be necessary to make multiple calls to chainlink.. + * Note that if there is not enough ether attached to the function call, + * the following error is thrown: "revert paymentProxy transferExactEthWithReferenceAndFee failed" + * This choice reduces the gas significantly, by delegating the whole conversion to the payment proxy. */ function batchEthConversionPaymentsWithReference( RequestInfo[] calldata requestsInfo, From ef4b86fdf988c481e7d4400cc6b8c0d38a0636f4 Mon Sep 17 00:00:00 2001 From: olivier7delf <55892112+olivier7delf@users.noreply.github.com> Date: Thu, 4 Aug 2022 17:36:50 +0200 Subject: [PATCH 029/105] Update packages/smart-contracts/src/contracts/BatchConversionPayments.sol Co-authored-by: Yo <56731761+yomarion@users.noreply.github.com> --- .../src/contracts/BatchConversionPayments.sol | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/smart-contracts/src/contracts/BatchConversionPayments.sol b/packages/smart-contracts/src/contracts/BatchConversionPayments.sol index 0adcc191f..7d44c5d82 100644 --- a/packages/smart-contracts/src/contracts/BatchConversionPayments.sol +++ b/packages/smart-contracts/src/contracts/BatchConversionPayments.sol @@ -11,13 +11,13 @@ import './BatchPaymentsPublic.sol'; * @notice This contract makes multiple conversion payments with references, in one transaction: * - on: * - ERC20 tokens: using Erc20ConversionProxy and ERC20FeeProxy - * - Native token: as Eth, using EthConversionProxy and EthereumFeeProxy + * - Native tokens: (e.g. ETH) using EthConversionProxy and EthereumFeeProxy * - to: multiple addresses * - fees: conversion proxy fees and additional batch conversion fees are paid to the same address. - * batchRouter is the main function to batch all kind of payments at once. - * If one transaction of the batch fail, all transactions are reverted. - * @dev Please notify than fees are now divided by 10_000 instead of 1_000 in previous version - * batchRouter is the main function, but others batch payment functions are "public" in order to do + * batchRouter is the main function to batch all kinds of payments at once. + * If one transaction of the batch fails, all transactions are reverted. + * @dev Note that fees have 4 decimals (instead of 3 in a previous version) + * batchRouter is the main function, but other batch payment functions are "public" in order to do * gas optimization in some cases. */ contract BatchConversionPayments is BatchPaymentsPublic { From 23b29801e4fc4e3f3fb71d4f9e217abe0614f371 Mon Sep 17 00:00:00 2001 From: olivier7delf <55892112+olivier7delf@users.noreply.github.com> Date: Thu, 4 Aug 2022 17:37:03 +0200 Subject: [PATCH 030/105] Update packages/smart-contracts/src/contracts/BatchConversionPayments.sol Co-authored-by: Yo <56731761+yomarion@users.noreply.github.com> --- .../smart-contracts/src/contracts/BatchConversionPayments.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/smart-contracts/src/contracts/BatchConversionPayments.sol b/packages/smart-contracts/src/contracts/BatchConversionPayments.sol index 7d44c5d82..5fff603c7 100644 --- a/packages/smart-contracts/src/contracts/BatchConversionPayments.sol +++ b/packages/smart-contracts/src/contracts/BatchConversionPayments.sol @@ -202,7 +202,7 @@ contract BatchConversionPayments is BatchPaymentsPublic { // Check proxy's allowance from user, and user's funds to pay approximated amounts. require( requestedToken.allowance(msg.sender, address(this)) >= uTokens[k].amountAndFee, - 'Not sufficient allowance for batch to pay' + 'Insufficient allowance for batch to pay' ); require( requestedToken.balanceOf(msg.sender) >= uTokens[k].amountAndFee + uTokens[k].batchFeeAmount, From abc54715c0457e685f8ffc617e7569267b064361 Mon Sep 17 00:00:00 2001 From: olivier7delf <55892112+olivier7delf@users.noreply.github.com> Date: Thu, 4 Aug 2022 17:37:19 +0200 Subject: [PATCH 031/105] Update packages/smart-contracts/src/contracts/BatchConversionPayments.sol Co-authored-by: Yo <56731761+yomarion@users.noreply.github.com> --- .../src/contracts/BatchConversionPayments.sol | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/smart-contracts/src/contracts/BatchConversionPayments.sol b/packages/smart-contracts/src/contracts/BatchConversionPayments.sol index 5fff603c7..0f331eec9 100644 --- a/packages/smart-contracts/src/contracts/BatchConversionPayments.sol +++ b/packages/smart-contracts/src/contracts/BatchConversionPayments.sol @@ -31,15 +31,15 @@ contract BatchConversionPayments is BatchPaymentsPublic { uint256 public basicFee; /** - * @dev All the information of a request, excepted the feeAddress - * _recipient Recipients address of the payement + * @dev All the information of a request, except the feeAddress + * _recipient Recipient address of the payment * _requestAmount Request amount in fiat * _path Conversion path - * _paymentReference References of the payment related - * _feeAmount The amount in fiat of the payment fee - * _maxToSpend Amounts max in token that we can spend on the behalf of the user: + * _paymentReference Unique reference of the payment + * _feeAmount The fee amount denominated in the first currency of `_path` + * _maxToSpend Maximum amount the payer wants to spend, denominated in the last currency of `_path`: * it includes fee proxy but NOT the batchConversionFee - * _maxRateTimespan Max times span with the oldestrate, ignored if zero + * _maxRateTimespan Max acceptable times span for conversion rates, ignored if zero */ struct RequestInfo { address recipient; From 4367d979ef2790ae66022cf73e83777be15eeaf9 Mon Sep 17 00:00:00 2001 From: olivier7delf <55892112+olivier7delf@users.noreply.github.com> Date: Thu, 4 Aug 2022 17:37:32 +0200 Subject: [PATCH 032/105] Update packages/smart-contracts/src/contracts/BatchConversionPayments.sol Co-authored-by: Yo <56731761+yomarion@users.noreply.github.com> --- .../smart-contracts/src/contracts/BatchConversionPayments.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/smart-contracts/src/contracts/BatchConversionPayments.sol b/packages/smart-contracts/src/contracts/BatchConversionPayments.sol index 0f331eec9..9dad83393 100644 --- a/packages/smart-contracts/src/contracts/BatchConversionPayments.sol +++ b/packages/smart-contracts/src/contracts/BatchConversionPayments.sol @@ -52,7 +52,7 @@ contract BatchConversionPayments is BatchPaymentsPublic { } /** - * @dev It is the structure of the input for the function from contract BatchPaymentsPublic + * @dev BatchPaymentsPublic contract input structure. */ struct RequestsInfoParent { address[] tokenAddresses; From 4f683a31230201a36c904424be25814aa4e95400 Mon Sep 17 00:00:00 2001 From: olivier7delf <55892112+olivier7delf@users.noreply.github.com> Date: Thu, 4 Aug 2022 17:38:06 +0200 Subject: [PATCH 033/105] Update packages/smart-contracts/src/contracts/BatchConversionPayments.sol Co-authored-by: Yo <56731761+yomarion@users.noreply.github.com> --- .../src/contracts/BatchConversionPayments.sol | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/packages/smart-contracts/src/contracts/BatchConversionPayments.sol b/packages/smart-contracts/src/contracts/BatchConversionPayments.sol index 9dad83393..a601e4cec 100644 --- a/packages/smart-contracts/src/contracts/BatchConversionPayments.sol +++ b/packages/smart-contracts/src/contracts/BatchConversionPayments.sol @@ -63,12 +63,10 @@ contract BatchConversionPayments is BatchPaymentsPublic { } /** - * @dev Used by batchRouter to hold information for any kind of request. - * - paymentNetworkId requests are grouped by paymentType to be paid with the appropriate function. - * More details in batchRouter description. - * - requestsInfo all informations required for conversion requests to be paid (=> paymentNetworkId equal 0 or 3) - * - requestsInfoParent all informations required for None-conversion requests to be paid - * (=> paymentNetworkId equal 1, 2, or 4) + * @dev Used by the batchRouter to handle information for heterogeneous batches, grouped by payment network. + * - paymentNetworkId: from 0 to 4, cf. `batchRouter()` method. + * - requestsInfo all the data required for conversion requests to be paid, for paymentNetworkId = 0 or 3 + * - requestsInfoParent all the data required to pay requests without conversion, for paymentNetworkId = 1, 2, or 4 */ struct MetaRequestsInfo { uint256 paymentNetworkId; From dde44dbc04f8ef1959ccfc0a5e5c428766f6539b Mon Sep 17 00:00:00 2001 From: olivier7delf <55892112+olivier7delf@users.noreply.github.com> Date: Thu, 4 Aug 2022 17:38:24 +0200 Subject: [PATCH 034/105] Update packages/smart-contracts/src/contracts/BatchConversionPayments.sol Co-authored-by: Yo <56731761+yomarion@users.noreply.github.com> --- .../src/contracts/BatchConversionPayments.sol | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/smart-contracts/src/contracts/BatchConversionPayments.sol b/packages/smart-contracts/src/contracts/BatchConversionPayments.sol index a601e4cec..b59603df6 100644 --- a/packages/smart-contracts/src/contracts/BatchConversionPayments.sol +++ b/packages/smart-contracts/src/contracts/BatchConversionPayments.sol @@ -75,11 +75,11 @@ contract BatchConversionPayments is BatchPaymentsPublic { } /** - * @param _paymentErc20Proxy The address to the ERC20 fee payment proxy to use. - * @param _paymentEthProxy The address to the Ethereum fee payment proxy to use. - * @param _paymentErc20ConversionProxy The address of the ERC20 Conversion payment proxy to use. - * @param _paymentEthConversionFeeProxy The address of the Ethereum Conversion payment proxy to use. - * @param _chainlinkConversionPathAddress The address of the conversion path contract + * @param _paymentErc20Proxy The ERC20 payment proxy address to use. + * @param _paymentEthProxy The ETH payment proxy address to use. + * @param _paymentErc20ConversionProxy The ERC20 Conversion payment proxy address to use. + * @param _paymentEthConversionFeeProxy The ETH Conversion payment proxy address to use. + * @param _chainlinkConversionPathAddress The conversion path contract address * @param _owner Owner of the contract. */ constructor( From f20d8020216c0878bd22a1270c76812303efded9 Mon Sep 17 00:00:00 2001 From: olivier7delf <55892112+olivier7delf@users.noreply.github.com> Date: Thu, 4 Aug 2022 17:40:46 +0200 Subject: [PATCH 035/105] Update packages/smart-contracts/src/contracts/BatchConversionPayments.sol Co-authored-by: Yo <56731761+yomarion@users.noreply.github.com> --- .../src/contracts/BatchConversionPayments.sol | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/smart-contracts/src/contracts/BatchConversionPayments.sol b/packages/smart-contracts/src/contracts/BatchConversionPayments.sol index b59603df6..b9708f913 100644 --- a/packages/smart-contracts/src/contracts/BatchConversionPayments.sol +++ b/packages/smart-contracts/src/contracts/BatchConversionPayments.sol @@ -105,15 +105,15 @@ contract BatchConversionPayments is BatchPaymentsPublic { /** * @notice Batch payments on different payment networks at once. - * - batchERC20ConversionPaymentsMultiTokens, paymentNetworks: 0 - * - batchERC20PaymentsWithReference, paymentNetworks: 1 - * - batchERC20PaymentsMultiTokensWithReference, paymentNetworks: 2 - * - batchEthConversionPaymentsWithReference, paymentNetworks: 3 - * - batchEthPaymentsWithReference, paymentNetworks: 4 * @param metaRequestsInfos contains paymentNetworkId and requestsInfo - * @param _feeAddress The address of the proxy to send the fees - * @dev batchRouter reduces gas consumption if you are using more than a single payment networks, - * else, it is more efficient to use the adapted batch function. + * - batchERC20ConversionPaymentsMultiTokens, paymentNetworkId=0 + * - batchERC20PaymentsWithReference, paymentNetworkId=1 + * - batchERC20PaymentsMultiTokensWithReference, paymentNetworkId=2 + * - batchEthConversionPaymentsWithReference, paymentNetworkId=3 + * - batchEthPaymentsWithReference, paymentNetworkId=4 + * @param _feeAddress The address where fees should be paid + * @dev batchRouter only reduces gas consumption when using more than a single payment network. + * For single payment network payments, it is more efficient to use the suited batch function. */ function batchRouter(MetaRequestsInfo[] calldata metaRequestsInfos, address _feeAddress) external From a0471493d438558239a9bd8bf3643ca6174cd236 Mon Sep 17 00:00:00 2001 From: olivier7delf <55892112+olivier7delf@users.noreply.github.com> Date: Thu, 4 Aug 2022 17:41:36 +0200 Subject: [PATCH 036/105] Update packages/smart-contracts/src/contracts/BatchConversionPayments.sol Co-authored-by: Yo <56731761+yomarion@users.noreply.github.com> --- .../src/contracts/BatchConversionPayments.sol | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/packages/smart-contracts/src/contracts/BatchConversionPayments.sol b/packages/smart-contracts/src/contracts/BatchConversionPayments.sol index b9708f913..a32504374 100644 --- a/packages/smart-contracts/src/contracts/BatchConversionPayments.sol +++ b/packages/smart-contracts/src/contracts/BatchConversionPayments.sol @@ -162,12 +162,9 @@ contract BatchConversionPayments is BatchPaymentsPublic { } /** - * @notice Transfers a batch of multiple ERC20 tokens with a reference with amount based on the request amount in fiat + * @notice Makes a batch of transfers for multiple ERC20 tokens, with amounts based on a request currency (e.g. fiat) and with a reference per payment. * @param requestsInfo list of requestInfo, each one containing all the information of a request * @param _feeAddress The fee recipient - * @dev amountAndFee is an approximation of the amount and the fee to be paid, in order to get enough tokens. - * The excess is sent back to the payer - * batchFeeAmount is an approximation for the same reason of amountAndFee */ function batchERC20ConversionPaymentsMultiTokens( RequestInfo[] calldata requestsInfo, From 014c430c3ae711e64e64001f9b96d45c3c64b930 Mon Sep 17 00:00:00 2001 From: olivier7delf <55892112+olivier7delf@users.noreply.github.com> Date: Thu, 4 Aug 2022 17:51:47 +0200 Subject: [PATCH 037/105] receive comment --- .../smart-contracts/src/contracts/BatchPaymentsPublic.sol | 6 ++++-- .../test/contracts/BatchConversionPayments.test.ts | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/smart-contracts/src/contracts/BatchPaymentsPublic.sol b/packages/smart-contracts/src/contracts/BatchPaymentsPublic.sol index be24081ed..5f68e0d49 100644 --- a/packages/smart-contracts/src/contracts/BatchPaymentsPublic.sol +++ b/packages/smart-contracts/src/contracts/BatchPaymentsPublic.sol @@ -52,8 +52,10 @@ contract BatchPaymentsPublic is Ownable { batchFee = 0; } - // batch Eth requires batch contract to receive funds from ethFeeProxy with a value = 0 - // and also from paymentEthConversionProxy with a value > 0 + /** + * This contract is non-payable. Making an ETH payment with conversion requires the contract to accept incoming ETH. + * See the end of `batchRouter` where the leftover is given back to the transaction sender. + */ receive() external payable { require(payerAuthorized || msg.value == 0, 'Non-payable'); } diff --git a/packages/smart-contracts/test/contracts/BatchConversionPayments.test.ts b/packages/smart-contracts/test/contracts/BatchConversionPayments.test.ts index 19b2aebcb..8143993f6 100644 --- a/packages/smart-contracts/test/contracts/BatchConversionPayments.test.ts +++ b/packages/smart-contracts/test/contracts/BatchConversionPayments.test.ts @@ -459,7 +459,7 @@ describe('contract: BatchErc20ConversionPayments', () => { // xSigner connect to the batch function setBatchConvFunction(xSigner); await expect(batchConvFunction(argTemplate([requestInfo]), feeAddress)).to.be.revertedWith( - 'Not sufficient allowance for batch to pay', + 'Insufficient allowance for batch to pay', ); // reset: signer connect to the batch function setBatchConvFunction(signer); From f63d653fdcebb882e7cd7fcbaa2d88fcfbd0901d Mon Sep 17 00:00:00 2001 From: olivier7delf <55892112+olivier7delf@users.noreply.github.com> Date: Fri, 5 Aug 2022 10:18:39 +0200 Subject: [PATCH 038/105] tmp tests fail --- ...test-deploy-batch-conversion-deployment.ts | 1 - .../src/contracts/BatchConversionPayments.sol | 17 +---- .../src/contracts/BatchPaymentsPublic.sol | 34 ++++----- .../contracts/BatchConversionPayments.test.ts | 69 +++++++++++++------ 4 files changed, 68 insertions(+), 53 deletions(-) diff --git a/packages/smart-contracts/scripts/test-deploy-batch-conversion-deployment.ts b/packages/smart-contracts/scripts/test-deploy-batch-conversion-deployment.ts index 28080f02e..cdded122f 100644 --- a/packages/smart-contracts/scripts/test-deploy-batch-conversion-deployment.ts +++ b/packages/smart-contracts/scripts/test-deploy-batch-conversion-deployment.ts @@ -39,7 +39,6 @@ export async function deployBatchConversionPayment( // Initialize batch conversion fee, useful to others packages. const [owner] = await hre.ethers.getSigners(); const batchConversion = batchConversionPaymentsArtifact.connect(hre.network.name, owner); - await batchConversion.connect(owner).setBasicFee(10); await batchConversion.connect(owner).setBatchFee(30); await batchConversion.connect(owner).setBatchConversionFee(30); diff --git a/packages/smart-contracts/src/contracts/BatchConversionPayments.sol b/packages/smart-contracts/src/contracts/BatchConversionPayments.sol index a32504374..f377cd189 100644 --- a/packages/smart-contracts/src/contracts/BatchConversionPayments.sol +++ b/packages/smart-contracts/src/contracts/BatchConversionPayments.sol @@ -28,7 +28,6 @@ contract BatchConversionPayments is BatchPaymentsPublic { ChainlinkConversionPath public chainlinkConversionPath; uint256 public batchConversionFee; - uint256 public basicFee; /** * @dev All the information of a request, except the feeAddress @@ -98,7 +97,6 @@ contract BatchConversionPayments is BatchPaymentsPublic { chainlinkConversionPath = ChainlinkConversionPath(_chainlinkConversionPathAddress); transferOwnership(_owner); - basicFee = 0; batchFee = 0; batchConversionFee = 0; } @@ -250,7 +248,7 @@ contract BatchConversionPayments is BatchPaymentsPublic { safeTransferFrom( uTokens[k].tokenAddress, _feeAddress, - ((((uTokens[k].amountAndFee - excessAmount) * 10000) / (10000 + basicFee)) * + ((uTokens[k].amountAndFee - excessAmount) * batchConversionFee) / 10000 ), 'batch fee transferFrom() failed' @@ -291,8 +289,7 @@ contract BatchConversionPayments is BatchPaymentsPublic { } // Check that batch contract has enough funds to pay batch conversion fees - uint256 amountBatchFees = ((((contractBalance - address(this).balance) * 10000) / - (10000 + basicFee)) * batchConversionFee) / 10000; + uint256 amountBatchFees = ((contractBalance - address(this).balance) * batchConversionFee) / 10000; require(address(this).balance >= amountBatchFees, 'not enough funds for batch conversion fees'); // Batch contract pays batch fee @@ -308,16 +305,6 @@ contract BatchConversionPayments is BatchPaymentsPublic { * Admin functions to edit the conversion proxies address and fees */ - /** - * @notice Fees applied for basic invoice, 0.1% at Request Finance - * @param _basicFee Between 0 and 10000, e.i: basicFee = 10 represent 0.10% of fees - * Update it cautiously. - * e.i: Only if the Request Finance 'basicFee' has evolve, which should be exceptional - */ - function setBasicFee(uint256 _basicFee) external onlyOwner { - basicFee = _basicFee; - } - /** * @notice fees added when using Erc20/Eth conversion batch functions * @param _batchConversionFee between 0 and 10000, i.e: batchFee = 50 represent 0.50% of fees diff --git a/packages/smart-contracts/src/contracts/BatchPaymentsPublic.sol b/packages/smart-contracts/src/contracts/BatchPaymentsPublic.sol index 5f68e0d49..43485492a 100644 --- a/packages/smart-contracts/src/contracts/BatchPaymentsPublic.sol +++ b/packages/smart-contracts/src/contracts/BatchPaymentsPublic.sol @@ -222,59 +222,59 @@ contract BatchPaymentsPublic is Ownable { // Create a list of unique tokens used and the amounts associated // Only considere tokens having: amounts + feeAmounts > 0 // batchFeeAmount is the amount's sum, and then, batch fee rate is applied - Token[] memory uniqueTokens = new Token[](_tokenAddresses.length); + Token[] memory uTokens = new Token[](_tokenAddresses.length); for (uint256 i = 0; i < _tokenAddresses.length; i++) { for (uint256 j = 0; j < _tokenAddresses.length; j++) { - // If the token is already in the existing uniqueTokens list - if (uniqueTokens[j].tokenAddress == _tokenAddresses[i]) { - uniqueTokens[j].amountAndFee += _amounts[i] + _feeAmounts[i]; - uniqueTokens[j].batchFeeAmount += _amounts[i]; + // If the token is already in the existing uTokens list + if (uTokens[j].tokenAddress == _tokenAddresses[i]) { + uTokens[j].amountAndFee += _amounts[i] + _feeAmounts[i]; + uTokens[j].batchFeeAmount += _amounts[i]; break; } // If the token is not in the list (amountAndFee = 0), and amount + fee > 0 - if (uniqueTokens[j].amountAndFee == 0 && (_amounts[i] + _feeAmounts[i]) > 0) { - uniqueTokens[j].tokenAddress = _tokenAddresses[i]; - uniqueTokens[j].amountAndFee = _amounts[i] + _feeAmounts[i]; - uniqueTokens[j].batchFeeAmount = _amounts[i]; + if (uTokens[j].amountAndFee == 0 && (_amounts[i] + _feeAmounts[i]) > 0) { + uTokens[j].tokenAddress = _tokenAddresses[i]; + uTokens[j].amountAndFee = _amounts[i] + _feeAmounts[i]; + uTokens[j].batchFeeAmount = _amounts[i]; break; } } } // The payer transfers tokens to the batch contract and pays batch fee - for (uint256 i = 0; i < uniqueTokens.length && uniqueTokens[i].amountAndFee > 0; i++) { - uniqueTokens[i].batchFeeAmount = (uniqueTokens[i].batchFeeAmount * batchFee) / 10000; - IERC20 requestedToken = IERC20(uniqueTokens[i].tokenAddress); + for (uint256 i = 0; i < uTokens.length && uTokens[i].amountAndFee > 0; i++) { + uTokens[i].batchFeeAmount = (uTokens[i].batchFeeAmount * batchFee) / 10000; + IERC20 requestedToken = IERC20(uTokens[i].tokenAddress); require( requestedToken.allowance(msg.sender, address(this)) >= - uniqueTokens[i].amountAndFee + uniqueTokens[i].batchFeeAmount, + uTokens[i].amountAndFee + uTokens[i].batchFeeAmount, 'Not sufficient allowance for batch to pay' ); // check if the payer can pay the amount, the fee, and the batchFee require( requestedToken.balanceOf(msg.sender) >= - uniqueTokens[i].amountAndFee + uniqueTokens[i].batchFeeAmount, + uTokens[i].amountAndFee + uTokens[i].batchFeeAmount, 'not enough funds' ); // Transfer only the amount and fee required for the token on the batch contract require( - safeTransferFrom(uniqueTokens[i].tokenAddress, address(this), uniqueTokens[i].amountAndFee), + safeTransferFrom(uTokens[i].tokenAddress, address(this), uTokens[i].amountAndFee), 'payment transferFrom() failed' ); // Batch contract approves Erc20FeeProxy to spend the token if ( requestedToken.allowance(address(this), address(paymentErc20Proxy)) < - uniqueTokens[i].amountAndFee + uTokens[i].amountAndFee ) { approvePaymentProxyToSpend(address(requestedToken), address(paymentErc20Proxy)); } // Payer pays batch fee amount require( - safeTransferFrom(uniqueTokens[i].tokenAddress, _feeAddress, uniqueTokens[i].batchFeeAmount), + safeTransferFrom(uTokens[i].tokenAddress, _feeAddress, uTokens[i].batchFeeAmount), 'batch fee transferFrom() failed' ); } diff --git a/packages/smart-contracts/test/contracts/BatchConversionPayments.test.ts b/packages/smart-contracts/test/contracts/BatchConversionPayments.test.ts index 8143993f6..368713c56 100644 --- a/packages/smart-contracts/test/contracts/BatchConversionPayments.test.ts +++ b/packages/smart-contracts/test/contracts/BatchConversionPayments.test.ts @@ -35,7 +35,6 @@ describe('contract: BatchErc20ConversionPayments', () => { let batchAddress: string; let signer: Signer; let xSigner: Signer; - const basicFee = 10; const batchFee = 100; const batchConvFee = 100; const amountInFiat = '100000000'; // 1 with 8 decimal @@ -139,7 +138,6 @@ describe('contract: BatchErc20ConversionPayments', () => { await signer.getAddress(), ); - await testBatchConversionProxy.setBasicFee(basicFee); await testBatchConversionProxy.setBatchFee(batchFee); await testBatchConversionProxy.setBatchConversionFee(batchConvFee); @@ -232,6 +230,7 @@ describe('contract: BatchErc20ConversionPayments', () => { _conversionToPay_result: BigNumber, _conversionFees_result: BigNumber, _conversionsToPay_results: BigNumber[], + _conversionFees_results: BigNumber[], ) => { fromDiffBalanceExpected = fromDiffBalanceExpected .add(_conversionToPay_result) @@ -240,19 +239,22 @@ describe('contract: BatchErc20ConversionPayments', () => { toDiffBalanceExpected = toDiffBalanceExpected.add(_conversionToPay_result); feeDiffBalanceExpected = feeDiffBalanceExpected.add(_conversionFees_result); if (_conversionsToPay_results.length > 0) - calculERC20BatchFeeBalances(_conversionsToPay_results); + calculERC20BatchFeeBalances(_conversionsToPay_results, _conversionFees_results); }; /** * @notice Used to calcul the expected new ERC20 fee batch balance for batch conversion. * @param _conversionsToPay_results is used to calcul batch fees, it case of payments multiple - * @dev in case of payments multiple, we sum the amount paid, and then, we calcul the fees amount + * @dev in case of payments multiple, we sum the amount paid including fees, and then, we calcul the batch fees amount * because the sum(batchFeeToPay(amountPay[i])) != batchFeeToPay(sum(amountPay[i])) */ - const calculERC20BatchFeeBalances = (_conversionsToPay_results: BigNumber[]) => { + const calculERC20BatchFeeBalances = ( + _conversionsToPay_results: BigNumber[], + _conversionFees_result: BigNumber[], + ) => { let sumToPay = BigNumber.from(0); for (let i = 0; i < _conversionsToPay_results.length; i++) { - sumToPay = sumToPay.add(_conversionsToPay_results[i]); + sumToPay = sumToPay.add(_conversionsToPay_results[i]).add(_conversionFees_result[i]); } fromDiffBalanceExpected = fromDiffBalanceExpected.add(batchFeeToPay(sumToPay)); feeDiffBalanceExpected = feeDiffBalanceExpected.add(batchFeeToPay(sumToPay)); @@ -275,7 +277,12 @@ describe('contract: BatchErc20ConversionPayments', () => { await emitOneTx(expect(result), requestInfo, conversionToPay, conversionFees); } - calculERC20Balances(conversionToPay.result, conversionFees.result, [conversionToPay.result]); + calculERC20Balances( + conversionToPay.result, + conversionFees.result, + [conversionToPay.result], + [conversionFees.result], + ); }; /** @@ -300,9 +307,11 @@ describe('contract: BatchErc20ConversionPayments', () => { let requestInfos: RequestInfo[] = []; let conversionsToPay: ConvToPay[] = []; + let conversionsFees: ConvToPay[] = []; for (let i = 0; i < nTimes; i++) { requestInfos = requestInfos.concat([requestInfo, requestInfo2]); conversionsToPay = conversionsToPay.concat([conversionToPay, conversionToPay2]); + conversionsFees = conversionsFees.concat([conversionFees, conversionFees2]); } const result = batchConvFunction(argTemplate(requestInfos), feeAddress); const tx = await result; @@ -317,18 +326,19 @@ describe('contract: BatchErc20ConversionPayments', () => { requestInfo2.path[requestInfo2.path.length - 1] ) { for (let i = 0; i < nTimes - 1; i++) { - calculERC20Balances(conversionToPay.result, conversionFees.result, []); - calculERC20Balances(conversionToPay2.result, conversionFees2.result, []); + calculERC20Balances(conversionToPay.result, conversionFees.result, [], []); + calculERC20Balances(conversionToPay2.result, conversionFees2.result, [], []); } - calculERC20Balances(conversionToPay.result, conversionFees.result, []); + calculERC20Balances(conversionToPay.result, conversionFees.result, [], []); calculERC20Balances( conversionToPay2.result, conversionFees2.result, conversionsToPay.map((ctp) => ctp.result), + conversionsFees.map((ctp) => ctp.result), ); } else { for (let i = 0; i < nTimes - 1; i++) { - calculERC20Balances(conversionToPay.result, conversionFees.result, []); + calculERC20Balances(conversionToPay.result, conversionFees.result, [], []); } const conversionsToPayBis = conversionsToPay.filter((_, i) => i % 2 === 0); @@ -336,6 +346,7 @@ describe('contract: BatchErc20ConversionPayments', () => { conversionToPay.result, conversionFees.result, conversionsToPayBis.map((ctp) => ctp.result), + conversionsFees.map((ctp) => ctp.result), ); } }; @@ -547,9 +558,12 @@ describe('contract: BatchErc20ConversionPayments', () => { amount * (batchFee / 10_000), // batch fee amount = 200 * 1% ); - calculERC20Balances(BigNumber.from(amount), BigNumber.from(feeAmount), [ + calculERC20Balances( BigNumber.from(amount), - ]); + BigNumber.from(feeAmount), + [BigNumber.from(amount)], + [BigNumber.from(feeAmount)], + ); }; it('batchERC20PaymentsWithReference transfers token', async function () { await batchERC20Payments(false, 'batchERC20PaymentsWithReference'); @@ -580,7 +594,7 @@ describe('contract: BatchErc20ConversionPayments', () => { let amountToPayExpected: BigNumber; let feeToPayExpected: BigNumber; const amount = BigNumber.from(100000); // usually in USD - const feeAmount = amount.mul(basicFee).div(10000); // usually in USD + const feeAmount = amount.mul(10).div(10000); // usually in USD let inputs: Array; const pathUsdEth = [USD_hash, ETH_hash]; @@ -650,13 +664,13 @@ describe('contract: BatchErc20ConversionPayments', () => { value: BigNumber.from('100000000000000000'), }); const receipt = await tx.wait(); - + console.log('lala'); if (logGas) console.log('gas consumption: ', receipt.gasUsed.toString()); const afterEthBalance = await provider.getBalance(await signer.getAddress()); const afterEthBalanceTo = await provider.getBalance(to); const afterEthBalanceFee = await provider.getBalance(feeAddress); - + console.log('lala'); const proxyBalance = await provider.getBalance(testBatchConversionProxy.address); const _diffBalance = beforeEthBalance.sub(afterEthBalance); const _diffBalanceTo = afterEthBalanceTo.sub(beforeEthBalanceTo); @@ -664,13 +678,28 @@ describe('contract: BatchErc20ConversionPayments', () => { const _diffBalanceExpect = receipt.gasUsed .mul(2 * 10 ** 10) .add(_diffBalanceTo) - .add(_diffBalanceFee); - + // .add(_diffBalanceTo.mul(10).div(10000)) + .add(_diffBalanceFee); //.mul(2)); + // 2000000000000 _diffBalanceTo + // 22020000000 _diffBalanceFee + // -22020000000 real + // +20020000000 expected + // 2000000000 missing to the expected ? + //TODO FIX test modif: now we calcul batch fee on the sum amount + fees? 2000000000 is missing in the expected expect(_diffBalance).to.equals(_diffBalanceExpect.toString(), 'DiffBalance'); expect(_diffBalanceTo).to.equals(amountToPayExpected.toString(), 'diffBalanceTo'); - + console.log('lala'); + console.log('_diffBalanceTo', _diffBalanceTo.toString()); + console.log('_diffBalanceFee', _diffBalanceFee.toString()); + console.log('amountToPayExpected', amountToPayExpected.toString()); + console.log('feeToPayExpected', feeToPayExpected.toString()); expect(_diffBalanceFee.toString()).to.equals( - amountToPayExpected.mul(batchConvFee).div(10000).add(feeToPayExpected).toString(), + amountToPayExpected + .mul(batchConvFee) + .add(feeToPayExpected) + .mul(batchConvFee) + .div(10000) + .toString(), 'diffBalanceFee', ); expect(proxyBalance).to.equals('0', 'proxyBalance'); From 0c8f375e2a1e3359392dfddf84f40c47be598f17 Mon Sep 17 00:00:00 2001 From: olivier7delf <55892112+olivier7delf@users.noreply.github.com> Date: Fri, 5 Aug 2022 15:19:32 +0200 Subject: [PATCH 039/105] batch calcul updated and tested - it includes fees now --- .../src/contracts/BatchConversionPayments.sol | 6 +- .../contracts/BatchConversionPayments.test.ts | 56 ++++++++++++------- 2 files changed, 40 insertions(+), 22 deletions(-) diff --git a/packages/smart-contracts/src/contracts/BatchConversionPayments.sol b/packages/smart-contracts/src/contracts/BatchConversionPayments.sol index a32504374..5b5ccd4ce 100644 --- a/packages/smart-contracts/src/contracts/BatchConversionPayments.sol +++ b/packages/smart-contracts/src/contracts/BatchConversionPayments.sol @@ -250,7 +250,7 @@ contract BatchConversionPayments is BatchPaymentsPublic { safeTransferFrom( uTokens[k].tokenAddress, _feeAddress, - ((((uTokens[k].amountAndFee - excessAmount) * 10000) / (10000 + basicFee)) * + ((uTokens[k].amountAndFee - excessAmount) * batchConversionFee) / 10000 ), 'batch fee transferFrom() failed' @@ -291,8 +291,8 @@ contract BatchConversionPayments is BatchPaymentsPublic { } // Check that batch contract has enough funds to pay batch conversion fees - uint256 amountBatchFees = ((((contractBalance - address(this).balance) * 10000) / - (10000 + basicFee)) * batchConversionFee) / 10000; + uint256 amountBatchFees = (((contractBalance - address(this).balance) + ) * batchConversionFee) / 10000; require(address(this).balance >= amountBatchFees, 'not enough funds for batch conversion fees'); // Batch contract pays batch fee diff --git a/packages/smart-contracts/test/contracts/BatchConversionPayments.test.ts b/packages/smart-contracts/test/contracts/BatchConversionPayments.test.ts index 8143993f6..794e1631b 100644 --- a/packages/smart-contracts/test/contracts/BatchConversionPayments.test.ts +++ b/packages/smart-contracts/test/contracts/BatchConversionPayments.test.ts @@ -35,7 +35,6 @@ describe('contract: BatchErc20ConversionPayments', () => { let batchAddress: string; let signer: Signer; let xSigner: Signer; - const basicFee = 10; const batchFee = 100; const batchConvFee = 100; const amountInFiat = '100000000'; // 1 with 8 decimal @@ -139,7 +138,6 @@ describe('contract: BatchErc20ConversionPayments', () => { await signer.getAddress(), ); - await testBatchConversionProxy.setBasicFee(basicFee); await testBatchConversionProxy.setBatchFee(batchFee); await testBatchConversionProxy.setBatchConversionFee(batchConvFee); @@ -232,6 +230,7 @@ describe('contract: BatchErc20ConversionPayments', () => { _conversionToPay_result: BigNumber, _conversionFees_result: BigNumber, _conversionsToPay_results: BigNumber[], + _conversionFees_results: BigNumber[], ) => { fromDiffBalanceExpected = fromDiffBalanceExpected .add(_conversionToPay_result) @@ -240,19 +239,22 @@ describe('contract: BatchErc20ConversionPayments', () => { toDiffBalanceExpected = toDiffBalanceExpected.add(_conversionToPay_result); feeDiffBalanceExpected = feeDiffBalanceExpected.add(_conversionFees_result); if (_conversionsToPay_results.length > 0) - calculERC20BatchFeeBalances(_conversionsToPay_results); + calculERC20BatchFeeBalances(_conversionsToPay_results, _conversionFees_results); }; /** * @notice Used to calcul the expected new ERC20 fee batch balance for batch conversion. * @param _conversionsToPay_results is used to calcul batch fees, it case of payments multiple - * @dev in case of payments multiple, we sum the amount paid, and then, we calcul the fees amount + * @dev in case of payments multiple, we sum the amount paid including fees, and then, we calcul the batch fees amount * because the sum(batchFeeToPay(amountPay[i])) != batchFeeToPay(sum(amountPay[i])) */ - const calculERC20BatchFeeBalances = (_conversionsToPay_results: BigNumber[]) => { + const calculERC20BatchFeeBalances = ( + _conversionsToPay_results: BigNumber[], + _conversionFees_result: BigNumber[], + ) => { let sumToPay = BigNumber.from(0); for (let i = 0; i < _conversionsToPay_results.length; i++) { - sumToPay = sumToPay.add(_conversionsToPay_results[i]); + sumToPay = sumToPay.add(_conversionsToPay_results[i]).add(_conversionFees_result[i]); } fromDiffBalanceExpected = fromDiffBalanceExpected.add(batchFeeToPay(sumToPay)); feeDiffBalanceExpected = feeDiffBalanceExpected.add(batchFeeToPay(sumToPay)); @@ -275,7 +277,12 @@ describe('contract: BatchErc20ConversionPayments', () => { await emitOneTx(expect(result), requestInfo, conversionToPay, conversionFees); } - calculERC20Balances(conversionToPay.result, conversionFees.result, [conversionToPay.result]); + calculERC20Balances( + conversionToPay.result, + conversionFees.result, + [conversionToPay.result], + [conversionFees.result], + ); }; /** @@ -300,9 +307,11 @@ describe('contract: BatchErc20ConversionPayments', () => { let requestInfos: RequestInfo[] = []; let conversionsToPay: ConvToPay[] = []; + let conversionsFees: ConvToPay[] = []; for (let i = 0; i < nTimes; i++) { requestInfos = requestInfos.concat([requestInfo, requestInfo2]); conversionsToPay = conversionsToPay.concat([conversionToPay, conversionToPay2]); + conversionsFees = conversionsFees.concat([conversionFees, conversionFees2]); } const result = batchConvFunction(argTemplate(requestInfos), feeAddress); const tx = await result; @@ -317,18 +326,19 @@ describe('contract: BatchErc20ConversionPayments', () => { requestInfo2.path[requestInfo2.path.length - 1] ) { for (let i = 0; i < nTimes - 1; i++) { - calculERC20Balances(conversionToPay.result, conversionFees.result, []); - calculERC20Balances(conversionToPay2.result, conversionFees2.result, []); + calculERC20Balances(conversionToPay.result, conversionFees.result, [], []); + calculERC20Balances(conversionToPay2.result, conversionFees2.result, [], []); } - calculERC20Balances(conversionToPay.result, conversionFees.result, []); + calculERC20Balances(conversionToPay.result, conversionFees.result, [], []); calculERC20Balances( conversionToPay2.result, conversionFees2.result, conversionsToPay.map((ctp) => ctp.result), + conversionsFees.map((ctp) => ctp.result), ); } else { for (let i = 0; i < nTimes - 1; i++) { - calculERC20Balances(conversionToPay.result, conversionFees.result, []); + calculERC20Balances(conversionToPay.result, conversionFees.result, [], []); } const conversionsToPayBis = conversionsToPay.filter((_, i) => i % 2 === 0); @@ -336,6 +346,7 @@ describe('contract: BatchErc20ConversionPayments', () => { conversionToPay.result, conversionFees.result, conversionsToPayBis.map((ctp) => ctp.result), + conversionsFees.map((ctp) => ctp.result), ); } }; @@ -547,9 +558,12 @@ describe('contract: BatchErc20ConversionPayments', () => { amount * (batchFee / 10_000), // batch fee amount = 200 * 1% ); - calculERC20Balances(BigNumber.from(amount), BigNumber.from(feeAmount), [ + calculERC20Balances( BigNumber.from(amount), - ]); + BigNumber.from(feeAmount), + [BigNumber.from(amount)], + [BigNumber.from(feeAmount)], + ); }; it('batchERC20PaymentsWithReference transfers token', async function () { await batchERC20Payments(false, 'batchERC20PaymentsWithReference'); @@ -580,7 +594,7 @@ describe('contract: BatchErc20ConversionPayments', () => { let amountToPayExpected: BigNumber; let feeToPayExpected: BigNumber; const amount = BigNumber.from(100000); // usually in USD - const feeAmount = amount.mul(basicFee).div(10000); // usually in USD + const feeAmount = amount.mul(10).div(10000); // usually in USD let inputs: Array; const pathUsdEth = [USD_hash, ETH_hash]; @@ -642,6 +656,7 @@ describe('contract: BatchErc20ConversionPayments', () => { feesToPay = await chainlinkPath.getConversion(requestInfo.feeAmount, requestInfo.path); amountToPayExpected = conversionToPay.result; + // fees does not include batch conv fees yet feeToPayExpected = feesToPay.result; }); @@ -650,13 +665,11 @@ describe('contract: BatchErc20ConversionPayments', () => { value: BigNumber.from('100000000000000000'), }); const receipt = await tx.wait(); - if (logGas) console.log('gas consumption: ', receipt.gasUsed.toString()); const afterEthBalance = await provider.getBalance(await signer.getAddress()); const afterEthBalanceTo = await provider.getBalance(to); const afterEthBalanceFee = await provider.getBalance(feeAddress); - const proxyBalance = await provider.getBalance(testBatchConversionProxy.address); const _diffBalance = beforeEthBalance.sub(afterEthBalance); const _diffBalanceTo = afterEthBalanceTo.sub(beforeEthBalanceTo); @@ -665,12 +678,17 @@ describe('contract: BatchErc20ConversionPayments', () => { .mul(2 * 10 ** 10) .add(_diffBalanceTo) .add(_diffBalanceFee); - expect(_diffBalance).to.equals(_diffBalanceExpect.toString(), 'DiffBalance'); expect(_diffBalanceTo).to.equals(amountToPayExpected.toString(), 'diffBalanceTo'); + // feeToPayExpected includes batch conversion fees now + feeToPayExpected = amountToPayExpected + .add(feeToPayExpected) + .mul(batchConvFee) + .div(10000) + .add(feeToPayExpected); expect(_diffBalanceFee.toString()).to.equals( - amountToPayExpected.mul(batchConvFee).div(10000).add(feeToPayExpected).toString(), + feeToPayExpected.toString(), 'diffBalanceFee', ); expect(proxyBalance).to.equals('0', 'proxyBalance'); @@ -714,7 +732,7 @@ describe('contract: BatchErc20ConversionPayments', () => { }); }; - ERC20TestSuite('batchRouter'); + // ERC20TestSuite('batchRouter'); ERC20TestSuite('batchERC20ConversionPaymentsMultiTokens'); EthTestSuite('batchRouter'); EthTestSuite('batchEthConversionPaymentsWithReference'); From 7cde97db86f0ac0c4baba4ed2f9d1057b0a919ee Mon Sep 17 00:00:00 2001 From: olivier7delf <55892112+olivier7delf@users.noreply.github.com> Date: Fri, 5 Aug 2022 16:16:51 +0200 Subject: [PATCH 040/105] delete basicFee inside the contract - update test - refacto script to deploy and local artifact --- ...test-deploy-batch-conversion-deployment.ts | 35 ++++++++++++++----- .../src/contracts/BatchConversionPayments.sol | 12 ------- .../BatchConversionPayments/0.1.0.json | 26 -------------- .../contracts/BatchConversionPayments.test.ts | 6 ++-- .../test/contracts/localArtifacts.ts | 15 ++++++++ 5 files changed, 45 insertions(+), 49 deletions(-) diff --git a/packages/smart-contracts/scripts/test-deploy-batch-conversion-deployment.ts b/packages/smart-contracts/scripts/test-deploy-batch-conversion-deployment.ts index 28080f02e..aa67ebd7d 100644 --- a/packages/smart-contracts/scripts/test-deploy-batch-conversion-deployment.ts +++ b/packages/smart-contracts/scripts/test-deploy-batch-conversion-deployment.ts @@ -2,7 +2,14 @@ import '@nomiclabs/hardhat-ethers'; import { HardhatRuntimeEnvironment } from 'hardhat/types'; import { deployOne } from './deploy-one'; -import { batchConversionPaymentsArtifact } from '../src/lib'; +import { + batchConversionPaymentsArtifact, + chainlinkConversionPath, + erc20ConversionProxy, + erc20FeeProxyArtifact, + ethConversionArtifact, + ethereumFeeProxyArtifact, +} from '../src/lib'; import { chainlinkConversionPath as chainlinkConvArtifact } from '../src/lib'; import { CurrencyManager } from '@requestnetwork/currency'; @@ -13,11 +20,11 @@ export async function deployBatchConversionPayment( ): Promise { try { console.log('start BatchConversionPayments'); - const _ERC20FeeProxyAddress = '0x75c35C980C0d37ef46DF04d31A140b65503c0eEd'; - const _EthereumFeeProxyAddress = '0x3d49d1eF2adE060a33c6E6Aa213513A7EE9a6241'; - const _chainlinkConversionPath = '0x4e71920b7330515faf5EA0c690f1aD06a85fB60c'; - const _paymentErc20ConversionFeeProxy = '0xdE5491f774F0Cb009ABcEA7326342E105dbb1B2E'; - const _paymentEthConversionFeeProxy = '0x98d9f9e8DEbd4A632682ba207670d2a5ACD3c489'; + const _ERC20FeeProxyAddress = erc20FeeProxyArtifact.getAddress('private'); + const _EthereumFeeProxyAddress = ethereumFeeProxyArtifact.getAddress('private'); + const _chainlinkConversionPath = chainlinkConversionPath.getAddress('private'); + const _paymentErc20ConversionFeeProxy = erc20ConversionProxy.getAddress('private'); + const _paymentEthConversionFeeProxy = ethConversionArtifact.getAddress('private'); // Deploy BatchConversionPayments contract const { address: BatchConversionPaymentsAddress } = await deployOne( @@ -39,7 +46,6 @@ export async function deployBatchConversionPayment( // Initialize batch conversion fee, useful to others packages. const [owner] = await hre.ethers.getSigners(); const batchConversion = batchConversionPaymentsArtifact.connect(hre.network.name, owner); - await batchConversion.connect(owner).setBasicFee(10); await batchConversion.connect(owner).setBatchFee(30); await batchConversion.connect(owner).setBatchConversionFee(30); @@ -61,9 +67,20 @@ export async function deployBatchConversionPayment( // ---------------------------------- console.log('Contracts deployed'); console.log(` - testERC20FakeFAU.address: ${testERC20FakeFAU.address} - BatchConversionPayments: ${BatchConversionPaymentsAddress} + testERC20FakeFAU.address: ${testERC20FakeFAU.address} + BatchConversionPayments: ${BatchConversionPaymentsAddress} `); + + // Check the addresses of our contracts, to avoid misleading bugs in the tests + // ref to secondLocalERC20AlphaArtifact.getAddress('private'), that cannot be used in deployment + const fakeFAU_addressExpected = '0x51FC52Fd0B30fA0319D97893dEFE0201fEd39C4c'; + if (testERC20FakeFAU.address !== fakeFAU_addressExpected) { + throw '! -> testERC20FakeFAU.address !== fakeFAU_addressExpected, please update your code or the artifact'; + } + const batchConversionExpected = batchConversionPaymentsArtifact.getAddress('private'); + if (BatchConversionPaymentsAddress !== batchConversionExpected) { + throw '! -> BatchConversionPaymentsAddress !== batchConversionExpected, please update your code or the artifact'; + } } catch (e) { console.error(e); } diff --git a/packages/smart-contracts/src/contracts/BatchConversionPayments.sol b/packages/smart-contracts/src/contracts/BatchConversionPayments.sol index 5b5ccd4ce..3c01f5d79 100644 --- a/packages/smart-contracts/src/contracts/BatchConversionPayments.sol +++ b/packages/smart-contracts/src/contracts/BatchConversionPayments.sol @@ -28,7 +28,6 @@ contract BatchConversionPayments is BatchPaymentsPublic { ChainlinkConversionPath public chainlinkConversionPath; uint256 public batchConversionFee; - uint256 public basicFee; /** * @dev All the information of a request, except the feeAddress @@ -98,7 +97,6 @@ contract BatchConversionPayments is BatchPaymentsPublic { chainlinkConversionPath = ChainlinkConversionPath(_chainlinkConversionPathAddress); transferOwnership(_owner); - basicFee = 0; batchFee = 0; batchConversionFee = 0; } @@ -308,16 +306,6 @@ contract BatchConversionPayments is BatchPaymentsPublic { * Admin functions to edit the conversion proxies address and fees */ - /** - * @notice Fees applied for basic invoice, 0.1% at Request Finance - * @param _basicFee Between 0 and 10000, e.i: basicFee = 10 represent 0.10% of fees - * Update it cautiously. - * e.i: Only if the Request Finance 'basicFee' has evolve, which should be exceptional - */ - function setBasicFee(uint256 _basicFee) external onlyOwner { - basicFee = _basicFee; - } - /** * @notice fees added when using Erc20/Eth conversion batch functions * @param _batchConversionFee between 0 and 10000, i.e: batchFee = 50 represent 0.50% of fees diff --git a/packages/smart-contracts/src/lib/artifacts/BatchConversionPayments/0.1.0.json b/packages/smart-contracts/src/lib/artifacts/BatchConversionPayments/0.1.0.json index d0347a903..d9c135df0 100644 --- a/packages/smart-contracts/src/lib/artifacts/BatchConversionPayments/0.1.0.json +++ b/packages/smart-contracts/src/lib/artifacts/BatchConversionPayments/0.1.0.json @@ -55,19 +55,6 @@ "name": "OwnershipTransferred", "type": "event" }, - { - "inputs": [], - "name": "basicFee", - "outputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "stateMutability": "view", - "type": "function" - }, { "inputs": [], "name": "batchConversionFee", @@ -471,19 +458,6 @@ "stateMutability": "nonpayable", "type": "function" }, - { - "inputs": [ - { - "internalType": "uint256", - "name": "_basicFee", - "type": "uint256" - } - ], - "name": "setBasicFee", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, { "inputs": [ { diff --git a/packages/smart-contracts/test/contracts/BatchConversionPayments.test.ts b/packages/smart-contracts/test/contracts/BatchConversionPayments.test.ts index 794e1631b..c9fab91da 100644 --- a/packages/smart-contracts/test/contracts/BatchConversionPayments.test.ts +++ b/packages/smart-contracts/test/contracts/BatchConversionPayments.test.ts @@ -18,7 +18,7 @@ import { BigNumber, BigNumberish, BytesLike, ContractTransaction, Signer } from import { expect } from 'chai'; import { CurrencyManager } from '@requestnetwork/currency'; import { chainlinkConversionPath } from '../../src/lib'; -import { localERC20AlphaArtifact } from './localArtifacts'; +import { localERC20AlphaArtifact, secondLocalERC20AlphaArtifact } from './localArtifacts'; import Utils from '@requestnetwork/utils'; import { HttpNetworkConfig } from 'hardhat/types'; @@ -144,7 +144,9 @@ describe('contract: BatchErc20ConversionPayments', () => { DAI_address = localERC20AlphaArtifact.getAddress(network.name); testERC20 = new TestERC20__factory(signer).attach(DAI_address); - fakeFAU_address = '0x7153CCD1a20Bbb2f6dc89c1024de368326EC6b4F'; + // caution, change add one transaction in deployment will modify this address ! + // fakeFAU_address = '0x51FC52Fd0B30fA0319D97893dEFE0201fEd39C4c'; + fakeFAU_address = secondLocalERC20AlphaArtifact.getAddress(network.name); testERC20b = new TestERC20__factory(signer).attach(fakeFAU_address); batchAddress = testBatchConversionProxy.address; }); diff --git a/packages/smart-contracts/test/contracts/localArtifacts.ts b/packages/smart-contracts/test/contracts/localArtifacts.ts index 45114d99a..9a8b6b2ba 100644 --- a/packages/smart-contracts/test/contracts/localArtifacts.ts +++ b/packages/smart-contracts/test/contracts/localArtifacts.ts @@ -16,6 +16,21 @@ export const localERC20AlphaArtifact = new ContractArtifact( '0.0.1', ); +export const secondLocalERC20AlphaArtifact = new ContractArtifact( + { + '0.0.1': { + abi: [], + deployment: { + private: { + address: '0x51FC52Fd0B30fA0319D97893dEFE0201fEd39C4c', + creationBlockNumber: 0, + }, + }, + }, + }, + '0.0.1', +); + export const localUSDTArtifact = new ContractArtifact( { '0.0.1': { From 69b10dc80af96aae1eaac17f542a3e8735d16e38 Mon Sep 17 00:00:00 2001 From: olivier7delf <55892112+olivier7delf@users.noreply.github.com> Date: Mon, 8 Aug 2022 15:16:08 +0200 Subject: [PATCH 041/105] check the addresses of the contracts deployed and raise an error if needed --- packages/smart-contracts/hardhat.config.ts | 7 ++++++- ...test-deploy-batch-conversion-deployment.ts | 15 ++++++------- .../test-deploy-batch-erc-eth-deployment.ts | 7 +++++++ packages/smart-contracts/scripts/utils.ts | 21 +++++++++++++++++++ .../src/contracts/BatchConversionPayments.sol | 7 +++---- .../contracts/BatchConversionPayments.test.ts | 21 +++++++++---------- 6 files changed, 55 insertions(+), 23 deletions(-) diff --git a/packages/smart-contracts/hardhat.config.ts b/packages/smart-contracts/hardhat.config.ts index f12fdc0d4..fc0dcc591 100644 --- a/packages/smart-contracts/hardhat.config.ts +++ b/packages/smart-contracts/hardhat.config.ts @@ -14,6 +14,7 @@ import { computeCreate2DeploymentAddressesFromList } from './scripts-create2/com import { VerifyCreate2FromList } from './scripts-create2/verify'; import { deployWithCreate2FromList } from './scripts-create2/deploy'; import utils from '@requestnetwork/utils'; +import { NUMBER_ERRORS } from './scripts/utils'; config(); @@ -173,7 +174,11 @@ export default { task('deploy-local-env', 'Deploy a local environment').setAction(async (args, hre) => { args.force = true; await deployAllContracts(args, hre); - console.log('All contracts (re)deployed locally'); + if (NUMBER_ERRORS > 0) { + console.log(`Deployment failed, please check the ${NUMBER_ERRORS} errors`); + } else { + console.log('All contracts (re)deployed locally'); + } }); task( diff --git a/packages/smart-contracts/scripts/test-deploy-batch-conversion-deployment.ts b/packages/smart-contracts/scripts/test-deploy-batch-conversion-deployment.ts index aa67ebd7d..fd36fcb29 100644 --- a/packages/smart-contracts/scripts/test-deploy-batch-conversion-deployment.ts +++ b/packages/smart-contracts/scripts/test-deploy-batch-conversion-deployment.ts @@ -12,6 +12,7 @@ import { } from '../src/lib'; import { chainlinkConversionPath as chainlinkConvArtifact } from '../src/lib'; import { CurrencyManager } from '@requestnetwork/currency'; +import { deployAddressChecking } from './utils'; // Deploys, set up the contracts export async function deployBatchConversionPayment( @@ -72,15 +73,15 @@ export async function deployBatchConversionPayment( `); // Check the addresses of our contracts, to avoid misleading bugs in the tests + // ref to secondLocalERC20AlphaArtifact.getAddress('private'), that cannot be used in deployment const fakeFAU_addressExpected = '0x51FC52Fd0B30fA0319D97893dEFE0201fEd39C4c'; - if (testERC20FakeFAU.address !== fakeFAU_addressExpected) { - throw '! -> testERC20FakeFAU.address !== fakeFAU_addressExpected, please update your code or the artifact'; - } - const batchConversionExpected = batchConversionPaymentsArtifact.getAddress('private'); - if (BatchConversionPaymentsAddress !== batchConversionExpected) { - throw '! -> BatchConversionPaymentsAddress !== batchConversionExpected, please update your code or the artifact'; - } + deployAddressChecking('testERC20FakeFAU', testERC20FakeFAU.address, fakeFAU_addressExpected); + deployAddressChecking( + 'batchConversionPayments', + BatchConversionPaymentsAddress, + batchConversionPaymentsArtifact.getAddress('private'), + ); } catch (e) { console.error(e); } diff --git a/packages/smart-contracts/scripts/test-deploy-batch-erc-eth-deployment.ts b/packages/smart-contracts/scripts/test-deploy-batch-erc-eth-deployment.ts index d483cd177..96fa279cc 100644 --- a/packages/smart-contracts/scripts/test-deploy-batch-erc-eth-deployment.ts +++ b/packages/smart-contracts/scripts/test-deploy-batch-erc-eth-deployment.ts @@ -3,6 +3,7 @@ import { HardhatRuntimeEnvironment } from 'hardhat/types'; import { deployOne } from '../scripts/deploy-one'; import { batchPaymentsArtifact } from '../src/lib'; +import { deployAddressChecking } from './utils'; // Deploys, set up the contracts export async function deployBatchPayment(args: any, hre: HardhatRuntimeEnvironment): Promise { @@ -29,6 +30,12 @@ export async function deployBatchPayment(args: any, hre: HardhatRuntimeEnvironme console.log(` BatchPayments: ${BatchPaymentsAddress} `); + + deployAddressChecking( + 'BatchPayments', + BatchPaymentsAddress, + batchPaymentsArtifact.getAddress('private'), + ); } catch (e) { console.error(e); } diff --git a/packages/smart-contracts/scripts/utils.ts b/packages/smart-contracts/scripts/utils.ts index 404230836..e2129a2bf 100644 --- a/packages/smart-contracts/scripts/utils.ts +++ b/packages/smart-contracts/scripts/utils.ts @@ -45,3 +45,24 @@ export const jumpToNonce = async (args: any, hre: HardhatRuntimeEnvironment, non nextNonce = await deployer.getTransactionCount(); } }; + +/** Variable used to count the number of contracts deployed at the wrong address */ +export let NUMBER_ERRORS = 0; + +/** + * The function compare the address of the contract deployed with the existing one, usually stored in artifacts + * @param contratName name of the contract used to deployed an instance, or name of the instance if they are many implementations + * @param contractAddress address of the current deployement + * @param contractAddressExpected usually stored in artifacts + */ +export const deployAddressChecking = ( + contratName: string, + contractAddress: string, + contractAddressExpected: string, +): void => { + if (contractAddress !== contractAddressExpected) { + NUMBER_ERRORS += 1; + const msg = `${contratName} deployed is different from the one expected, please update your code or the artifact`; + throw Error(msg); + } +}; diff --git a/packages/smart-contracts/src/contracts/BatchConversionPayments.sol b/packages/smart-contracts/src/contracts/BatchConversionPayments.sol index 3c01f5d79..ead72e7c8 100644 --- a/packages/smart-contracts/src/contracts/BatchConversionPayments.sol +++ b/packages/smart-contracts/src/contracts/BatchConversionPayments.sol @@ -248,8 +248,7 @@ contract BatchConversionPayments is BatchPaymentsPublic { safeTransferFrom( uTokens[k].tokenAddress, _feeAddress, - ((uTokens[k].amountAndFee - excessAmount) * - batchConversionFee) / 10000 + ((uTokens[k].amountAndFee - excessAmount) * batchConversionFee) / 10000 ), 'batch fee transferFrom() failed' ); @@ -289,8 +288,8 @@ contract BatchConversionPayments is BatchPaymentsPublic { } // Check that batch contract has enough funds to pay batch conversion fees - uint256 amountBatchFees = (((contractBalance - address(this).balance) - ) * batchConversionFee) / 10000; + uint256 amountBatchFees = (((contractBalance - address(this).balance)) * batchConversionFee) / + 10000; require(address(this).balance >= amountBatchFees, 'not enough funds for batch conversion fees'); // Batch contract pays batch fee diff --git a/packages/smart-contracts/test/contracts/BatchConversionPayments.test.ts b/packages/smart-contracts/test/contracts/BatchConversionPayments.test.ts index c9fab91da..25c60834a 100644 --- a/packages/smart-contracts/test/contracts/BatchConversionPayments.test.ts +++ b/packages/smart-contracts/test/contracts/BatchConversionPayments.test.ts @@ -3,7 +3,6 @@ import { ERC20FeeProxy__factory, Erc20ConversionProxy__factory, EthConversionProxy__factory, - BatchConversionPayments__factory, EthereumFeeProxy__factory, ERC20FeeProxy, EthereumFeeProxy, @@ -17,7 +16,7 @@ import { import { BigNumber, BigNumberish, BytesLike, ContractTransaction, Signer } from 'ethers'; import { expect } from 'chai'; import { CurrencyManager } from '@requestnetwork/currency'; -import { chainlinkConversionPath } from '../../src/lib'; +import { chainlinkConversionPath, batchConversionPaymentsArtifact } from '../../src/lib'; import { localERC20AlphaArtifact, secondLocalERC20AlphaArtifact } from './localArtifacts'; import Utils from '@requestnetwork/utils'; import { HttpNetworkConfig } from 'hardhat/types'; @@ -129,14 +128,15 @@ describe('contract: BatchErc20ConversionPayments', () => { chainlinkPath.address, ETH_hash, ); - testBatchConversionProxy = await new BatchConversionPayments__factory(signer).deploy( - erc20FeeProxy.address, - ethereumFeeProxy.address, - testErc20ConversionProxy.address, - testEthConversionProxy.address, - chainlinkPath.address, - await signer.getAddress(), - ); + + testBatchConversionProxy = batchConversionPaymentsArtifact.connect(network.name, signer); + + // update batch payment proxies, chainlink path, and batch fees + await testBatchConversionProxy.setPaymentErc20Proxy(erc20FeeProxy.address); + await testBatchConversionProxy.setPaymentEthProxy(ethereumFeeProxy.address); + await testBatchConversionProxy.setPaymentErc20ConversionProxy(testErc20ConversionProxy.address); + await testBatchConversionProxy.setPaymentEthConversionProxy(testEthConversionProxy.address); + await testBatchConversionProxy.setConversionPathAddress(chainlinkPath.address); await testBatchConversionProxy.setBatchFee(batchFee); await testBatchConversionProxy.setBatchConversionFee(batchConvFee); @@ -144,7 +144,6 @@ describe('contract: BatchErc20ConversionPayments', () => { DAI_address = localERC20AlphaArtifact.getAddress(network.name); testERC20 = new TestERC20__factory(signer).attach(DAI_address); - // caution, change add one transaction in deployment will modify this address ! // fakeFAU_address = '0x51FC52Fd0B30fA0319D97893dEFE0201fEd39C4c'; fakeFAU_address = secondLocalERC20AlphaArtifact.getAddress(network.name); testERC20b = new TestERC20__factory(signer).attach(fakeFAU_address); From aa085636935c57a0840813e80fddacd238dd13fa Mon Sep 17 00:00:00 2001 From: olivier7delf <55892112+olivier7delf@users.noreply.github.com> Date: Mon, 8 Aug 2022 16:33:52 +0200 Subject: [PATCH 042/105] smart contract add variable tenThousand --- .../src/contracts/BatchConversionPayments.sol | 6 +++--- .../src/contracts/BatchPaymentsPublic.sol | 9 ++++++--- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/packages/smart-contracts/src/contracts/BatchConversionPayments.sol b/packages/smart-contracts/src/contracts/BatchConversionPayments.sol index ead72e7c8..643120a87 100644 --- a/packages/smart-contracts/src/contracts/BatchConversionPayments.sol +++ b/packages/smart-contracts/src/contracts/BatchConversionPayments.sol @@ -191,7 +191,7 @@ contract BatchConversionPayments is BatchPaymentsPublic { // For each token: check allowance, transfer funds on the contract and approve the paymentProxy to spend if needed for (uint256 k = 0; k < uTokens.length && uTokens[k].amountAndFee > 0; k++) { requestedToken = IERC20(uTokens[k].tokenAddress); - uTokens[k].batchFeeAmount = (uTokens[k].amountAndFee * batchConversionFee) / 10000; + uTokens[k].batchFeeAmount = (uTokens[k].amountAndFee * batchConversionFee) / tenThousand; // Check proxy's allowance from user, and user's funds to pay approximated amounts. require( requestedToken.allowance(msg.sender, address(this)) >= uTokens[k].amountAndFee, @@ -248,7 +248,7 @@ contract BatchConversionPayments is BatchPaymentsPublic { safeTransferFrom( uTokens[k].tokenAddress, _feeAddress, - ((uTokens[k].amountAndFee - excessAmount) * batchConversionFee) / 10000 + ((uTokens[k].amountAndFee - excessAmount) * batchConversionFee) / tenThousand ), 'batch fee transferFrom() failed' ); @@ -289,7 +289,7 @@ contract BatchConversionPayments is BatchPaymentsPublic { // Check that batch contract has enough funds to pay batch conversion fees uint256 amountBatchFees = (((contractBalance - address(this).balance)) * batchConversionFee) / - 10000; + tenThousand; require(address(this).balance >= amountBatchFees, 'not enough funds for batch conversion fees'); // Batch contract pays batch fee diff --git a/packages/smart-contracts/src/contracts/BatchPaymentsPublic.sol b/packages/smart-contracts/src/contracts/BatchPaymentsPublic.sol index 5f68e0d49..36dcd539e 100644 --- a/packages/smart-contracts/src/contracts/BatchPaymentsPublic.sol +++ b/packages/smart-contracts/src/contracts/BatchPaymentsPublic.sol @@ -27,6 +27,9 @@ contract BatchPaymentsPublic is Ownable { IEthereumFeeProxy public paymentEthProxy; uint256 public batchFee; + /** Used to to calcul batch fees */ + uint256 internal tenThousand = 10000; + // payerAuthorized is set to true only when needed for batch Eth conversion bool internal payerAuthorized; @@ -102,7 +105,7 @@ contract BatchPaymentsPublic is Ownable { } // amount is updated into batch fee amount - amount = (amount * batchFee) / 10000; + amount = (amount * batchFee) / tenThousand; // Check that batch contract has enough funds to pay batch fee require(address(this).balance >= amount, 'not enough funds for batch fee'); // Batch pays batch fee @@ -180,7 +183,7 @@ contract BatchPaymentsPublic is Ownable { } // amount is updated into batch fee amount - amount = (amount * batchFee) / 10000; + amount = (amount * batchFee) / tenThousand; // Check if the payer has enough funds to pay batch fee require(requestedToken.balanceOf(msg.sender) >= amount, 'not enough funds for the batch fee'); @@ -243,7 +246,7 @@ contract BatchPaymentsPublic is Ownable { // The payer transfers tokens to the batch contract and pays batch fee for (uint256 i = 0; i < uniqueTokens.length && uniqueTokens[i].amountAndFee > 0; i++) { - uniqueTokens[i].batchFeeAmount = (uniqueTokens[i].batchFeeAmount * batchFee) / 10000; + uniqueTokens[i].batchFeeAmount = (uniqueTokens[i].batchFeeAmount * batchFee) / tenThousand; IERC20 requestedToken = IERC20(uniqueTokens[i].tokenAddress); require( From 570de98850360ddb5182019be7487b6310c1a43c Mon Sep 17 00:00:00 2001 From: olivier7delf <55892112+olivier7delf@users.noreply.github.com> Date: Mon, 8 Aug 2022 16:38:50 +0200 Subject: [PATCH 043/105] contract require message - wording --- packages/smart-contracts/src/contracts/BatchPayments.sol | 4 ++-- .../smart-contracts/src/contracts/BatchPaymentsPublic.sol | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/smart-contracts/src/contracts/BatchPayments.sol b/packages/smart-contracts/src/contracts/BatchPayments.sol index 0947637f5..bc02063fb 100644 --- a/packages/smart-contracts/src/contracts/BatchPayments.sol +++ b/packages/smart-contracts/src/contracts/BatchPayments.sol @@ -145,7 +145,7 @@ contract BatchPayments is Ownable, ReentrancyGuard { IERC20 requestedToken = IERC20(_tokenAddress); require( requestedToken.allowance(msg.sender, address(this)) >= amount, - 'Not sufficient allowance for batch to pay' + 'Insufficient allowance for batch to pay' ); require(requestedToken.balanceOf(msg.sender) >= amount, 'not enough funds'); require( @@ -242,7 +242,7 @@ contract BatchPayments is Ownable, ReentrancyGuard { require( requestedToken.allowance(msg.sender, address(this)) >= uniqueTokens[i].amountAndFee + uniqueTokens[i].batchFeeAmount, - 'Not sufficient allowance for batch to pay' + 'Insufficient allowance for batch to pay' ); // check if the payer can pay the amount, the fee, and the batchFee require( diff --git a/packages/smart-contracts/src/contracts/BatchPaymentsPublic.sol b/packages/smart-contracts/src/contracts/BatchPaymentsPublic.sol index 36dcd539e..643e8d0ad 100644 --- a/packages/smart-contracts/src/contracts/BatchPaymentsPublic.sol +++ b/packages/smart-contracts/src/contracts/BatchPaymentsPublic.sol @@ -252,7 +252,7 @@ contract BatchPaymentsPublic is Ownable { require( requestedToken.allowance(msg.sender, address(this)) >= uniqueTokens[i].amountAndFee + uniqueTokens[i].batchFeeAmount, - 'Not sufficient allowance for batch to pay' + 'Insufficient allowance for batch to pay' ); // check if the payer can pay the amount, the fee, and the batchFee require( From b3787cb3cc7100ea64218b8745d8d188587d78c9 Mon Sep 17 00:00:00 2001 From: olivier7delf <55892112+olivier7delf@users.noreply.github.com> Date: Mon, 8 Aug 2022 16:56:19 +0200 Subject: [PATCH 044/105] smart contract delete chainlink --- ...test-deploy-batch-conversion-deployment.ts | 33 +++++++++---------- packages/smart-contracts/scripts/utils.ts | 2 +- .../src/contracts/BatchConversionPayments.sol | 13 -------- .../contracts/BatchConversionPayments.test.ts | 6 ++-- .../test/contracts/localArtifacts.ts | 2 +- 5 files changed, 19 insertions(+), 37 deletions(-) diff --git a/packages/smart-contracts/scripts/test-deploy-batch-conversion-deployment.ts b/packages/smart-contracts/scripts/test-deploy-batch-conversion-deployment.ts index fd36fcb29..7901a43c2 100644 --- a/packages/smart-contracts/scripts/test-deploy-batch-conversion-deployment.ts +++ b/packages/smart-contracts/scripts/test-deploy-batch-conversion-deployment.ts @@ -4,7 +4,6 @@ import { deployOne } from './deploy-one'; import { batchConversionPaymentsArtifact, - chainlinkConversionPath, erc20ConversionProxy, erc20FeeProxyArtifact, ethConversionArtifact, @@ -23,7 +22,6 @@ export async function deployBatchConversionPayment( console.log('start BatchConversionPayments'); const _ERC20FeeProxyAddress = erc20FeeProxyArtifact.getAddress('private'); const _EthereumFeeProxyAddress = ethereumFeeProxyArtifact.getAddress('private'); - const _chainlinkConversionPath = chainlinkConversionPath.getAddress('private'); const _paymentErc20ConversionFeeProxy = erc20ConversionProxy.getAddress('private'); const _paymentEthConversionFeeProxy = ethConversionArtifact.getAddress('private'); @@ -38,19 +36,14 @@ export async function deployBatchConversionPayment( _EthereumFeeProxyAddress, _paymentErc20ConversionFeeProxy, _paymentEthConversionFeeProxy, - _chainlinkConversionPath, await (await hre.ethers.getSigners())[0].getAddress(), ], }, ); - // Initialize batch conversion fee, useful to others packages. - const [owner] = await hre.ethers.getSigners(); - const batchConversion = batchConversionPaymentsArtifact.connect(hre.network.name, owner); - await batchConversion.connect(owner).setBatchFee(30); - await batchConversion.connect(owner).setBatchConversionFee(30); - // Add a second ERC20 token and aggregator - useful for batch test + console.log('start adding a second conversion path instance'); + const [owner] = await hre.ethers.getSigners(); const erc20Factory = await hre.ethers.getContractFactory('TestERC20'); const testERC20FakeFAU = await erc20Factory.deploy('1000000000000000000000000000000'); const { address: AggFakeFAU_USD_address } = await deployOne(args, hre, 'AggregatorMock', { @@ -65,23 +58,27 @@ export async function deployBatchConversionPayment( [AggFakeFAU_USD_address], ); - // ---------------------------------- - console.log('Contracts deployed'); - console.log(` - testERC20FakeFAU.address: ${testERC20FakeFAU.address} - BatchConversionPayments: ${BatchConversionPaymentsAddress} - `); - // Check the addresses of our contracts, to avoid misleading bugs in the tests - // ref to secondLocalERC20AlphaArtifact.getAddress('private'), that cannot be used in deployment - const fakeFAU_addressExpected = '0x51FC52Fd0B30fA0319D97893dEFE0201fEd39C4c'; + const fakeFAU_addressExpected = '0xe4e47451AAd6C89a6D9E4aD104A7b77FfE1D3b36'; deployAddressChecking('testERC20FakeFAU', testERC20FakeFAU.address, fakeFAU_addressExpected); deployAddressChecking( 'batchConversionPayments', BatchConversionPaymentsAddress, batchConversionPaymentsArtifact.getAddress('private'), ); + + // Initialize batch conversion fee, useful to others packages. + const batchConversion = batchConversionPaymentsArtifact.connect(hre.network.name, owner); + await batchConversion.connect(owner).setBatchFee(30); + await batchConversion.connect(owner).setBatchConversionFee(30); + + // ---------------------------------- + console.log('Contracts deployed'); + console.log(` + testERC20FakeFAU.address: ${testERC20FakeFAU.address} + BatchConversionPayments: ${BatchConversionPaymentsAddress} + `); } catch (e) { console.error(e); } diff --git a/packages/smart-contracts/scripts/utils.ts b/packages/smart-contracts/scripts/utils.ts index e2129a2bf..c712a7b81 100644 --- a/packages/smart-contracts/scripts/utils.ts +++ b/packages/smart-contracts/scripts/utils.ts @@ -62,7 +62,7 @@ export const deployAddressChecking = ( ): void => { if (contractAddress !== contractAddressExpected) { NUMBER_ERRORS += 1; - const msg = `${contratName} deployed is different from the one expected, please update your code or the artifact`; + const msg = `${contratName} deployed at ${contractAddress} is different from the one expected: ${contractAddressExpected}, please update your code or the artifact`; throw Error(msg); } }; diff --git a/packages/smart-contracts/src/contracts/BatchConversionPayments.sol b/packages/smart-contracts/src/contracts/BatchConversionPayments.sol index 643120a87..3adcfca5f 100644 --- a/packages/smart-contracts/src/contracts/BatchConversionPayments.sol +++ b/packages/smart-contracts/src/contracts/BatchConversionPayments.sol @@ -3,7 +3,6 @@ pragma solidity ^0.8.4; import './interfaces/IERC20ConversionProxy.sol'; import './interfaces/IEthConversionProxy.sol'; -import './ChainlinkConversionPath.sol'; import './BatchPaymentsPublic.sol'; /** @@ -25,7 +24,6 @@ contract BatchConversionPayments is BatchPaymentsPublic { IERC20ConversionProxy paymentErc20ConversionProxy; IEthConversionProxy paymentEthConversionProxy; - ChainlinkConversionPath public chainlinkConversionPath; uint256 public batchConversionFee; @@ -78,7 +76,6 @@ contract BatchConversionPayments is BatchPaymentsPublic { * @param _paymentEthProxy The ETH payment proxy address to use. * @param _paymentErc20ConversionProxy The ERC20 Conversion payment proxy address to use. * @param _paymentEthConversionFeeProxy The ETH Conversion payment proxy address to use. - * @param _chainlinkConversionPathAddress The conversion path contract address * @param _owner Owner of the contract. */ constructor( @@ -86,7 +83,6 @@ contract BatchConversionPayments is BatchPaymentsPublic { address _paymentEthProxy, address _paymentErc20ConversionProxy, address _paymentEthConversionFeeProxy, - address _chainlinkConversionPathAddress, address _owner ) BatchPaymentsPublic(_paymentErc20Proxy, _paymentEthProxy, _owner) { paymentErc20Proxy = IERC20FeeProxy(_paymentErc20Proxy); @@ -94,7 +90,6 @@ contract BatchConversionPayments is BatchPaymentsPublic { paymentErc20ConversionProxy = IERC20ConversionProxy(_paymentErc20ConversionProxy); paymentEthConversionProxy = IEthConversionProxy(_paymentEthConversionFeeProxy); - chainlinkConversionPath = ChainlinkConversionPath(_chainlinkConversionPathAddress); transferOwnership(_owner); batchFee = 0; @@ -328,12 +323,4 @@ contract BatchConversionPayments is BatchPaymentsPublic { function setPaymentEthConversionProxy(address _paymentEthConversionProxy) external onlyOwner { paymentEthConversionProxy = IEthConversionProxy(_paymentEthConversionProxy); } - - /** - * @notice Update the conversion path contract used to fetch conversions - * @param _chainlinkConversionPathAddress address of the conversion path contract - */ - function setConversionPathAddress(address _chainlinkConversionPathAddress) external onlyOwner { - chainlinkConversionPath = ChainlinkConversionPath(_chainlinkConversionPathAddress); - } } diff --git a/packages/smart-contracts/test/contracts/BatchConversionPayments.test.ts b/packages/smart-contracts/test/contracts/BatchConversionPayments.test.ts index 25c60834a..ef764bc6f 100644 --- a/packages/smart-contracts/test/contracts/BatchConversionPayments.test.ts +++ b/packages/smart-contracts/test/contracts/BatchConversionPayments.test.ts @@ -131,12 +131,11 @@ describe('contract: BatchErc20ConversionPayments', () => { testBatchConversionProxy = batchConversionPaymentsArtifact.connect(network.name, signer); - // update batch payment proxies, chainlink path, and batch fees + // update batch payment proxies, and batch fees await testBatchConversionProxy.setPaymentErc20Proxy(erc20FeeProxy.address); await testBatchConversionProxy.setPaymentEthProxy(ethereumFeeProxy.address); await testBatchConversionProxy.setPaymentErc20ConversionProxy(testErc20ConversionProxy.address); await testBatchConversionProxy.setPaymentEthConversionProxy(testEthConversionProxy.address); - await testBatchConversionProxy.setConversionPathAddress(chainlinkPath.address); await testBatchConversionProxy.setBatchFee(batchFee); await testBatchConversionProxy.setBatchConversionFee(batchConvFee); @@ -144,7 +143,6 @@ describe('contract: BatchErc20ConversionPayments', () => { DAI_address = localERC20AlphaArtifact.getAddress(network.name); testERC20 = new TestERC20__factory(signer).attach(DAI_address); - // fakeFAU_address = '0x51FC52Fd0B30fA0319D97893dEFE0201fEd39C4c'; fakeFAU_address = secondLocalERC20AlphaArtifact.getAddress(network.name); testERC20b = new TestERC20__factory(signer).attach(fakeFAU_address); batchAddress = testBatchConversionProxy.address; @@ -733,7 +731,7 @@ describe('contract: BatchErc20ConversionPayments', () => { }); }; - // ERC20TestSuite('batchRouter'); + ERC20TestSuite('batchRouter'); ERC20TestSuite('batchERC20ConversionPaymentsMultiTokens'); EthTestSuite('batchRouter'); EthTestSuite('batchEthConversionPaymentsWithReference'); diff --git a/packages/smart-contracts/test/contracts/localArtifacts.ts b/packages/smart-contracts/test/contracts/localArtifacts.ts index 9a8b6b2ba..f87e518d2 100644 --- a/packages/smart-contracts/test/contracts/localArtifacts.ts +++ b/packages/smart-contracts/test/contracts/localArtifacts.ts @@ -22,7 +22,7 @@ export const secondLocalERC20AlphaArtifact = new ContractArtifact( abi: [], deployment: { private: { - address: '0x51FC52Fd0B30fA0319D97893dEFE0201fEd39C4c', + address: '0xe4e47451AAd6C89a6D9E4aD104A7b77FfE1D3b36', creationBlockNumber: 0, }, }, From 101d9cdcacf95fdb44f1dca87db7565b02f0b619 Mon Sep 17 00:00:00 2001 From: olivier7delf <55892112+olivier7delf@users.noreply.github.com> Date: Mon, 8 Aug 2022 17:30:44 +0200 Subject: [PATCH 045/105] revert batchPayments modif --- packages/smart-contracts/src/contracts/BatchPayments.sol | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/smart-contracts/src/contracts/BatchPayments.sol b/packages/smart-contracts/src/contracts/BatchPayments.sol index bc02063fb..0947637f5 100644 --- a/packages/smart-contracts/src/contracts/BatchPayments.sol +++ b/packages/smart-contracts/src/contracts/BatchPayments.sol @@ -145,7 +145,7 @@ contract BatchPayments is Ownable, ReentrancyGuard { IERC20 requestedToken = IERC20(_tokenAddress); require( requestedToken.allowance(msg.sender, address(this)) >= amount, - 'Insufficient allowance for batch to pay' + 'Not sufficient allowance for batch to pay' ); require(requestedToken.balanceOf(msg.sender) >= amount, 'not enough funds'); require( @@ -242,7 +242,7 @@ contract BatchPayments is Ownable, ReentrancyGuard { require( requestedToken.allowance(msg.sender, address(this)) >= uniqueTokens[i].amountAndFee + uniqueTokens[i].batchFeeAmount, - 'Insufficient allowance for batch to pay' + 'Not sufficient allowance for batch to pay' ); // check if the payer can pay the amount, the fee, and the batchFee require( From 70f9fe9877cd46b93614d35b0f48154b5bc4c89b Mon Sep 17 00:00:00 2001 From: olivier7delf <55892112+olivier7delf@users.noreply.github.com> Date: Mon, 8 Aug 2022 17:42:58 +0200 Subject: [PATCH 046/105] contract - remove irrelevant variable - wording uTokens --- .../src/contracts/BatchConversionPayments.sol | 15 ++++---- .../src/contracts/BatchPaymentsPublic.sol | 34 +++++++++---------- 2 files changed, 24 insertions(+), 25 deletions(-) diff --git a/packages/smart-contracts/src/contracts/BatchConversionPayments.sol b/packages/smart-contracts/src/contracts/BatchConversionPayments.sol index 3adcfca5f..332937df5 100644 --- a/packages/smart-contracts/src/contracts/BatchConversionPayments.sol +++ b/packages/smart-contracts/src/contracts/BatchConversionPayments.sol @@ -214,16 +214,15 @@ contract BatchConversionPayments is BatchPaymentsPublic { // Batch pays the requests using Erc20ConversionFeeProxy for (uint256 i = 0; i < requestsInfo.length; i++) { - RequestInfo memory rI = requestsInfo[i]; paymentErc20ConversionProxy.transferFromWithReferenceAndFee( - rI.recipient, - rI.requestAmount, - rI.path, - rI.paymentReference, - rI.feeAmount, + requestsInfo[i].recipient, + requestsInfo[i].requestAmount, + requestsInfo[i].path, + requestsInfo[i].paymentReference, + requestsInfo[i].feeAmount, _feeAddress, - rI.maxToSpend, - rI.maxRateTimespan + requestsInfo[i].maxToSpend, + requestsInfo[i].maxRateTimespan ); } diff --git a/packages/smart-contracts/src/contracts/BatchPaymentsPublic.sol b/packages/smart-contracts/src/contracts/BatchPaymentsPublic.sol index 643e8d0ad..899e36f47 100644 --- a/packages/smart-contracts/src/contracts/BatchPaymentsPublic.sol +++ b/packages/smart-contracts/src/contracts/BatchPaymentsPublic.sol @@ -225,59 +225,59 @@ contract BatchPaymentsPublic is Ownable { // Create a list of unique tokens used and the amounts associated // Only considere tokens having: amounts + feeAmounts > 0 // batchFeeAmount is the amount's sum, and then, batch fee rate is applied - Token[] memory uniqueTokens = new Token[](_tokenAddresses.length); + Token[] memory uTokens = new Token[](_tokenAddresses.length); for (uint256 i = 0; i < _tokenAddresses.length; i++) { for (uint256 j = 0; j < _tokenAddresses.length; j++) { - // If the token is already in the existing uniqueTokens list - if (uniqueTokens[j].tokenAddress == _tokenAddresses[i]) { - uniqueTokens[j].amountAndFee += _amounts[i] + _feeAmounts[i]; - uniqueTokens[j].batchFeeAmount += _amounts[i]; + // If the token is already in the existing uTokens list + if (uTokens[j].tokenAddress == _tokenAddresses[i]) { + uTokens[j].amountAndFee += _amounts[i] + _feeAmounts[i]; + uTokens[j].batchFeeAmount += _amounts[i]; break; } // If the token is not in the list (amountAndFee = 0), and amount + fee > 0 - if (uniqueTokens[j].amountAndFee == 0 && (_amounts[i] + _feeAmounts[i]) > 0) { - uniqueTokens[j].tokenAddress = _tokenAddresses[i]; - uniqueTokens[j].amountAndFee = _amounts[i] + _feeAmounts[i]; - uniqueTokens[j].batchFeeAmount = _amounts[i]; + if (uTokens[j].amountAndFee == 0 && (_amounts[i] + _feeAmounts[i]) > 0) { + uTokens[j].tokenAddress = _tokenAddresses[i]; + uTokens[j].amountAndFee = _amounts[i] + _feeAmounts[i]; + uTokens[j].batchFeeAmount = _amounts[i]; break; } } } // The payer transfers tokens to the batch contract and pays batch fee - for (uint256 i = 0; i < uniqueTokens.length && uniqueTokens[i].amountAndFee > 0; i++) { - uniqueTokens[i].batchFeeAmount = (uniqueTokens[i].batchFeeAmount * batchFee) / tenThousand; - IERC20 requestedToken = IERC20(uniqueTokens[i].tokenAddress); + for (uint256 i = 0; i < uTokens.length && uTokens[i].amountAndFee > 0; i++) { + uTokens[i].batchFeeAmount = (uTokens[i].batchFeeAmount * batchFee) / tenThousand; + IERC20 requestedToken = IERC20(uTokens[i].tokenAddress); require( requestedToken.allowance(msg.sender, address(this)) >= - uniqueTokens[i].amountAndFee + uniqueTokens[i].batchFeeAmount, + uTokens[i].amountAndFee + uTokens[i].batchFeeAmount, 'Insufficient allowance for batch to pay' ); // check if the payer can pay the amount, the fee, and the batchFee require( requestedToken.balanceOf(msg.sender) >= - uniqueTokens[i].amountAndFee + uniqueTokens[i].batchFeeAmount, + uTokens[i].amountAndFee + uTokens[i].batchFeeAmount, 'not enough funds' ); // Transfer only the amount and fee required for the token on the batch contract require( - safeTransferFrom(uniqueTokens[i].tokenAddress, address(this), uniqueTokens[i].amountAndFee), + safeTransferFrom(uTokens[i].tokenAddress, address(this), uTokens[i].amountAndFee), 'payment transferFrom() failed' ); // Batch contract approves Erc20FeeProxy to spend the token if ( requestedToken.allowance(address(this), address(paymentErc20Proxy)) < - uniqueTokens[i].amountAndFee + uTokens[i].amountAndFee ) { approvePaymentProxyToSpend(address(requestedToken), address(paymentErc20Proxy)); } // Payer pays batch fee amount require( - safeTransferFrom(uniqueTokens[i].tokenAddress, _feeAddress, uniqueTokens[i].batchFeeAmount), + safeTransferFrom(uTokens[i].tokenAddress, _feeAddress, uTokens[i].batchFeeAmount), 'batch fee transferFrom() failed' ); } From 4b9f0af565accf28f244e45a37c0cc8282f9450c Mon Sep 17 00:00:00 2001 From: olivier7delf <55892112+olivier7delf@users.noreply.github.com> Date: Mon, 8 Aug 2022 18:08:12 +0200 Subject: [PATCH 047/105] renaming requestInfo into conversionDetail and requestsInfoParent into cryptoDetails --- .../src/contracts/BatchConversionPayments.sol | 132 +++++++++--------- .../src/contracts/BatchPaymentsPublic.sol | 3 +- .../BatchConversionPayments/0.1.0.json | 51 ++----- .../contracts/BatchConversionPayments.test.ts | 126 ++++++++--------- 4 files changed, 140 insertions(+), 172 deletions(-) diff --git a/packages/smart-contracts/src/contracts/BatchConversionPayments.sol b/packages/smart-contracts/src/contracts/BatchConversionPayments.sol index 332937df5..95fa505e8 100644 --- a/packages/smart-contracts/src/contracts/BatchConversionPayments.sol +++ b/packages/smart-contracts/src/contracts/BatchConversionPayments.sol @@ -38,7 +38,7 @@ contract BatchConversionPayments is BatchPaymentsPublic { * it includes fee proxy but NOT the batchConversionFee * _maxRateTimespan Max acceptable times span for conversion rates, ignored if zero */ - struct RequestInfo { + struct ConversionDetail { address recipient; uint256 requestAmount; address[] path; @@ -51,7 +51,7 @@ contract BatchConversionPayments is BatchPaymentsPublic { /** * @dev BatchPaymentsPublic contract input structure. */ - struct RequestsInfoParent { + struct CryptoDetails { address[] tokenAddresses; address[] recipients; uint256[] amounts; @@ -62,13 +62,13 @@ contract BatchConversionPayments is BatchPaymentsPublic { /** * @dev Used by the batchRouter to handle information for heterogeneous batches, grouped by payment network. * - paymentNetworkId: from 0 to 4, cf. `batchRouter()` method. - * - requestsInfo all the data required for conversion requests to be paid, for paymentNetworkId = 0 or 3 - * - requestsInfoParent all the data required to pay requests without conversion, for paymentNetworkId = 1, 2, or 4 + * - conversionDetails all the data required for conversion requests to be paid, for paymentNetworkId = 0 or 3 + * - cryptoDetails all the data required to pay requests without conversion, for paymentNetworkId = 1, 2, or 4 */ - struct MetaRequestsInfo { + struct MetaDetail { uint256 paymentNetworkId; - RequestInfo[] requestsInfo; - RequestsInfoParent requestsInfoParent; + ConversionDetail[] conversionDetails; + CryptoDetails cryptoDetails; } /** @@ -98,7 +98,7 @@ contract BatchConversionPayments is BatchPaymentsPublic { /** * @notice Batch payments on different payment networks at once. - * @param metaRequestsInfos contains paymentNetworkId and requestsInfo + * @param metaDetails contains paymentNetworkId and conversionDetails * - batchERC20ConversionPaymentsMultiTokens, paymentNetworkId=0 * - batchERC20PaymentsWithReference, paymentNetworkId=1 * - batchERC20PaymentsMultiTokensWithReference, paymentNetworkId=2 @@ -108,44 +108,44 @@ contract BatchConversionPayments is BatchPaymentsPublic { * @dev batchRouter only reduces gas consumption when using more than a single payment network. * For single payment network payments, it is more efficient to use the suited batch function. */ - function batchRouter(MetaRequestsInfo[] calldata metaRequestsInfos, address _feeAddress) - external - payable - { - require(metaRequestsInfos.length < 4, 'more than 4 requestsinfo'); - for (uint256 i = 0; i < metaRequestsInfos.length; i++) { - MetaRequestsInfo calldata metaRequestsInfo = metaRequestsInfos[i]; - if (metaRequestsInfo.paymentNetworkId == 0) { - batchERC20ConversionPaymentsMultiTokens(metaRequestsInfo.requestsInfo, _feeAddress); - } else if (metaRequestsInfo.paymentNetworkId == 1) { + function batchRouter(MetaDetail[] calldata metaDetails, address _feeAddress) external payable { + require(metaDetails.length < 4, 'more than 4 conversionDetails'); + for (uint256 i = 0; i < metaDetails.length; i++) { + MetaDetail calldata metaConversionDetail = metaDetails[i]; + if (metaConversionDetail.paymentNetworkId == 0) { + batchERC20ConversionPaymentsMultiTokens( + metaConversionDetail.conversionDetails, + _feeAddress + ); + } else if (metaConversionDetail.paymentNetworkId == 1) { batchERC20PaymentsWithReference( - metaRequestsInfo.requestsInfoParent.tokenAddresses[0], - metaRequestsInfo.requestsInfoParent.recipients, - metaRequestsInfo.requestsInfoParent.amounts, - metaRequestsInfo.requestsInfoParent.paymentReferences, - metaRequestsInfo.requestsInfoParent.feeAmounts, + metaConversionDetail.cryptoDetails.tokenAddresses[0], + metaConversionDetail.cryptoDetails.recipients, + metaConversionDetail.cryptoDetails.amounts, + metaConversionDetail.cryptoDetails.paymentReferences, + metaConversionDetail.cryptoDetails.feeAmounts, _feeAddress ); - } else if (metaRequestsInfo.paymentNetworkId == 2) { + } else if (metaConversionDetail.paymentNetworkId == 2) { batchERC20PaymentsMultiTokensWithReference( - metaRequestsInfo.requestsInfoParent.tokenAddresses, - metaRequestsInfo.requestsInfoParent.recipients, - metaRequestsInfo.requestsInfoParent.amounts, - metaRequestsInfo.requestsInfoParent.paymentReferences, - metaRequestsInfo.requestsInfoParent.feeAmounts, + metaConversionDetail.cryptoDetails.tokenAddresses, + metaConversionDetail.cryptoDetails.recipients, + metaConversionDetail.cryptoDetails.amounts, + metaConversionDetail.cryptoDetails.paymentReferences, + metaConversionDetail.cryptoDetails.feeAmounts, _feeAddress ); - } else if (metaRequestsInfo.paymentNetworkId == 3) { + } else if (metaConversionDetail.paymentNetworkId == 3) { batchEthConversionPaymentsWithReference( - metaRequestsInfo.requestsInfo, + metaConversionDetail.conversionDetails, payable(_feeAddress) ); - } else if (metaRequestsInfo.paymentNetworkId == 4) { + } else if (metaConversionDetail.paymentNetworkId == 4) { batchEthPaymentsWithReference( - metaRequestsInfo.requestsInfoParent.recipients, - metaRequestsInfo.requestsInfoParent.amounts, - metaRequestsInfo.requestsInfoParent.paymentReferences, - metaRequestsInfo.requestsInfoParent.feeAmounts, + metaConversionDetail.cryptoDetails.recipients, + metaConversionDetail.cryptoDetails.amounts, + metaConversionDetail.cryptoDetails.paymentReferences, + metaConversionDetail.cryptoDetails.feeAmounts, payable(_feeAddress) ); } else { @@ -155,28 +155,31 @@ contract BatchConversionPayments is BatchPaymentsPublic { } /** - * @notice Makes a batch of transfers for multiple ERC20 tokens, with amounts based on a request currency (e.g. fiat) and with a reference per payment. - * @param requestsInfo list of requestInfo, each one containing all the information of a request + * @notice Makes a batch of transfers for multiple ERC20 tokens, with amounts based on a request + * currency (e.g. fiat) and with a reference per payment. + * @param conversionDetails list of requestInfo, each one containing all the information of a request * @param _feeAddress The fee recipient */ function batchERC20ConversionPaymentsMultiTokens( - RequestInfo[] calldata requestsInfo, + ConversionDetail[] calldata conversionDetails, address _feeAddress ) public { // a list of unique tokens, with the sum of maxToSpend by token - Token[] memory uTokens = new Token[](requestsInfo.length); - for (uint256 i = 0; i < requestsInfo.length; i++) { - for (uint256 k = 0; k < requestsInfo.length; k++) { + Token[] memory uTokens = new Token[](conversionDetails.length); + for (uint256 i = 0; i < conversionDetails.length; i++) { + for (uint256 k = 0; k < conversionDetails.length; k++) { // If the token is already in the existing uTokens list - if (uTokens[k].tokenAddress == requestsInfo[i].path[requestsInfo[i].path.length - 1]) { - uTokens[k].amountAndFee += requestsInfo[i].maxToSpend; + if ( + uTokens[k].tokenAddress == conversionDetails[i].path[conversionDetails[i].path.length - 1] + ) { + uTokens[k].amountAndFee += conversionDetails[i].maxToSpend; break; } // If the token is not in the list (amountAndFee = 0) - else if (uTokens[k].amountAndFee == 0 && (requestsInfo[i].maxToSpend) > 0) { - uTokens[k].tokenAddress = requestsInfo[i].path[requestsInfo[i].path.length - 1]; + else if (uTokens[k].amountAndFee == 0 && (conversionDetails[i].maxToSpend) > 0) { + uTokens[k].tokenAddress = conversionDetails[i].path[conversionDetails[i].path.length - 1]; // amountAndFee is used to store _maxToSpend, useful to send enough tokens to this contract - uTokens[k].amountAndFee = requestsInfo[i].maxToSpend; + uTokens[k].amountAndFee = conversionDetails[i].maxToSpend; break; } } @@ -213,16 +216,17 @@ contract BatchConversionPayments is BatchPaymentsPublic { } // Batch pays the requests using Erc20ConversionFeeProxy - for (uint256 i = 0; i < requestsInfo.length; i++) { + for (uint256 i = 0; i < conversionDetails.length; i++) { + ConversionDetail memory rI = conversionDetails[i]; paymentErc20ConversionProxy.transferFromWithReferenceAndFee( - requestsInfo[i].recipient, - requestsInfo[i].requestAmount, - requestsInfo[i].path, - requestsInfo[i].paymentReference, - requestsInfo[i].feeAmount, + rI.recipient, + rI.requestAmount, + rI.path, + rI.paymentReference, + rI.feeAmount, _feeAddress, - requestsInfo[i].maxToSpend, - requestsInfo[i].maxRateTimespan + rI.maxToSpend, + rI.maxRateTimespan ); } @@ -252,7 +256,7 @@ contract BatchConversionPayments is BatchPaymentsPublic { /** * @notice Send a batch of ETH conversion payments with fees and paymentReferences to multiple accounts. * If one payment fails, the whole batch is reverted. - * @param requestsInfo List of requestInfos, each one containing all the information of a request. + * @param conversionDetails List of requestInfos, each one containing all the information of a request. * _maxToSpend is not used in this function. * @param _feeAddress The fee recipient. * @dev It uses EthereumConversionProxy to pay an invoice and fees. @@ -262,22 +266,22 @@ contract BatchConversionPayments is BatchPaymentsPublic { * This choice reduces the gas significantly, by delegating the whole conversion to the payment proxy. */ function batchEthConversionPaymentsWithReference( - RequestInfo[] calldata requestsInfo, + ConversionDetail[] calldata conversionDetails, address payable _feeAddress ) public payable { uint256 contractBalance = address(this).balance; payerAuthorized = true; // Batch contract pays the requests through EthConversionProxy - for (uint256 i = 0; i < requestsInfo.length; i++) { + for (uint256 i = 0; i < conversionDetails.length; i++) { paymentEthConversionProxy.transferWithReferenceAndFee{value: address(this).balance}( - payable(requestsInfo[i].recipient), - requestsInfo[i].requestAmount, - requestsInfo[i].path, - requestsInfo[i].paymentReference, - requestsInfo[i].feeAmount, + payable(conversionDetails[i].recipient), + conversionDetails[i].requestAmount, + conversionDetails[i].path, + conversionDetails[i].paymentReference, + conversionDetails[i].feeAmount, _feeAddress, - requestsInfo[i].maxRateTimespan + conversionDetails[i].maxRateTimespan ); } diff --git a/packages/smart-contracts/src/contracts/BatchPaymentsPublic.sol b/packages/smart-contracts/src/contracts/BatchPaymentsPublic.sol index 899e36f47..1ac74deaf 100644 --- a/packages/smart-contracts/src/contracts/BatchPaymentsPublic.sol +++ b/packages/smart-contracts/src/contracts/BatchPaymentsPublic.sol @@ -256,8 +256,7 @@ contract BatchPaymentsPublic is Ownable { ); // check if the payer can pay the amount, the fee, and the batchFee require( - requestedToken.balanceOf(msg.sender) >= - uTokens[i].amountAndFee + uTokens[i].batchFeeAmount, + requestedToken.balanceOf(msg.sender) >= uTokens[i].amountAndFee + uTokens[i].batchFeeAmount, 'not enough funds' ); diff --git a/packages/smart-contracts/src/lib/artifacts/BatchConversionPayments/0.1.0.json b/packages/smart-contracts/src/lib/artifacts/BatchConversionPayments/0.1.0.json index d9c135df0..377ce324c 100644 --- a/packages/smart-contracts/src/lib/artifacts/BatchConversionPayments/0.1.0.json +++ b/packages/smart-contracts/src/lib/artifacts/BatchConversionPayments/0.1.0.json @@ -22,11 +22,6 @@ "name": "_paymentEthConversionFeeProxy", "type": "address" }, - { - "internalType": "address", - "name": "_chainlinkConversionPathAddress", - "type": "address" - }, { "internalType": "address", "name": "_owner", @@ -108,8 +103,8 @@ "type": "uint256" } ], - "internalType": "struct BatchConversionPayments.RequestInfo[]", - "name": "requestsInfo", + "internalType": "struct BatchConversionPayments.ConversionDetail[]", + "name": "conversionDetails", "type": "tuple[]" }, { @@ -239,8 +234,8 @@ "type": "uint256" } ], - "internalType": "struct BatchConversionPayments.RequestInfo[]", - "name": "requestsInfo", + "internalType": "struct BatchConversionPayments.ConversionDetail[]", + "name": "conversionDetails", "type": "tuple[]" }, { @@ -347,8 +342,8 @@ "type": "uint256" } ], - "internalType": "struct BatchConversionPayments.RequestInfo[]", - "name": "requestsInfo", + "internalType": "struct BatchConversionPayments.ConversionDetail[]", + "name": "conversionDetails", "type": "tuple[]" }, { @@ -379,13 +374,13 @@ "type": "uint256[]" } ], - "internalType": "struct BatchConversionPayments.RequestsInfoParent", - "name": "requestsInfoParent", + "internalType": "struct BatchConversionPayments.CryptoDetails", + "name": "cryptoDetails", "type": "tuple" } ], - "internalType": "struct BatchConversionPayments.MetaRequestsInfo[]", - "name": "metaRequestsInfos", + "internalType": "struct BatchConversionPayments.MetaDetail[]", + "name": "metaDetails", "type": "tuple[]" }, { @@ -399,19 +394,6 @@ "stateMutability": "payable", "type": "function" }, - { - "inputs": [], - "name": "chainlinkConversionPath", - "outputs": [ - { - "internalType": "contract ChainlinkConversionPath", - "name": "", - "type": "address" - } - ], - "stateMutability": "view", - "type": "function" - }, { "inputs": [], "name": "owner", @@ -484,19 +466,6 @@ "stateMutability": "nonpayable", "type": "function" }, - { - "inputs": [ - { - "internalType": "address", - "name": "_chainlinkConversionPathAddress", - "type": "address" - } - ], - "name": "setConversionPathAddress", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, { "inputs": [ { diff --git a/packages/smart-contracts/test/contracts/BatchConversionPayments.test.ts b/packages/smart-contracts/test/contracts/BatchConversionPayments.test.ts index ef764bc6f..4c2d255b5 100644 --- a/packages/smart-contracts/test/contracts/BatchConversionPayments.test.ts +++ b/packages/smart-contracts/test/contracts/BatchConversionPayments.test.ts @@ -80,7 +80,7 @@ describe('contract: BatchErc20ConversionPayments', () => { let toDiffBalanceExpected: BigNumber; let feeDiffBalanceExpected: BigNumber; - type RequestInfo = { + type ConversionDetail = { recipient: string; requestAmount: BigNumberish; path: string[]; @@ -89,9 +89,9 @@ describe('contract: BatchErc20ConversionPayments', () => { maxToSpend: BigNumberish; maxRateTimespan: BigNumberish; }; - let requestInfo: RequestInfo; + let convDetail: ConversionDetail; - let requestsInfoParent1 = { + let cryptoDetails1 = { tokenAddresses: [], recipients: [], amounts: [], @@ -153,9 +153,9 @@ describe('contract: BatchErc20ConversionPayments', () => { }; /** - * @notice it gets the conversions including fees to be paid, and it set the requestInfo + * @notice it gets the conversions including fees to be paid, and it set the convDetail */ - const initConvToPayAndRequestInfo = async ( + const initConvToPayAndConvDetail = async ( _recipient: string, _path: string[], _requestAmount: string, @@ -165,7 +165,7 @@ describe('contract: BatchErc20ConversionPayments', () => { ) => { conversionToPay = await _chainlinkPath.getConversion(_requestAmount, _path); conversionFees = await _chainlinkPath.getConversion(_feeAmount, _path); - requestInfo = { + convDetail = { recipient: _recipient, requestAmount: _requestAmount, path: _path, @@ -260,20 +260,20 @@ describe('contract: BatchErc20ConversionPayments', () => { }; /** - * @notice update requestInfo, do an ERC20 conv batch payment and calcul the balances + * @notice update convDetail, do an ERC20 conv batch payment and calcul the balances * @param path to update the resquestInfo */ const transferOneTokenConv = async (path: string[]) => { - await initConvToPayAndRequestInfo(to, path, amountInFiat, feesAmountInFiat, 0, chainlinkPath); + await initConvToPayAndConvDetail(to, path, amountInFiat, feesAmountInFiat, 0, chainlinkPath); - const result = batchConvFunction(argTemplate([requestInfo]), feeAddress); + const result = batchConvFunction(argTemplate([convDetail]), feeAddress); if (logGas) { const tx = await result; await tx.wait(1); const receipt = await tx.wait(); console.log(`gas consumption: `, receipt.gasUsed.toString()); } else { - await emitOneTx(expect(result), requestInfo, conversionToPay, conversionFees); + await emitOneTx(expect(result), convDetail, conversionToPay, conversionFees); } calculERC20Balances( @@ -285,7 +285,7 @@ describe('contract: BatchErc20ConversionPayments', () => { }; /** - * @notice generate nTimes 2 requestInfos, do an ERC20 conv batch payment with theses 2*nTimes requests + * @notice generate nTimes 2 convDetails, do an ERC20 conv batch payment with theses 2*nTimes requests * and calcul the balances * @param path2 to update the second resquestInfo */ @@ -297,22 +297,22 @@ describe('contract: BatchErc20ConversionPayments', () => { const conversionToPay2 = await chainlinkPath.getConversion(amountInFiat2, path2); const conversionFees2 = await chainlinkPath.getConversion(feesAmountInFiat2, path2); - let requestInfo2 = Utils.deepCopy(requestInfo); + let convDetail2 = Utils.deepCopy(convDetail); - requestInfo2.path = path2; - requestInfo2.requestAmount = amountInFiat2; - requestInfo2.feeAmount = feesAmountInFiat2; - requestInfo2.maxToSpend = conversionToPay2.result.add(conversionFees2.result).toString(); + convDetail2.path = path2; + convDetail2.requestAmount = amountInFiat2; + convDetail2.feeAmount = feesAmountInFiat2; + convDetail2.maxToSpend = conversionToPay2.result.add(conversionFees2.result).toString(); - let requestInfos: RequestInfo[] = []; + let convDetails: ConversionDetail[] = []; let conversionsToPay: ConvToPay[] = []; let conversionsFees: ConvToPay[] = []; for (let i = 0; i < nTimes; i++) { - requestInfos = requestInfos.concat([requestInfo, requestInfo2]); + convDetails = convDetails.concat([convDetail, convDetail2]); conversionsToPay = conversionsToPay.concat([conversionToPay, conversionToPay2]); conversionsFees = conversionsFees.concat([conversionFees, conversionFees2]); } - const result = batchConvFunction(argTemplate(requestInfos), feeAddress); + const result = batchConvFunction(argTemplate(convDetails), feeAddress); const tx = await result; await tx.wait(1); if (logGas) { @@ -321,8 +321,7 @@ describe('contract: BatchErc20ConversionPayments', () => { } if ( - requestInfo.path[requestInfo.path.length - 1] === - requestInfo2.path[requestInfo2.path.length - 1] + convDetail.path[convDetail.path.length - 1] === convDetail2.path[convDetail2.path.length - 1] ) { for (let i = 0; i < nTimes - 1; i++) { calculERC20Balances(conversionToPay.result, conversionFees.result, [], []); @@ -357,7 +356,7 @@ describe('contract: BatchErc20ConversionPayments', () => { const ERC20TestSuite = (erc20Function: string) => { emitOneTx = ( result: Chai.Assertion, - requestInfo: RequestInfo, + convDetail: ConversionDetail, _conversionToPay = conversionToPay, _conversionFees = conversionFees, _testErc20ConversionProxy = testErc20ConversionProxy, @@ -365,16 +364,16 @@ describe('contract: BatchErc20ConversionPayments', () => { return result.to .emit(testErc20ConversionProxy, 'TransferWithConversionAndReference') .withArgs( - requestInfo.requestAmount, - ethers.utils.getAddress(requestInfo.path[0]), + convDetail.requestAmount, + ethers.utils.getAddress(convDetail.path[0]), ethers.utils.keccak256(referenceExample), - requestInfo.feeAmount, + convDetail.feeAmount, '0', ) .to.emit(testErc20ConversionProxy, 'TransferWithReferenceAndFee') .withArgs( ethers.utils.getAddress(DAI_address), - ethers.utils.getAddress(requestInfo.recipient), + ethers.utils.getAddress(convDetail.recipient), _conversionToPay.result, ethers.utils.keccak256(referenceExample), _conversionFees.result, @@ -384,18 +383,18 @@ describe('contract: BatchErc20ConversionPayments', () => { beforeEach(async () => { path = [USD_hash, DAI_address]; - initConvToPayAndRequestInfo(to, path, amountInFiat, feesAmountInFiat, 0, chainlinkPath); + initConvToPayAndConvDetail(to, path, amountInFiat, feesAmountInFiat, 0, chainlinkPath); }); const setBatchConvFunction = async (_signer: Signer) => { if (erc20Function === 'batchRouter') { batchConvFunction = testBatchConversionProxy.connect(_signer).batchRouter; - argTemplate = (requestInfos: RequestInfo[]) => { + argTemplate = (convDetails: ConversionDetail[]) => { return [ { paymentNetworkId: '0', - requestsInfo: requestInfos, - requestsInfoParent: requestsInfoParent1, + conversionDetails: convDetails, + cryptoDetails: cryptoDetails1, }, ]; }; @@ -403,8 +402,8 @@ describe('contract: BatchErc20ConversionPayments', () => { if (erc20Function === 'batchERC20ConversionPaymentsMultiTokens') { batchConvFunction = testBatchConversionProxy.connect(_signer).batchERC20ConversionPaymentsMultiTokens; - argTemplate = (requestInfos: RequestInfo[]) => { - return requestInfos; + argTemplate = (convDetails: ConversionDetail[]) => { + return convDetails; }; } }; @@ -441,26 +440,23 @@ describe('contract: BatchErc20ConversionPayments', () => { describe('batchERC20ConversionPaymentsMultiTokens with errors', () => { it('cannot transfer with invalid path', async function () { const wrongPath = [EUR_hash, ETH_hash, DAI_address]; - requestInfo.path = wrongPath; - await expect(batchConvFunction(argTemplate([requestInfo]), feeAddress)).to.be.revertedWith( + convDetail.path = wrongPath; + await expect(batchConvFunction(argTemplate([convDetail]), feeAddress)).to.be.revertedWith( 'revert No aggregator found', ); }); it('cannot transfer if max to spend too low', async function () { - requestInfo.maxToSpend = conversionToPay.result - .add(conversionFees.result) - .sub(1) - .toString(); - await expect(batchConvFunction(argTemplate([requestInfo]), feeAddress)).to.be.revertedWith( + convDetail.maxToSpend = conversionToPay.result.add(conversionFees.result).sub(1).toString(); + await expect(batchConvFunction(argTemplate([convDetail]), feeAddress)).to.be.revertedWith( 'Amount to pay is over the user limit', ); }); it('cannot transfer if rate is too old', async function () { - requestInfo.maxRateTimespan = 10; + convDetail.maxRateTimespan = 10; - await expect(batchConvFunction(argTemplate([requestInfo]), feeAddress)).to.be.revertedWith( + await expect(batchConvFunction(argTemplate([convDetail]), feeAddress)).to.be.revertedWith( 'aggregator rate is outdated', ); }); @@ -468,7 +464,7 @@ describe('contract: BatchErc20ConversionPayments', () => { it('Not enough allowance', async function () { // xSigner connect to the batch function setBatchConvFunction(xSigner); - await expect(batchConvFunction(argTemplate([requestInfo]), feeAddress)).to.be.revertedWith( + await expect(batchConvFunction(argTemplate([convDetail]), feeAddress)).to.be.revertedWith( 'Insufficient allowance for batch to pay', ); // reset: signer connect to the batch function @@ -483,7 +479,7 @@ describe('contract: BatchErc20ConversionPayments', () => { // xSigner connect to the batch function setBatchConvFunction(xSigner); - await expect(batchConvFunction(argTemplate([requestInfo]), feeAddress)).to.be.revertedWith( + await expect(batchConvFunction(argTemplate([convDetail]), feeAddress)).to.be.revertedWith( 'not enough funds, including fees', ); @@ -511,8 +507,8 @@ describe('contract: BatchErc20ConversionPayments', () => { [ { paymentNetworkId: subFunction === 'batchERC20PaymentsWithReference' ? 1 : 2, - requestsInfo: [], - requestsInfoParent: { + conversionDetails: [], + cryptoDetails: { tokenAddresses: [tokenAddress], recipients: [to], amounts: [amount], @@ -594,20 +590,20 @@ describe('contract: BatchErc20ConversionPayments', () => { let feeToPayExpected: BigNumber; const amount = BigNumber.from(100000); // usually in USD const feeAmount = amount.mul(10).div(10000); // usually in USD - let inputs: Array; + let inputs: Array; const pathUsdEth = [USD_hash, ETH_hash]; /** * @notice it modify the Eth batch inputs if needed, depending of the function used: ethFunction - * @param inputs a list of requestInfo + * @param inputs a list of convDetail */ - const getInputs = (inputs: Array) => { + const getInputs = (inputs: Array) => { if (ethFunction !== 'batchEthConversionPaymentsWithReference') { return [ { paymentNetworkId: '3', - requestsInfo: inputs, - requestsInfoParent: requestsInfoParent1, // not used + conversionDetails: inputs, + cryptoDetails: cryptoDetails1, // not used }, ]; } @@ -621,7 +617,7 @@ describe('contract: BatchErc20ConversionPayments', () => { batchConvFunction = testBatchConversionProxy.batchRouter; } - requestInfo = { + convDetail = { recipient: to, requestAmount: amount, path: pathUsdEth, @@ -637,7 +633,7 @@ describe('contract: BatchErc20ConversionPayments', () => { beforeEthBalanceTo = await provider.getBalance(to); beforeEthBalanceFee = await provider.getBalance(feeAddress); beforeEthBalance = await provider.getBalance(await signer.getAddress()); - requestInfo = { + convDetail = { recipient: to, requestAmount: amount, path: pathUsdEth, @@ -649,10 +645,10 @@ describe('contract: BatchErc20ConversionPayments', () => { // basic setup: 1 payment conversionToPay = await chainlinkPath.getConversion( - requestInfo.requestAmount, - requestInfo.path, + convDetail.requestAmount, + convDetail.path, ); - feesToPay = await chainlinkPath.getConversion(requestInfo.feeAmount, requestInfo.path); + feesToPay = await chainlinkPath.getConversion(convDetail.feeAmount, convDetail.path); amountToPayExpected = conversionToPay.result; // fees does not include batch conv fees yet @@ -694,36 +690,36 @@ describe('contract: BatchErc20ConversionPayments', () => { }); it('batchEthConversionPaymentsWithReference transfer 1 payment in ethers denominated in USD', async function () { - inputs = [requestInfo]; + inputs = [convDetail]; }); it('batchEthConversionPaymentsWithReference transfer 3 payment in ethers denominated in USD', async function () { amountToPayExpected = amountToPayExpected.mul(3); feeToPayExpected = feeToPayExpected.mul(3); - inputs = [requestInfo, requestInfo, requestInfo]; + inputs = [convDetail, convDetail, convDetail]; }); it('batchEthConversionPaymentsWithReference transfer 3 payments in ethers denominated in USD and EUR', async function () { - const EurRequestInfo = Utils.deepCopy(requestInfo); - EurRequestInfo.path = [EUR_hash, USD_hash, ETH_hash]; + const EurConvDetail = Utils.deepCopy(convDetail); + EurConvDetail.path = [EUR_hash, USD_hash, ETH_hash]; const eurConversionToPay = await chainlinkPath.getConversion( - EurRequestInfo.requestAmount, - EurRequestInfo.path, + EurConvDetail.requestAmount, + EurConvDetail.path, ); const eurFeesToPay = await chainlinkPath.getConversion( - EurRequestInfo.feeAmount, - EurRequestInfo.path, + EurConvDetail.feeAmount, + EurConvDetail.path, ); amountToPayExpected = eurConversionToPay.result.add(amountToPayExpected.mul(2)); feeToPayExpected = eurFeesToPay.result.add(feeToPayExpected.mul(2)); - inputs = [requestInfo, EurRequestInfo, requestInfo]; + inputs = [convDetail, EurConvDetail, convDetail]; }); }); it('batchEthConversionPaymentsWithReference transfer FAIL: not enough funds', async function () { await expect( - batchConvFunction(getInputs([requestInfo]), feeAddress, { + batchConvFunction(getInputs([convDetail]), feeAddress, { value: 10000, }), ).to.be.revertedWith('paymentProxy transferExactEthWithReferenceAndFee failed'); From c2d842e2d9427d82d6e9247e4076b91542e48800 Mon Sep 17 00:00:00 2001 From: olivier7delf <55892112+olivier7delf@users.noreply.github.com> Date: Tue, 9 Aug 2022 10:35:53 +0200 Subject: [PATCH 048/105] contract - clean constructor --- .../src/contracts/BatchConversionPayments.sol | 8 +------- .../smart-contracts/src/contracts/BatchPaymentsPublic.sol | 2 +- 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/packages/smart-contracts/src/contracts/BatchConversionPayments.sol b/packages/smart-contracts/src/contracts/BatchConversionPayments.sol index 95fa505e8..9b2e56539 100644 --- a/packages/smart-contracts/src/contracts/BatchConversionPayments.sol +++ b/packages/smart-contracts/src/contracts/BatchConversionPayments.sol @@ -85,20 +85,14 @@ contract BatchConversionPayments is BatchPaymentsPublic { address _paymentEthConversionFeeProxy, address _owner ) BatchPaymentsPublic(_paymentErc20Proxy, _paymentEthProxy, _owner) { - paymentErc20Proxy = IERC20FeeProxy(_paymentErc20Proxy); - paymentEthProxy = IEthereumFeeProxy(_paymentEthProxy); - paymentErc20ConversionProxy = IERC20ConversionProxy(_paymentErc20ConversionProxy); paymentEthConversionProxy = IEthConversionProxy(_paymentEthConversionFeeProxy); - transferOwnership(_owner); - - batchFee = 0; batchConversionFee = 0; } /** * @notice Batch payments on different payment networks at once. - * @param metaDetails contains paymentNetworkId and conversionDetails + * @param metaDetails contains paymentNetworkId, conversionDetails, and cryptoDetails * - batchERC20ConversionPaymentsMultiTokens, paymentNetworkId=0 * - batchERC20PaymentsWithReference, paymentNetworkId=1 * - batchERC20PaymentsMultiTokensWithReference, paymentNetworkId=2 diff --git a/packages/smart-contracts/src/contracts/BatchPaymentsPublic.sol b/packages/smart-contracts/src/contracts/BatchPaymentsPublic.sol index 1ac74deaf..82e8a6a73 100644 --- a/packages/smart-contracts/src/contracts/BatchPaymentsPublic.sol +++ b/packages/smart-contracts/src/contracts/BatchPaymentsPublic.sol @@ -16,7 +16,7 @@ import './interfaces/EthereumFeeProxy.sol'; * An additional batch fee is paid to the same address. * If one transaction of the batch fail, every transactions are reverted. * @dev It is a clone of BatchPayment.sol, with three main modifications: - * - function "receive" is not implemented + * - function "receive" has one other condition: payerAuthorized * - fees are now divided by 10_000 instead of 1_000 in previous version * - batch payment functions are now public, instead of external */ From 73fa7a73851f73eb4190b44bca403c753efbcbcf Mon Sep 17 00:00:00 2001 From: olivier7delf <55892112+olivier7delf@users.noreply.github.com> Date: Tue, 9 Aug 2022 10:51:49 +0200 Subject: [PATCH 049/105] wording receive comments add batchEthPaymentsWithReference tests --- .../src/contracts/BatchPaymentsPublic.sol | 2 +- .../contracts/BatchConversionPayments.test.ts | 129 +++++++++++++----- 2 files changed, 93 insertions(+), 38 deletions(-) diff --git a/packages/smart-contracts/src/contracts/BatchPaymentsPublic.sol b/packages/smart-contracts/src/contracts/BatchPaymentsPublic.sol index 82e8a6a73..585a17ffd 100644 --- a/packages/smart-contracts/src/contracts/BatchPaymentsPublic.sol +++ b/packages/smart-contracts/src/contracts/BatchPaymentsPublic.sol @@ -57,7 +57,7 @@ contract BatchPaymentsPublic is Ownable { /** * This contract is non-payable. Making an ETH payment with conversion requires the contract to accept incoming ETH. - * See the end of `batchRouter` where the leftover is given back to the transaction sender. + * See the end of `paymentEthConversionProxy.transferWithReferenceAndFee` where the leftover is given back. */ receive() external payable { require(payerAuthorized || msg.value == 0, 'Non-payable'); diff --git a/packages/smart-contracts/test/contracts/BatchConversionPayments.test.ts b/packages/smart-contracts/test/contracts/BatchConversionPayments.test.ts index 4c2d255b5..fccac7480 100644 --- a/packages/smart-contracts/test/contracts/BatchConversionPayments.test.ts +++ b/packages/smart-contracts/test/contracts/BatchConversionPayments.test.ts @@ -148,34 +148,6 @@ describe('contract: BatchErc20ConversionPayments', () => { batchAddress = testBatchConversionProxy.address; }); - const batchFeeToPay = (conversionAmountToPay: BigNumber) => { - return conversionAmountToPay.mul(batchConvFee).div(10000); - }; - - /** - * @notice it gets the conversions including fees to be paid, and it set the convDetail - */ - const initConvToPayAndConvDetail = async ( - _recipient: string, - _path: string[], - _requestAmount: string, - _feeAmount: string, - _maxRateTimespan: number, - _chainlinkPath: ChainlinkConversionPath, - ) => { - conversionToPay = await _chainlinkPath.getConversion(_requestAmount, _path); - conversionFees = await _chainlinkPath.getConversion(_feeAmount, _path); - convDetail = { - recipient: _recipient, - requestAmount: _requestAmount, - path: _path, - paymentReference: referenceExample, - feeAmount: _feeAmount, - maxToSpend: conversionToPay.result.add(conversionFees.result).toString(), - maxRateTimespan: _maxRateTimespan, - }; - }; - beforeEach(async () => { fromDiffBalanceExpected = BigNumber.from(0); toDiffBalanceExpected = BigNumber.from(0); @@ -220,6 +192,34 @@ describe('contract: BatchErc20ConversionPayments', () => { expect(batchDiffBalance).to.equals('0', 'batchDiffBalance'); }); + const batchFeeToPay = (conversionAmountToPay: BigNumber) => { + return conversionAmountToPay.mul(batchConvFee).div(10000); + }; + + /** + * @notice it gets the conversions including fees to be paid, and it set the convDetail + */ + const initConvToPayAndConvDetail = async ( + _recipient: string, + _path: string[], + _requestAmount: string, + _feeAmount: string, + _maxRateTimespan: number, + _chainlinkPath: ChainlinkConversionPath, + ) => { + conversionToPay = await _chainlinkPath.getConversion(_requestAmount, _path); + conversionFees = await _chainlinkPath.getConversion(_feeAmount, _path); + convDetail = { + recipient: _recipient, + requestAmount: _requestAmount, + path: _path, + paymentReference: referenceExample, + feeAmount: _feeAmount, + maxToSpend: conversionToPay.result.add(conversionFees.result).toString(), + maxRateTimespan: _maxRateTimespan, + }; + }; + /** * @notice Used to calcul the expected new ERC20 balance for batch conversion. * It can also be used for batch IF batchFee == batchConvFee @@ -261,7 +261,7 @@ describe('contract: BatchErc20ConversionPayments', () => { /** * @notice update convDetail, do an ERC20 conv batch payment and calcul the balances - * @param path to update the resquestInfo + * @param path to update the convDetail */ const transferOneTokenConv = async (path: string[]) => { await initConvToPayAndConvDetail(to, path, amountInFiat, feesAmountInFiat, 0, chainlinkPath); @@ -287,7 +287,7 @@ describe('contract: BatchErc20ConversionPayments', () => { /** * @notice generate nTimes 2 convDetails, do an ERC20 conv batch payment with theses 2*nTimes requests * and calcul the balances - * @param path2 to update the second resquestInfo + * @param path2 to update the second convDetail */ const transferTokensConv = async (path2: string[], nTimes: number) => { const coef = 2; @@ -312,9 +312,7 @@ describe('contract: BatchErc20ConversionPayments', () => { conversionsToPay = conversionsToPay.concat([conversionToPay, conversionToPay2]); conversionsFees = conversionsFees.concat([conversionFees, conversionFees2]); } - const result = batchConvFunction(argTemplate(convDetails), feeAddress); - const tx = await result; - await tx.wait(1); + const tx = await batchConvFunction(argTemplate(convDetails), feeAddress); if (logGas) { const receipt = await tx.wait(); console.log(`${2 * nTimes} req, gas consumption: `, receipt.gasUsed.toString()); @@ -597,7 +595,7 @@ describe('contract: BatchErc20ConversionPayments', () => { * @notice it modify the Eth batch inputs if needed, depending of the function used: ethFunction * @param inputs a list of convDetail */ - const getInputs = (inputs: Array) => { + const getEthInputs = (inputs: Array) => { if (ethFunction !== 'batchEthConversionPaymentsWithReference') { return [ { @@ -613,7 +611,7 @@ describe('contract: BatchErc20ConversionPayments', () => { before(() => { if (ethFunction === 'batchEthConversionPaymentsWithReference') { batchConvFunction = testBatchConversionProxy.batchEthConversionPaymentsWithReference; - } else { + } else if (ethFunction === 'batchRouter') { batchConvFunction = testBatchConversionProxy.batchRouter; } @@ -656,7 +654,7 @@ describe('contract: BatchErc20ConversionPayments', () => { }); afterEach(async () => { - tx = await batchConvFunction(getInputs(inputs), feeAddress, { + tx = await batchConvFunction(getEthInputs(inputs), feeAddress, { value: BigNumber.from('100000000000000000'), }); const receipt = await tx.wait(); @@ -719,11 +717,68 @@ describe('contract: BatchErc20ConversionPayments', () => { }); it('batchEthConversionPaymentsWithReference transfer FAIL: not enough funds', async function () { await expect( - batchConvFunction(getInputs([convDetail]), feeAddress, { + batchConvFunction(getEthInputs([convDetail]), feeAddress, { value: 10000, }), ).to.be.revertedWith('paymentProxy transferExactEthWithReferenceAndFee failed'); }); + + describe(`Eth BatchPaymentPublic functions: ${ + ethFunction === 'batchRouter' ?? '' + } batchEthPaymentsWithReference`, () => { + it('transfer 1 payment', async function () { + beforeEthBalanceTo = await provider.getBalance(to); + beforeEthBalanceFee = await provider.getBalance(feeAddress); + beforeEthBalance = await provider.getBalance(await signer.getAddress()); + + const cryptoDetails = { + tokenAddresses: [], + recipients: [to], + amounts: [amount], + paymentReferences: [referenceExample], + feeAmounts: [feeAmount], + }; + if (ethFunction === 'batchRouter') { + await testBatchConversionProxy.batchRouter( + [ + { + paymentNetworkId: 4, + conversionDetails: [convDetail], // not used + cryptoDetails: cryptoDetails, + }, + ], + feeAddress, + { value: 1000000000 }, + ); + } else if (ethFunction === 'batchEthConversionPaymentsWithReference') { + await testBatchConversionProxy.batchEthPaymentsWithReference( + cryptoDetails.recipients, + cryptoDetails.amounts, + cryptoDetails.paymentReferences, + cryptoDetails.feeAmounts, + feeAddress, + { value: 1000000000 }, + ); + } + + amountToPayExpected = amount; + feeToPayExpected = feeAmount; + const afterEthBalanceTo = await provider.getBalance(to); + const afterEthBalanceFee = await provider.getBalance(feeAddress); + const proxyBalance = await provider.getBalance(testBatchConversionProxy.address); + const _diffBalanceTo = afterEthBalanceTo.sub(beforeEthBalanceTo); + const _diffBalanceFee = afterEthBalanceFee.sub(beforeEthBalanceFee); + + expect(_diffBalanceTo).to.equals(amountToPayExpected.toString(), 'diffBalanceTo'); + + feeToPayExpected = amountToPayExpected.mul(batchFee).div(10000).add(feeToPayExpected); + expect(_diffBalanceFee.toString()).to.equals( + feeToPayExpected.toString(), + 'diffBalanceFee', + ); + expect(proxyBalance).to.equals('0', 'proxyBalance'); + }); + }); }); }; From 04a2d9273b9e55c8ab082dfb3bd2bc8dc03dba97 Mon Sep 17 00:00:00 2001 From: olivier7delf <55892112+olivier7delf@users.noreply.github.com> Date: Tue, 9 Aug 2022 13:38:33 +0200 Subject: [PATCH 050/105] clean packages --- packages/smart-contracts/package.json | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/packages/smart-contracts/package.json b/packages/smart-contracts/package.json index 73e9b1343..6f1cfd6e4 100644 --- a/packages/smart-contracts/package.json +++ b/packages/smart-contracts/package.json @@ -35,8 +35,6 @@ "build:lib": "tsc -b tsconfig.build.json && cp src/types/*.d.ts dist/src/types && cp -r dist/src/types types", "build:sol": "yarn hardhat compile", "build": "yarn build:sol && yarn build:lib", - "build:copy": "cp build/src/contracts/BatchConversionPayments.sol/BatchConversionPayments.json src/lib/artifacts/BatchConversionPayments", - "cbuildcopy": "yarn clean && yarn build && yarn build:copy", "clean:types": "shx rm -rf types && shx rm -rf src/types", "clean:lib": "shx rm -rf dist tsconfig.tsbuildinfo tsconfig.build.tsbuildinfo", "clean:hardhat": "shx rm -rf cache && shx rm -rf build", @@ -50,10 +48,7 @@ "ganache": "ganache-cli -l 90000000 -p 8545 -m \"candy maple cake sugar pudding cream honey rich smooth crumble sweet treat\"", "deploy": "yarn hardhat deploy-local-env --network private", "test": "yarn hardhat test --network private", - "test:lib": "yarn jest test/lib", - "testp": "yarn test test/contracts/BatchConversionPayments.test.ts", - "testcp": "yarn hardhat compile && yarn testp", - "redeploy": "yarn clean && yarn build && yarn deploy" + "test:lib": "yarn jest test/lib" }, "dependencies": { "tslib": "2.3.1" From 5e86486b1c76d4afd051dd72ecf15d6aa378ae10 Mon Sep 17 00:00:00 2001 From: olivier7delf <55892112+olivier7delf@users.noreply.github.com> Date: Wed, 10 Aug 2022 09:27:26 +0200 Subject: [PATCH 051/105] wip --- .../src/payment/any-to-erc20-proxy.ts | 61 ++++- .../src/payment/batch-proxy-conv.ts | 226 +++++++++++++++--- 2 files changed, 241 insertions(+), 46 deletions(-) diff --git a/packages/payment-processor/src/payment/any-to-erc20-proxy.ts b/packages/payment-processor/src/payment/any-to-erc20-proxy.ts index 3c2b65665..6fbcb32d0 100644 --- a/packages/payment-processor/src/payment/any-to-erc20-proxy.ts +++ b/packages/payment-processor/src/payment/any-to-erc20-proxy.ts @@ -61,6 +61,43 @@ export function encodePayAnyToErc20ProxyRequest( amount?: BigNumberish, feeAmountOverride?: BigNumberish, ): string { + const { + path, + paymentReference, + paymentAddress, + feeAddress, + maxRateTimespan, + amountToPay, + feeToPay, + } = prepAnyToErc20ProxyRequest(request, paymentSettings, amount, feeAmountOverride); + + const proxyContract = Erc20ConversionProxy__factory.createInterface(); + return proxyContract.encodeFunctionData('transferFromWithReferenceAndFee', [ + paymentAddress, + amountToPay, + path, + `0x${paymentReference}`, + feeToPay, + feeAddress || constants.AddressZero, + BigNumber.from(paymentSettings.maxToSpend), + maxRateTimespan || 0, + ]); +} + +export function prepAnyToErc20ProxyRequest( + request: ClientTypes.IRequestData, + paymentSettings: IConversionPaymentSettings, + amount?: BigNumberish, + feeAmountOverride?: BigNumberish, +): { + path: string[]; + paymentReference: string; + paymentAddress: string; + feeAddress: string | undefined; + maxRateTimespan: string | undefined; + amountToPay: BigNumber; + feeToPay: BigNumber; +} { if (!paymentSettings.currency) { throw new Error('currency must be provided in the paymentSettings'); } @@ -96,23 +133,25 @@ export function encodePayAnyToErc20ProxyRequest( // Check request validateConversionFeeProxyRequest(request, path, amount, feeAmountOverride); - const { paymentReference, paymentAddress, feeAddress, feeAmount, maxRateTimespan } = - getRequestPaymentValues(request); + const { + paymentReference, + paymentAddress, + feeAddress, + feeAmount, + maxRateTimespan, + } = getRequestPaymentValues(request); const amountToPay = padAmountForChainlink(getAmountToPay(request, amount), requestCurrency); const feeToPay = padAmountForChainlink(feeAmountOverride || feeAmount || 0, requestCurrency); - - const proxyContract = Erc20ConversionProxy__factory.createInterface(); - return proxyContract.encodeFunctionData('transferFromWithReferenceAndFee', [ + return { + path, + paymentReference, paymentAddress, + feeAddress, + maxRateTimespan, amountToPay, - path, - `0x${paymentReference}`, feeToPay, - feeAddress || constants.AddressZero, - BigNumber.from(paymentSettings.maxToSpend), - maxRateTimespan || 0, - ]); + }; } export function prepareAnyToErc20ProxyPaymentTransaction( diff --git a/packages/payment-processor/src/payment/batch-proxy-conv.ts b/packages/payment-processor/src/payment/batch-proxy-conv.ts index 29b36582e..a0c794be1 100644 --- a/packages/payment-processor/src/payment/batch-proxy-conv.ts +++ b/packages/payment-processor/src/payment/batch-proxy-conv.ts @@ -1,6 +1,6 @@ import { ContractTransaction, Signer, providers, constants, BigNumber, BigNumberish } from 'ethers'; import { batchPaymentsArtifact } from '@requestnetwork/smart-contracts'; -import { BatchPayments__factory } from '@requestnetwork/smart-contracts/types'; +import { BatchConversionPayments__factory, BatchPayments__factory } from '@requestnetwork/smart-contracts/types'; import { ClientTypes, PaymentTypes } from '@requestnetwork/types'; import { ITransactionOverrides } from './transaction-overrides'; import { @@ -16,6 +16,61 @@ import { validateEthFeeProxyRequest } from './eth-fee-proxy'; import { IPreparedTransaction } from './prepared-transaction'; import { checkErc20Allowance, encodeApproveAnyErc20 } from './erc20'; import { IConversionPaymentSettings } from './index'; +import { prepAnyToErc20ProxyRequest } from './any-to-erc20-proxy'; + +// used by batch smart contract +export type ConversionDetail = { + recipient: string; + requestAmount: BigNumberish; + path: string[]; + paymentReference: string; + feeAmount: BigNumberish; + maxToSpend: BigNumberish; + maxRateTimespan: BigNumberish; +}; + +export type CryptoDetails = { + tokenAddresses: Array; + recipients: Array; + amounts: Array; + paymentReferences: Array; + feeAmounts: Array; +}; + +const emptyConversionDetail = { + recipient: '', + requestAmount: 0, + path: [], + paymentReference: '', + feeAmount: 0, + maxToSpend: 0, + maxRateTimespan: 0, +}; + +const emptyCryptoDetails = { + tokenAddresses: [], + recipients: [], + amounts: [], + paymentReferences: [], + feeAmounts: [], +}; + +export type MetaDetail = { + paymentNetworkId: number; + conversionDetails: ConversionDetail[]; + cryptoDetails: CryptoDetails; +}; + +// used only for batch payment processor +type EnrichedRequest = { + paymentNetworkId: number; // ref in batchConversionPayment.sol + requests: ClientTypes.IRequestData[]; + paymentSettings?: IConversionPaymentSettings[]; + amount?: BigNumberish[]; + feeAmount?: BigNumberish[]; + version?: string; + batchFee?: number; +}; /** TODO UPDATE * ERC20 Batch Proxy payment details: @@ -31,29 +86,19 @@ import { IConversionPaymentSettings } from './index'; * but only call ethFeeProxy. It can impact payment detection */ -type MetaRequest = { - paymentNetworkId: number; // ref in batchConversionPayment.sol - requests: ClientTypes.IRequestData[]; - paymentSettings?: IConversionPaymentSettings[]; - amount?: BigNumberish[]; - feeAmount?: BigNumberish[]; - version?: string; - batchFee?: number; -}; - /** * Processes a transaction to pay a batch of requests with an ERC20 or ETH currency that is different from the request currency (eg. fiat). * The payment is made by the ERC20, or ETH fee proxy contract. - * @param metaRequests List of MetaRequest + * @param enrichedRequests List of EnrichedRequest * @param signerOrProvider the Web3 provider, or signer. Defaults to window.ethereum. * @param overrides optionally, override default transaction values, like gas. */ export async function payBatchConvProxyRequest( - metaRequests: MetaRequest[], + enrichedRequests: EnrichedRequest[], signerOrProvider: providers.Provider | Signer = getProvider(), overrides?: ITransactionOverrides, ): Promise { - const { data, to, value } = prepareBatchConvPaymentTransaction(metaRequests); + const { data, to, value } = prepareBatchConvPaymentTransaction(enrichedRequests); const signer = getSigner(signerOrProvider); return signer.sendTransaction({ data, to, value, ...overrides }); } @@ -63,36 +108,63 @@ export async function payBatchConvProxyRequest( * Requests paymentType must be "ETH" or "ERC20" */ export function prepareBatchConvPaymentTransaction( - metaRequests: MetaRequest[], + enrichedRequests: EnrichedRequest[], ): IPreparedTransaction { - // we only implement batchRouter + // we only implement batchRouter and the ERC20 normal and conversion functions + // batchERC20ConversionPaymentsMultiTokens, and batchERC20PaymentsMultiTokensWithReference + // => paymentNetworkId take only theses values: 0 or 2 // later, to do gas optimizaton, we will implement the others batch functions, - // at this moment, paymentNetworkId will be useful - - const metaRequest = metaRequests[0]; - for (let i = 0; i < metaRequests.length; i++) { - if (metaRequests[i].paymentNetworkId === 0) { - } else if (metaRequests[i].paymentNetworkId === 1) { - } else if (metaRequests[i].paymentNetworkId === 2) { - } else if (metaRequests[i].paymentNetworkId === 3) { - } else if (metaRequests[i].paymentNetworkId === 4) { + // revoir cette partie, il faut créer deux obj dict, un avec paymentNetworkId 0 (puis 2) ayant toutes les requests, feeAddress + let clusteredEnrichedRequest = [] + for (let i = 0; i < enrichedRequests.length; i++) { + if (enrichedRequests[i].paymentNetworkId === 0) { + clusteredEnrichedRequest.push({paymentNetworkId: 0, request: enrichedRequests[i], feeAddress: enrichedRequests[i].}) + } else if (enrichedRequests[i].paymentNetworkId === 2) { + clusteredEnrichedRequest.push({paymentNetworkId: 0, request: enrichedRequests[i]}) + } + } + // create MetaDetails input + const metaDetails = []; + let ERC20ConversionInput: ConversionDetail[]; + let ERC20Input: CryptoDetails; + for (let i = 0; i < enrichedRequests.length; i++) { + const enrichedRequest = enrichedRequests[i]; + if (enrichedRequest.paymentNetworkId === 0) { + ERC20ConversionInput = batchERC20ConversionPaymentsMultiTokensInput(enrichedRequest); + metaDetails.push({ + paymentNetworkId: 0, + conversionDetails: ERC20ConversionInput, + cryptoDetails: emptyCryptoDetails, + }); + } else if (enrichedRequests[i].paymentNetworkId === 2) { + ERC20Input = batchERC20PaymentsMultiTokensWithReferenceInput(enrichedRequest); + metaDetails.push({ + paymentNetworkId: 2, + conversionDetails: [emptyConversionDetail], + cryptoDetails: ERC20Input, + }); + } else { + throw new Error('paymentNetworkId must be equal to 0 or 2'); } } + console.log('metaDetails', metaDetails); + metaDetails; + const encodedTx = encodePayBatchRequest(requests); const proxyAddress = getBatchProxyAddress(requests[0], version); - let totalAmount = 0; + // let totalAmount = 0; - if (requests[0].currencyInfo.type === 'ETH') { - const { amountsToPay, feesToPay } = getBatchArgs(requests); + // if (requests[0].currencyInfo.type === 'ETH') { + // const { amountsToPay, feesToPay } = getBatchArgs(requests); - const amountToPay = amountsToPay.reduce((sum, current) => sum.add(current), BigNumber.from(0)); - const batchFeeToPay = BigNumber.from(amountToPay).mul(batchFee).div(1000); - const feeToPay = feesToPay.reduce( - (sum, current) => sum.add(current), - BigNumber.from(batchFeeToPay), - ); - totalAmount = amountToPay.add(feeToPay).toNumber(); + // const amountToPay = amountsToPay.reduce((sum, current) => sum.add(current), BigNumber.from(0)); + // const batchFeeToPay = BigNumber.from(amountToPay).mul(batchFee).div(1000); + // const feeToPay = feesToPay.reduce( + // (sum, current) => sum.add(current), + // BigNumber.from(batchFeeToPay), + // ); + // totalAmount = amountToPay.add(feeToPay).toNumber(); } return { @@ -102,6 +174,90 @@ export function prepareBatchConvPaymentTransaction( }; } +function batchERC20ConversionPaymentsMultiTokensInput( + enrichedRequest: EnrichedRequest, +): ConversionDetail[] { + // encodePayAnyToErc20ProxyRequest look what they check + for (let i = 0; i < enrichedRequest.requests.length; i++) { + const { + path, + paymentReference, + paymentAddress, + feeAddress, + maxRateTimespan, + amountToPay, + feeToPay, + } = prepAnyToErc20ProxyRequest(request, enrichedRequest.paymentSettings, enrichedRequest.amount, enrichedRequest.feeAmountOverride); + + } + + return []; +} + +function batchERC20PaymentsMultiTokensWithReferenceInput(enrichedRequest: EnrichedRequest): CryptoDetails { + return { + tokenAddresses: [], + recipients: [], + amounts: [], + paymentReferences: [], + feeAmounts: [], + }; +} + +function samePaymentNetwork(requests: ClientTypes.IRequestData[]): void { + const pn = getPaymentNetworkExtension(requests[0]); + for (let i = 0; i < requests.length; i++) { + validateErc20FeeProxyRequest(requests[i]); + if (!comparePnTypeAndVersion(pn, requests[i])) { + throw new Error(`Every payment network type and version must be identical`); + } + } +} + +/** + * Encodes the call to pay a batch of requests through the ERC20Bacth or ETHBatch proxy contract, + * can be used with a Multisig contract. + * @param requests list of ECR20 requests to pay + * @dev pn version of the requests is checked to avoid paying with two differents proxies (e.g: erc20proxy v1 and v2) + */ + export function encodePayBatchConversion(metaDetails: MetaDetail[]): string { + + const proxyContract = BatchConversionPayments__factory.createInterface(); + + + + + if (isMultiTokens) { + return proxyContract.encodeFunctionData('batchERC20PaymentsMultiTokensWithReference', [ + tokenAddresses, + paymentAddresses, + amountsToPay, + paymentReferences, + feesToPay, + feeAddressUsed, + ]); + } else { + return proxyContract.encodeFunctionData('batchERC20PaymentsWithReference', [ + tokenAddresses[0], + paymentAddresses, + amountsToPay, + paymentReferences, + feesToPay, + feeAddressUsed, + ]); + } + } else { + tokenAddresses; + return proxyContract.encodeFunctionData('batchEthPaymentsWithReference', [ + paymentAddresses, + amountsToPay, + paymentReferences, + feesToPay, + feeAddressUsed, + ]); + } +} + /** * Encodes the call to pay a batch of requests through the ERC20Bacth or ETHBatch proxy contract, * can be used with a Multisig contract. From 1a55c1ece2d812a8c020498fbc3563f2ded48cfd Mon Sep 17 00:00:00 2001 From: olivier7delf <55892112+olivier7delf@users.noreply.github.com> Date: Wed, 10 Aug 2022 15:54:20 +0200 Subject: [PATCH 052/105] refacto batch conversion ERC20 tests --- .../ERC20BatchConversionPayments.test.ts | 611 ++++++++++++++++++ 1 file changed, 611 insertions(+) create mode 100644 packages/smart-contracts/test/contracts/ERC20BatchConversionPayments.test.ts diff --git a/packages/smart-contracts/test/contracts/ERC20BatchConversionPayments.test.ts b/packages/smart-contracts/test/contracts/ERC20BatchConversionPayments.test.ts new file mode 100644 index 000000000..54dc1e634 --- /dev/null +++ b/packages/smart-contracts/test/contracts/ERC20BatchConversionPayments.test.ts @@ -0,0 +1,611 @@ +import { ethers, network } from 'hardhat'; +import { + ERC20FeeProxy__factory, + Erc20ConversionProxy__factory, + EthConversionProxy__factory, + EthereumFeeProxy__factory, + ERC20FeeProxy, + EthereumFeeProxy, + ChainlinkConversionPath, + TestERC20, + Erc20ConversionProxy, + EthConversionProxy, + TestERC20__factory, + BatchConversionPayments, +} from '../../src/types'; +import { BigNumber, BigNumberish, BytesLike, ContractTransaction, Signer } from 'ethers'; +import { expect } from 'chai'; +import { CurrencyManager } from '@requestnetwork/currency'; +import { chainlinkConversionPath, batchConversionPaymentsArtifact } from '../../src/lib'; +import { localERC20AlphaArtifact, secondLocalERC20AlphaArtifact } from './localArtifacts'; +import Utils from '@requestnetwork/utils'; + +// set to true to log batch payments's gas consumption +const logGas = false; + +describe('contract: BatchErc20ConversionPayments', () => { + let from: string; + let to: string; + let feeAddress: string; + let batchAddress: string; + let signer: Signer; + let xSigner: Signer; + + // variables used to set up batch conversion proxy, and also requests payment + const batchFee = 50; + const batchConvFee = 100; + const amountInFiat = '100000000'; // 1 with 8 decimal + const feesAmountInFiat = '100000'; // 0.001 with 8 decimal + const thousandWith18Decimal = '1000000000000000000000'; + const referenceExample = '0xaaaa'; + + // variables to set up proxies and paths + 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 DAI_address: string; + let FAU_address: string; + + let testErc20ConversionProxy: Erc20ConversionProxy; + let testEthConversionProxy: EthConversionProxy; + let testBatchConversionProxy: BatchConversionPayments; + let testERC20: TestERC20; + let testERC20b: TestERC20; + let erc20FeeProxy: ERC20FeeProxy; + let ethereumFeeProxy: EthereumFeeProxy; + let chainlinkPath: ChainlinkConversionPath; + + // variables used to check testERC20 balances + let fromOldBalance: BigNumber; + let toOldBalance: BigNumber; + let feeOldBalance: BigNumber; + + let fromDiffBalanceExpected: BigNumber; + let toDiffBalanceExpected: BigNumber; + let feeDiffBalanceExpected: BigNumber; + + // variables needed for chainlink and + let path: string[]; + type ConvToPay = [BigNumber, BigNumber] & { + result: BigNumber; + oldestRateTimestamp: BigNumber; + }; + let conversionToPay: ConvToPay; + let conversionFees: ConvToPay; + + // type required by Erc20 conversion batch function input + type ConversionDetail = { + recipient: string; + requestAmount: BigNumberish; + path: string[]; + paymentReference: BytesLike; + feeAmount: BigNumberish; + maxToSpend: BigNumberish; + maxRateTimespan: BigNumberish; + }; + let convDetail: ConversionDetail; + + /** + * @notice Function batch conversion, it can be the batchRouter function, + * used with conversion args, or directly batchERC20ConversionPaymentsMultiTokens + */ + let batchConvFunction: ( + args: any, + feeAddress: string, + optional?: any, + ) => Promise; + + /** Format arguments so they can be used by batchConvFunction */ + let argTemplate: Function; + + before(async () => { + [from, to, feeAddress] = (await ethers.getSigners()).map((s) => s.address); + [signer, xSigner, xSigner, xSigner] = await ethers.getSigners(); + chainlinkPath = chainlinkConversionPath.connect(network.name, signer); + erc20FeeProxy = await new ERC20FeeProxy__factory(signer).deploy(); + ethereumFeeProxy = await new EthereumFeeProxy__factory(signer).deploy(); + testErc20ConversionProxy = await new Erc20ConversionProxy__factory(signer).deploy( + erc20FeeProxy.address, + chainlinkPath.address, + await signer.getAddress(), + ); + testEthConversionProxy = await new EthConversionProxy__factory(signer).deploy( + ethereumFeeProxy.address, + chainlinkPath.address, + ETH_hash, + ); + testBatchConversionProxy = batchConversionPaymentsArtifact.connect(network.name, signer); + + // update batch payment proxies, and batch fees + await testBatchConversionProxy.setPaymentErc20Proxy(erc20FeeProxy.address); + await testBatchConversionProxy.setPaymentEthProxy(ethereumFeeProxy.address); + await testBatchConversionProxy.setPaymentErc20ConversionProxy(testErc20ConversionProxy.address); + await testBatchConversionProxy.setPaymentEthConversionProxy(testEthConversionProxy.address); + + await testBatchConversionProxy.setBatchFee(batchFee); + await testBatchConversionProxy.setBatchConversionFee(batchConvFee); + + // set ERC20 tokens + DAI_address = localERC20AlphaArtifact.getAddress(network.name); + testERC20 = new TestERC20__factory(signer).attach(DAI_address); + + FAU_address = secondLocalERC20AlphaArtifact.getAddress(network.name); + testERC20b = new TestERC20__factory(signer).attach(FAU_address); + batchAddress = testBatchConversionProxy.address; + }); + + beforeEach(async () => { + fromDiffBalanceExpected = BigNumber.from(0); + toDiffBalanceExpected = BigNumber.from(0); + feeDiffBalanceExpected = BigNumber.from(0); + await testERC20.approve(testBatchConversionProxy.address, thousandWith18Decimal, { + from, + }); + await testERC20b.approve(testBatchConversionProxy.address, thousandWith18Decimal, { + from, + }); + // get balances of testERC20 token + fromOldBalance = await testERC20.balanceOf(from); + toOldBalance = await testERC20.balanceOf(to); + feeOldBalance = await testERC20.balanceOf(feeAddress); + + // create a default convDetail + path = [USD_hash, DAI_address]; + getConvToPayAndConvDetail(to, path, amountInFiat, feesAmountInFiat, 0, chainlinkPath); + }); + + afterEach(async () => { + // check balances of testERC20 token + checkBalancesForOneToken( + testERC20, + fromOldBalance, + toOldBalance, + feeOldBalance, + fromDiffBalanceExpected, + toDiffBalanceExpected, + feeDiffBalanceExpected, + ); + }); + + /** + * @notice it gets the conversions including fees to be paid, and it set the convDetail input + */ + const getConvToPayAndConvDetail = async ( + _recipient: string, + _path: string[], + _requestAmount: string, + _feeAmount: string, + _maxRateTimespan: number, + _chainlinkPath: ChainlinkConversionPath, + ) => { + conversionToPay = await _chainlinkPath.getConversion(_requestAmount, _path); + conversionFees = await _chainlinkPath.getConversion(_feeAmount, _path); + convDetail = { + recipient: _recipient, + requestAmount: _requestAmount, + path: _path, + paymentReference: referenceExample, + feeAmount: _feeAmount, + maxToSpend: conversionToPay.result.add(conversionFees.result).toString(), + maxRateTimespan: _maxRateTimespan, + }; + }; + + /** + * check testERC20 balances of: the payer (from), the recipient (to), the feeAddress, and the batch contract + */ + const checkBalancesForOneToken = async ( + _testERC20: TestERC20, + _fromOldBalance: BigNumber, + _toOldBalance: BigNumber, + _feeOldBalance: BigNumber, + _fromDiffBalanceExpected: BigNumber, + _toDiffBalanceExpected: BigNumber, + _feeDiffBalanceExpected: BigNumber, + ) => { + // Get balances + const fromBalance = await _testERC20.balanceOf(from); + const toBalance = await _testERC20.balanceOf(to); + const feeBalance = await _testERC20.balanceOf(feeAddress); + const batchBalance = await _testERC20.balanceOf(batchAddress); + + // Calculate the difference of the balance : now - before + const fromDiffBalance = BigNumber.from(fromBalance).sub(_fromOldBalance); + const toDiffBalance = BigNumber.from(toBalance).sub(_toOldBalance); + const feeDiffBalance = BigNumber.from(feeBalance).sub(_feeOldBalance); + // Check balance changes + expect(fromDiffBalance).to.equals(_fromDiffBalanceExpected, 'fromDiffBalance'); + expect(toDiffBalance).to.equals(_toDiffBalanceExpected, 'toDiffBalance'); + expect(feeDiffBalance).to.equals(_feeDiffBalanceExpected, 'feeDiffBalance'); + expect(batchBalance).to.equals('0', 'batchBalance'); + }; + + /** + * @notice Used to calculate the expected new ERC20 balance of a single token for batch conversion. + * @dev fees are not exactly calculated with the same formula, depending if it is with conversion or not + */ + const expectedERC20Balances = ( + _conversionToPay_results: BigNumber[], + _conversionFees_results: BigNumber[], + appliedFees: number, + withConversion = true, + ) => { + let _fromDiffBalanceExpected = _conversionToPay_results.reduce( + (prev, x) => prev.sub(x), + BigNumber.from(0), + ); + let _toDiffBalanceExpected = _fromDiffBalanceExpected.mul(-1); + let _feeDiffBalanceExpected = _conversionFees_results.reduce( + (prev, x) => prev.add(x), + BigNumber.from(0), + ); + + _feeDiffBalanceExpected = withConversion + ? _toDiffBalanceExpected + .add(_feeDiffBalanceExpected) + .mul(appliedFees) + .div(10000) + .add(_feeDiffBalanceExpected) + : _toDiffBalanceExpected.mul(appliedFees).div(10000).add(_feeDiffBalanceExpected); + + _fromDiffBalanceExpected = _fromDiffBalanceExpected.sub(_feeDiffBalanceExpected); + return [_fromDiffBalanceExpected, _toDiffBalanceExpected, _feeDiffBalanceExpected]; + }; + + /** + * It sets the right batch conversion function, with the associated arguments format + * @param isBatchRouter allows to use batchERC20ConversionPaymentsMultiTokens with batchRouter + * @param _signer + */ + const setBatchConvFunction = async (isBatchRouter: boolean, _signer: Signer) => { + if (isBatchRouter) { + batchConvFunction = testBatchConversionProxy.connect(_signer).batchRouter; + argTemplate = (convDetails: ConversionDetail[]) => { + return [ + { + paymentNetworkId: '0', + conversionDetails: convDetails, + cryptoDetails: { + tokenAddresses: [], + recipients: [], + amounts: [], + paymentReferences: [], + feeAmounts: [], + }, + }, + ]; + }; + } else { + batchConvFunction = + testBatchConversionProxy.connect(_signer).batchERC20ConversionPaymentsMultiTokens; + argTemplate = (convDetails: ConversionDetail[]) => { + return convDetails; + }; + } + }; + + /** + * Function used to check the events emitted from the batch conversion proxy. + * @dev referenceExample and feeAddress are not args because there values never change + */ + const emitOneTx = ( + result: Chai.Assertion, + _convDetail: ConversionDetail, + _conversionToPay = conversionToPay, + _conversionFees = conversionFees, + _testErc20ConversionProxy = testErc20ConversionProxy, + ) => { + return result.to + .emit(_testErc20ConversionProxy, 'TransferWithConversionAndReference') + .withArgs( + _convDetail.requestAmount, + ethers.utils.getAddress(_convDetail.path[0]), + ethers.utils.keccak256(referenceExample), + _convDetail.feeAmount, + '0', + ) + .to.emit(_testErc20ConversionProxy, 'TransferWithReferenceAndFee') + .withArgs( + ethers.utils.getAddress(DAI_address), + ethers.utils.getAddress(_convDetail.recipient), + _conversionToPay.result, + ethers.utils.keccak256(referenceExample), + _conversionFees.result, + feeAddress, + ); + }; + + /** + * @notice update convDetail, do an ERC20 conversion batch payment with a single payment inside and calculate the balances + * @param path to update the convDetail + */ + const onePaymentBatchConv = async (path: string[]) => { + await getConvToPayAndConvDetail(to, path, amountInFiat, feesAmountInFiat, 0, chainlinkPath); + + const result = batchConvFunction(argTemplate([convDetail]), feeAddress); + if (logGas) { + const tx = await result; + await tx.wait(1); + const receipt = await tx.wait(); + console.log(`gas consumption: `, receipt.gasUsed.toString()); + } else { + await emitOneTx(expect(result), convDetail, conversionToPay, conversionFees); + } + + [fromDiffBalanceExpected, toDiffBalanceExpected, feeDiffBalanceExpected] = + expectedERC20Balances([conversionToPay.result], [conversionFees.result], batchConvFee); + }; + + /** + * @notice generate nTimes 2 convDetails, do an ERC20 conv batch payment with theses 2*nTimes payments + * and calculate the balances + * @param path2 to update the second convDetail + */ + const manyPaymentsBatchConv = async (path2: string[], nTimes: number) => { + // define a second payment request + const amountInFiat2 = BigNumber.from(amountInFiat).mul(2).toString(); + const feesAmountInFiat2 = BigNumber.from(feesAmountInFiat).mul(2).toString(); + + const conversionToPay2 = await chainlinkPath.getConversion(amountInFiat2, path2); + const conversionFees2 = await chainlinkPath.getConversion(feesAmountInFiat2, path2); + + let convDetail2 = Utils.deepCopy(convDetail); + + convDetail2.path = path2; + convDetail2.requestAmount = amountInFiat2; + convDetail2.feeAmount = feesAmountInFiat2; + convDetail2.maxToSpend = conversionToPay2.result.add(conversionFees2.result).toString(); + + // define the new arg convDetails for the function, + // and conversionsToPay & conversionsFees results to calculate the expected balances + let convDetails: ConversionDetail[] = []; + let conversionsToPay_results: BigNumber[] = []; + let conversionsFees_results: BigNumber[] = []; + for (let i = 0; i < nTimes; i++) { + convDetails = convDetails.concat([convDetail, convDetail2]); + conversionsToPay_results = conversionsToPay_results.concat([ + conversionToPay.result, + conversionToPay2.result, + ]); + conversionsFees_results = conversionsFees_results.concat([ + conversionFees.result, + conversionFees2.result, + ]); + } + + // get balances of the 2nd token, useful when there are 2 different tokens used + const fromOldBalance2 = await testERC20b.balanceOf(from); + const toOldBalance2 = await testERC20b.balanceOf(to); + const feeOldBalance2 = await testERC20b.balanceOf(feeAddress); + + const tx = await batchConvFunction(argTemplate(convDetails), feeAddress); + if (logGas) { + const receipt = await tx.wait(); + console.log(`${2 * nTimes} req, gas consumption: `, receipt.gasUsed.toString()); + } + + // 1st condition: every tokens (end of the paths) are identicals + if ( + convDetail.path[convDetail.path.length - 1] === convDetail2.path[convDetail2.path.length - 1] + ) { + [fromDiffBalanceExpected, toDiffBalanceExpected, feeDiffBalanceExpected] = + expectedERC20Balances(conversionsToPay_results, conversionsFees_results, batchConvFee); + } + // else: there are 2 different tokens used (end of the paths): testERC20 and testERC20b + else { + // calculate the expected balances of the 1st token: testERC20 + [fromDiffBalanceExpected, toDiffBalanceExpected, feeDiffBalanceExpected] = + expectedERC20Balances( + conversionsToPay_results.filter((_, i) => i % 2 === 0), + conversionsFees_results.filter((_, i) => i % 2 === 0), + batchConvFee, + ); + + // calculate the expected balances of the 2nd token: testERC20b + const [fromDiffBalanceExpected2, toDiffBalanceExpected2, feeDiffBalanceExpected2] = + expectedERC20Balances( + conversionsToPay_results.filter((_, i) => i % 2 === 1), + conversionsFees_results.filter((_, i) => i % 2 === 1), + batchConvFee, + ); + + // check the balance of testERC20b token, which is not checked in "afterEach" as testERC20 token. + checkBalancesForOneToken( + testERC20b, + fromOldBalance2, + toOldBalance2, + feeOldBalance2, + fromDiffBalanceExpected2, + toDiffBalanceExpected2, + feeDiffBalanceExpected2, + ); + } + }; + + /** + * @notice it contains all the tests related to the ERC20 batch conversion payment, and its context required + * @param isBatchRouter allows to use the function "batchERC20ConversionPaymentsMultiTokens" + * through the batchRouter or directly + */ + const ERC20ConversionTestSuite = (isBatchRouter: boolean) => { + before(() => { + setBatchConvFunction(isBatchRouter, signer); + }); + + describe(isBatchRouter ? 'Through batchRouter' : 'Without batchRouter', () => { + describe('batchERC20ConversionPaymentsMultiTokens with DAI', async () => { + it('allows to transfer DAI tokens for USD payment', async () => { + await onePaymentBatchConv(path); + }); + it('allows to transfer DAI tokens for EUR payment', async () => { + path = [EUR_hash, USD_hash, DAI_address]; + await onePaymentBatchConv(path); + }); + it('allows to transfer 2 transactions DAI tokens for USD payment', async function () { + await manyPaymentsBatchConv(path, 1); + }); + it('allows to transfer DAI tokens for EUR payment', async () => { + path = [EUR_hash, USD_hash, DAI_address]; + await onePaymentBatchConv(path); + }); + it('allows to transfer 2 transactions DAI tokens for USD and EUR payments', async function () { + const path2 = [EUR_hash, USD_hash, DAI_address]; + await manyPaymentsBatchConv(path2, 1); + }); + it('allows to transfer two kinds of tokens for USD', async function () { + const path2 = [USD_hash, FAU_address]; + await manyPaymentsBatchConv(path2, 1); + }); + }); + }); + + describe('batchERC20ConversionPaymentsMultiTokens with errors', () => { + it('cannot transfer with invalid path', async function () { + const wrongPath = [EUR_hash, ETH_hash, DAI_address]; + convDetail.path = wrongPath; + await expect(batchConvFunction(argTemplate([convDetail]), feeAddress)).to.be.revertedWith( + 'revert No aggregator found', + ); + }); + + it('cannot transfer if max to spend too low', async function () { + convDetail.maxToSpend = conversionToPay.result.add(conversionFees.result).sub(1).toString(); + await expect(batchConvFunction(argTemplate([convDetail]), feeAddress)).to.be.revertedWith( + 'Amount to pay is over the user limit', + ); + }); + + it('cannot transfer if rate is too old', async function () { + convDetail.maxRateTimespan = 10; + + await expect(batchConvFunction(argTemplate([convDetail]), feeAddress)).to.be.revertedWith( + 'aggregator rate is outdated', + ); + }); + + it('Not enough allowance', async function () { + // xSigner connect to the batch function + setBatchConvFunction(isBatchRouter, xSigner); + await expect(batchConvFunction(argTemplate([convDetail]), feeAddress)).to.be.revertedWith( + 'Insufficient allowance for batch to pay', + ); + // reset: signer connect to the batch function + setBatchConvFunction(isBatchRouter, signer); + }); + + it('Not enough funds', async function () { + // increase xSigner allowance + await testERC20 + .connect(xSigner) + .approve(testBatchConversionProxy.address, thousandWith18Decimal); + // xSigner connect to the batch function + setBatchConvFunction(isBatchRouter, xSigner); + + await expect(batchConvFunction(argTemplate([convDetail]), feeAddress)).to.be.revertedWith( + 'not enough funds, including fees', + ); + + // reset: + // - decrease xSigner allowance + // - connect with signer account + await testERC20.connect(xSigner).approve(testBatchConversionProxy.address, '0'); + testERC20.connect(signer); + setBatchConvFunction(isBatchRouter, signer); + }); + }); + }; + + /** + * @notice it does the set up of the main variables, it selects and pays with + * the desired batch EC20 batch function (no conversion), and it checks the ERC20 balances + * @param isBatchRouter allows to use a function through the batchRouter or not + * @param erc20Function selects the batch function name tested: "batchERC20PaymentsWithReference" + * or "batchERC20PaymentsMultiTokensWithReference" + */ + const batchERC20Payments = async (isBatchRouter: boolean, erc20Function: string) => { + // set up main variables + const amount = 200000; + const feeAmount = 3000; + const tokenAddress = testERC20.address; + + // Select the batch function and pay + let batchFunction: Function; + let result; + if (isBatchRouter) { + batchFunction = testBatchConversionProxy.batchRouter; + result = batchFunction( + [ + { + paymentNetworkId: erc20Function === 'batchERC20PaymentsWithReference' ? 1 : 2, + conversionDetails: [], + cryptoDetails: { + tokenAddresses: [tokenAddress], + recipients: [to], + amounts: [amount], + paymentReferences: [referenceExample], + feeAmounts: [feeAmount], + }, + }, + ], + feeAddress, + ); + } else { + batchFunction = + erc20Function === 'batchERC20PaymentsWithReference' + ? testBatchConversionProxy.batchERC20PaymentsWithReference + : testBatchConversionProxy.batchERC20PaymentsMultiTokensWithReference; + result = batchFunction( + erc20Function === 'batchERC20PaymentsWithReference' ? tokenAddress : [tokenAddress], + [to], + [amount], + [referenceExample], + [feeAmount], + feeAddress, + ); + } + // payment, check what is emitted + await expect(result) + .to.emit(testERC20, 'Transfer') + .withArgs(from, batchAddress, amount + feeAmount) + .to.emit(erc20FeeProxy, 'TransferWithReferenceAndFee') + .withArgs( + testERC20.address, + to, + amount, + ethers.utils.keccak256(referenceExample), + feeAmount, + feeAddress, + ) + // batch fee amount from the spender to feeAddress + .to.emit(testERC20, 'Transfer') + .withArgs( + from, + feeAddress, + amount * (batchFee / 10_000), // batch fee amount = 200000 * .5% + ); + + [fromDiffBalanceExpected, toDiffBalanceExpected, feeDiffBalanceExpected] = + expectedERC20Balances([BigNumber.from(amount)], [BigNumber.from(feeAmount)], batchFee, false); + }; + /** Make sure the existing ERC20 functions from the parent contract BatchPaymentPublic.sol are still working */ + describe('Test BatchErc20Payments functions', () => { + it('batchERC20PaymentsWithReference transfers token', async function () { + await batchERC20Payments(false, 'batchERC20PaymentsWithReference'); + }); + it('with batchRouter, batchERC20PaymentsWithReference transfers token', async function () { + await batchERC20Payments(true, 'batchERC20PaymentsWithReference'); + }); + + it('batchERC20PaymentsMultiTokensWithReference transfers token', async function () { + await batchERC20Payments(false, 'batchERC20PaymentsMultiTokensWithReference'); + }); + it('with batchRouter, batchERC20PaymentsMultiTokensWithReference transfers token', async function () { + await batchERC20Payments(true, 'batchERC20PaymentsMultiTokensWithReference'); + }); + }); + + ERC20ConversionTestSuite(true); + ERC20ConversionTestSuite(false); +}); From 8e3485d81a50b026a8ac52911f8cfa1d1373a5f7 Mon Sep 17 00:00:00 2001 From: olivier7delf <55892112+olivier7delf@users.noreply.github.com> Date: Wed, 10 Aug 2022 18:48:50 +0200 Subject: [PATCH 053/105] eth batch functions tested --- .../EthBatchConversionPayments.test.ts | 343 ++++++++++++++++++ 1 file changed, 343 insertions(+) create mode 100644 packages/smart-contracts/test/contracts/EthBatchConversionPayments.test.ts diff --git a/packages/smart-contracts/test/contracts/EthBatchConversionPayments.test.ts b/packages/smart-contracts/test/contracts/EthBatchConversionPayments.test.ts new file mode 100644 index 000000000..bcbd8ad0c --- /dev/null +++ b/packages/smart-contracts/test/contracts/EthBatchConversionPayments.test.ts @@ -0,0 +1,343 @@ +import { ethers, network } from 'hardhat'; +import { + EthConversionProxy__factory, + EthereumFeeProxy__factory, + EthereumFeeProxy, + ChainlinkConversionPath, + EthConversionProxy, + BatchConversionPayments, +} from '../../src/types'; +import { BigNumber, BigNumberish, BytesLike, ContractTransaction, Signer } from 'ethers'; +import { expect } from 'chai'; +import { CurrencyManager } from '@requestnetwork/currency'; +import { chainlinkConversionPath, batchConversionPaymentsArtifact } from '../../src/lib'; +import Utils from '@requestnetwork/utils'; +import { HttpNetworkConfig } from 'hardhat/types'; + +// set to true to log batch payments's gas consumption +const logGas = false; + +describe('contract: BatchConversionPayments', () => { + const networkConfig = network.config as HttpNetworkConfig; + const provider = new ethers.providers.JsonRpcProvider(networkConfig.url); + + let from: string; + let to: string; + let feeAddress: string; + let signer: Signer; + const batchFee = 50; + const batchConvFee = 100; + 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 testBatchConversionProxy: BatchConversionPayments; + let ethereumFeeProxy: EthereumFeeProxy; + let chainlinkPath: ChainlinkConversionPath; + + type ConvToPay = [BigNumber, BigNumber] & { + result: BigNumber; + oldestRateTimestamp: BigNumber; + }; + let conversionToPay: ConvToPay; + type ConversionDetail = { + recipient: string; + requestAmount: BigNumberish; + path: string[]; + paymentReference: BytesLike; + feeAmount: BigNumberish; + maxToSpend: BigNumberish; + maxRateTimespan: BigNumberish; + }; + let convDetail: ConversionDetail; + + let beforeEthBalanceTo: BigNumber; + let beforeEthBalanceFee: BigNumber; + let beforeEthBalance: BigNumber; + let feesToPay: ConvToPay; + let tx: ContractTransaction; + let amountToPayExpected: BigNumber; + let feeToPayExpected: BigNumber; + const amount = BigNumber.from(100000); // usually in USD + const feeAmount = amount.mul(10).div(10000); // usually in USD + let inputs: Array; + const pathUsdEth = [USD_hash, ETH_hash]; + + before(async () => { + [from, to, feeAddress] = (await ethers.getSigners()).map((s) => s.address); + from; + [signer] = await ethers.getSigners(); + chainlinkPath = chainlinkConversionPath.connect(network.name, signer); + ethereumFeeProxy = await new EthereumFeeProxy__factory(signer).deploy(); + testEthConversionProxy = await new EthConversionProxy__factory(signer).deploy( + ethereumFeeProxy.address, + chainlinkPath.address, + ETH_hash, + ); + + testBatchConversionProxy = batchConversionPaymentsArtifact.connect(network.name, signer); + + // update batch payment proxies, and batch fees + await testBatchConversionProxy.setPaymentEthProxy(ethereumFeeProxy.address); + await testBatchConversionProxy.setPaymentEthConversionProxy(testEthConversionProxy.address); + await testBatchConversionProxy.setBatchFee(batchFee); + await testBatchConversionProxy.setBatchConversionFee(batchConvFee); + + convDetail = { + recipient: to, + requestAmount: amount, + path: pathUsdEth, + paymentReference: referenceExample, + feeAmount: feeAmount, + maxToSpend: BigNumber.from(0), + maxRateTimespan: BigNumber.from(0), + }; + + // basic setup: 1 payment + conversionToPay = await chainlinkPath.getConversion(convDetail.requestAmount, convDetail.path); + feesToPay = await chainlinkPath.getConversion(convDetail.feeAmount, convDetail.path); + }); + + beforeEach(async () => { + beforeEthBalanceTo = await provider.getBalance(to); + beforeEthBalanceFee = await provider.getBalance(feeAddress); + beforeEthBalance = await provider.getBalance(await signer.getAddress()); + + // expected balances + amountToPayExpected = conversionToPay.result; + // fees does not include batch fees yet + feeToPayExpected = feesToPay.result; + }); + + /** + * @notice Function batch conversion, it can be the batchRouter function, used with conversion args, + * or directly batchERC20ConversionPaymentsMultiTokens + * */ + let batchConvFunction: ( + args: any, + feeAddress: string, + optional?: any, + ) => Promise; + + /** + * @notice it modify the Eth batch conversion inputs if needed, depending it is + * directly or through batchRouter + * @param isBatchRouter + * @param inputs a list of convDetail + */ + const getEthConvInputs = (isBatchRouter: boolean, inputs: Array) => { + if (isBatchRouter) { + return [ + { + paymentNetworkId: '3', + conversionDetails: inputs, + cryptoDetails: { + tokenAddresses: [], + recipients: [], + amounts: [], + paymentReferences: [], + feeAmounts: [], + }, // not used + }, + ]; + } + return inputs; + }; + + const checkEthBalances = async (amountToPayExpected: BigNumber, feeToPayExpected: BigNumber) => { + const receipt = await tx.wait(); + if (logGas) console.log('gas consumption: ', receipt.gasUsed.toString()); // get balances + const gasUsed = receipt.gasUsed.mul(2 * 10 ** 10); + + const afterEthBalance = await provider.getBalance(await signer.getAddress()); + const afterEthBalanceTo = await provider.getBalance(to); + const afterEthBalanceFee = await provider.getBalance(feeAddress); + const proxyBalance = await provider.getBalance(testBatchConversionProxy.address); + + // Calculate the difference of the balance : now - before + const _diffBalance = beforeEthBalance.sub(afterEthBalance); + const _diffBalanceTo = afterEthBalanceTo.sub(beforeEthBalanceTo); + const _diffBalanceFee = afterEthBalanceFee.sub(beforeEthBalanceFee); + + // feeToPayExpected includes batch conversion fees now + feeToPayExpected = amountToPayExpected + .add(feeToPayExpected) + .mul(batchConvFee) + .div(10000) + .add(feeToPayExpected); + const _diffBalanceExpect = gasUsed.add(amountToPayExpected).add(feeToPayExpected); + + // Check balance changes + expect(_diffBalance).to.equals(_diffBalanceExpect, 'DiffBalance'); + expect(_diffBalanceTo).to.equals(amountToPayExpected, 'diffBalanceTo'); + expect(_diffBalanceFee).to.equals(feeToPayExpected, 'diffBalanceFee'); + expect(proxyBalance).to.equals('0', 'proxyBalance'); + }; + /** + * @notice it contains all the tests related to the Eth batch payment, and its context required. + * It tests the 2 functions directly, or through batchRouter function. + * Functions: batchEthConversionPaymentsWithReference, and batchEthPaymentsWithReference + * @param isBatchRouter + */ + const EthTestSuite = (isBatchRouter: boolean) => { + describe(`Test ETH batch functions ${ + isBatchRouter ? 'through batchRouter' : 'without batchRouter' + }`, () => { + before(() => { + if (isBatchRouter) { + batchConvFunction = testBatchConversionProxy.batchRouter; + } else { + batchConvFunction = testBatchConversionProxy.batchEthConversionPaymentsWithReference; + } + }); + + describe('success functions', () => { + it('batchEthConversionPaymentsWithReference transfer 1 payment in ethers denominated in USD', async function () { + inputs = [convDetail]; + tx = await batchConvFunction(getEthConvInputs(isBatchRouter, inputs), feeAddress, { + value: BigNumber.from('100000000000000000'), + }); + await checkEthBalances(amountToPayExpected, feeToPayExpected); + }); + + it('batchEthConversionPaymentsWithReference transfer 3 payment in ethers denominated in USD', async function () { + amountToPayExpected = amountToPayExpected.mul(3); + feeToPayExpected = feeToPayExpected.mul(3); + inputs = [convDetail, convDetail, convDetail]; + tx = await batchConvFunction(getEthConvInputs(isBatchRouter, inputs), feeAddress, { + value: BigNumber.from('100000000000000000'), + }); + await checkEthBalances(amountToPayExpected, feeToPayExpected); + }); + + it('batchEthConversionPaymentsWithReference transfer 3 payments in ethers denominated in USD and EUR', async function () { + const EurConvDetail = Utils.deepCopy(convDetail); + EurConvDetail.path = [EUR_hash, USD_hash, ETH_hash]; + + const eurConversionToPay = await chainlinkPath.getConversion( + EurConvDetail.requestAmount, + EurConvDetail.path, + ); + const eurFeesToPay = await chainlinkPath.getConversion( + EurConvDetail.feeAmount, + EurConvDetail.path, + ); + + amountToPayExpected = eurConversionToPay.result.add(amountToPayExpected.mul(2)); + feeToPayExpected = eurFeesToPay.result.add(feeToPayExpected.mul(2)); + inputs = [convDetail, EurConvDetail, convDetail]; + + tx = await batchConvFunction(getEthConvInputs(isBatchRouter, inputs), feeAddress, { + value: BigNumber.from('100000000000000000'), + }); + await checkEthBalances(amountToPayExpected, feeToPayExpected); + }); + + it('batchEthPaymentsWithReference transfer 1 payment', async function () { + beforeEthBalanceTo = await provider.getBalance(to); + beforeEthBalanceFee = await provider.getBalance(feeAddress); + beforeEthBalance = await provider.getBalance(await signer.getAddress()); + + const cryptoDetails = { + tokenAddresses: [], + recipients: [to], + amounts: [amount], + paymentReferences: [referenceExample], + feeAmounts: [feeAmount], + }; + if (isBatchRouter) { + await testBatchConversionProxy.batchRouter( + [ + { + paymentNetworkId: 4, + conversionDetails: [convDetail], // not used + cryptoDetails: cryptoDetails, + }, + ], + feeAddress, + { value: 1000000000 }, + ); + } else { + await testBatchConversionProxy.batchEthPaymentsWithReference( + cryptoDetails.recipients, + cryptoDetails.amounts, + cryptoDetails.paymentReferences, + cryptoDetails.feeAmounts, + feeAddress, + { value: 1000000000 }, + ); + } + + amountToPayExpected = amount; + feeToPayExpected = feeAmount; + const afterEthBalanceTo = await provider.getBalance(to); + const afterEthBalanceFee = await provider.getBalance(feeAddress); + const proxyBalance = await provider.getBalance(testBatchConversionProxy.address); + const _diffBalanceTo = afterEthBalanceTo.sub(beforeEthBalanceTo); + const _diffBalanceFee = afterEthBalanceFee.sub(beforeEthBalanceFee); + + expect(_diffBalanceTo).to.equals(amountToPayExpected.toString(), 'diffBalanceTo'); + + feeToPayExpected = amountToPayExpected.mul(batchFee).div(10000).add(feeToPayExpected); + expect(_diffBalanceFee.toString()).to.equals( + feeToPayExpected.toString(), + 'diffBalanceFee', + ); + expect(proxyBalance).to.equals('0', 'proxyBalance'); + }); + }); + describe('revert functions', () => { + it('batchEthConversionPaymentsWithReference transfer FAIL: not enough funds', async function () { + await expect( + batchConvFunction(getEthConvInputs(isBatchRouter, [convDetail]), feeAddress, { + value: 10000, + }), + ).to.be.revertedWith('paymentProxy transferExactEthWithReferenceAndFee failed'); + }); + it('batchEthPaymentsWithReference transfer FAIL: not enough funds', async function () { + const cryptoDetails = { + tokenAddresses: [], + recipients: [to], + amounts: [amount], + paymentReferences: [referenceExample], + feeAmounts: [feeAmount], + }; + + // it contains the function being just executed, and still processing + let batchEthPayments; + if (isBatchRouter) { + batchEthPayments = testBatchConversionProxy.batchRouter( + [ + { + paymentNetworkId: 4, + conversionDetails: [convDetail], // not used + cryptoDetails: cryptoDetails, + }, + ], + feeAddress, + { value: 10000 }, + ); + } else { + batchEthPayments = testBatchConversionProxy.batchEthPaymentsWithReference( + cryptoDetails.recipients, + cryptoDetails.amounts, + cryptoDetails.paymentReferences, + cryptoDetails.feeAmounts, + feeAddress, + { value: 10000 }, + ); + } + await expect(batchEthPayments).to.be.revertedWith('not enough funds'); + }); + }); + }); + }; + + EthTestSuite(true); + EthTestSuite(false); +}); From a5dcb320f0847df6e0e16ba89f49cee6dba7f1ea Mon Sep 17 00:00:00 2001 From: olivier7delf <55892112+olivier7delf@users.noreply.github.com> Date: Wed, 10 Aug 2022 18:58:41 +0200 Subject: [PATCH 054/105] rewording and cleaning --- .../src/contracts/BatchPaymentsPublic.sol | 2 +- .../contracts/BatchConversionPayments.test.ts | 789 ------------------ .../ERC20BatchConversionPayments.test.ts | 2 +- .../EthBatchConversionPayments.test.ts | 9 +- 4 files changed, 7 insertions(+), 795 deletions(-) delete mode 100644 packages/smart-contracts/test/contracts/BatchConversionPayments.test.ts diff --git a/packages/smart-contracts/src/contracts/BatchPaymentsPublic.sol b/packages/smart-contracts/src/contracts/BatchPaymentsPublic.sol index 585a17ffd..95181065d 100644 --- a/packages/smart-contracts/src/contracts/BatchPaymentsPublic.sol +++ b/packages/smart-contracts/src/contracts/BatchPaymentsPublic.sol @@ -27,7 +27,7 @@ contract BatchPaymentsPublic is Ownable { IEthereumFeeProxy public paymentEthProxy; uint256 public batchFee; - /** Used to to calcul batch fees */ + /** Used to to calculate batch fees */ uint256 internal tenThousand = 10000; // payerAuthorized is set to true only when needed for batch Eth conversion diff --git a/packages/smart-contracts/test/contracts/BatchConversionPayments.test.ts b/packages/smart-contracts/test/contracts/BatchConversionPayments.test.ts deleted file mode 100644 index fccac7480..000000000 --- a/packages/smart-contracts/test/contracts/BatchConversionPayments.test.ts +++ /dev/null @@ -1,789 +0,0 @@ -import { ethers, network } from 'hardhat'; -import { - ERC20FeeProxy__factory, - Erc20ConversionProxy__factory, - EthConversionProxy__factory, - EthereumFeeProxy__factory, - ERC20FeeProxy, - EthereumFeeProxy, - ChainlinkConversionPath, - TestERC20, - Erc20ConversionProxy, - EthConversionProxy, - TestERC20__factory, - BatchConversionPayments, -} from '../../src/types'; -import { BigNumber, BigNumberish, BytesLike, ContractTransaction, Signer } from 'ethers'; -import { expect } from 'chai'; -import { CurrencyManager } from '@requestnetwork/currency'; -import { chainlinkConversionPath, batchConversionPaymentsArtifact } from '../../src/lib'; -import { localERC20AlphaArtifact, secondLocalERC20AlphaArtifact } from './localArtifacts'; -import Utils from '@requestnetwork/utils'; -import { HttpNetworkConfig } from 'hardhat/types'; - -// set to true to log batch payments's gas consumption -const logGas = false; - -describe('contract: BatchErc20ConversionPayments', () => { - const networkConfig = network.config as HttpNetworkConfig; - const provider = new ethers.providers.JsonRpcProvider(networkConfig.url); - - let from: string; - let to: string; - let feeAddress: string; - let batchAddress: string; - let signer: Signer; - let xSigner: Signer; - const batchFee = 100; - const batchConvFee = 100; - const amountInFiat = '100000000'; // 1 with 8 decimal - const feesAmountInFiat = '100000'; // 0.001 with 8 decimal - const thousandWith18Decimal = '1000000000000000000000'; - 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 DAI_address: string; - let fakeFAU_address: string; - - let testErc20ConversionProxy: Erc20ConversionProxy; - let testEthConversionProxy: EthConversionProxy; - let testBatchConversionProxy: BatchConversionPayments; - let testERC20: TestERC20; - let testERC20b: TestERC20; - let erc20FeeProxy: ERC20FeeProxy; - let ethereumFeeProxy: EthereumFeeProxy; - let chainlinkPath: ChainlinkConversionPath; - - let path: string[]; - type ConvToPay = [BigNumber, BigNumber] & { - result: BigNumber; - oldestRateTimestamp: BigNumber; - }; - let conversionToPay: ConvToPay; - let conversionFees: ConvToPay; - - let fromOldBalance: BigNumber; - let toOldBalance: BigNumber; - let feeOldBalance: BigNumber; - let batchOldBalance: BigNumber; - - let fromBalance: BigNumber; - let toBalance: BigNumber; - let feeBalance: BigNumber; - let batchBalance: BigNumber; - - let fromDiffBalanceExpected: BigNumber; - let toDiffBalanceExpected: BigNumber; - let feeDiffBalanceExpected: BigNumber; - - type ConversionDetail = { - recipient: string; - requestAmount: BigNumberish; - path: string[]; - paymentReference: BytesLike; - feeAmount: BigNumberish; - maxToSpend: BigNumberish; - maxRateTimespan: BigNumberish; - }; - let convDetail: ConversionDetail; - - let cryptoDetails1 = { - tokenAddresses: [], - recipients: [], - amounts: [], - paymentReferences: [], - feeAmounts: [], - }; - /** Function used to emit events of batch conversion proxy */ - let emitOneTx: Function; - /** - * @notice Function batch conversion, it can be the batchRouter function, used with conversion args, - * or directly batchERC20ConversionPaymentsMultiTokens - * */ - let batchConvFunction: ( - args: any, - feeAddress: string, - optional?: any, - ) => Promise; - /** Format arguments so they can be used by batchConvFunction */ - let argTemplate: Function; - - before(async () => { - [from, to, feeAddress] = (await ethers.getSigners()).map((s) => s.address); - [signer, xSigner, xSigner, xSigner] = await ethers.getSigners(); - chainlinkPath = chainlinkConversionPath.connect(network.name, signer); - erc20FeeProxy = await new ERC20FeeProxy__factory(signer).deploy(); - ethereumFeeProxy = await new EthereumFeeProxy__factory(signer).deploy(); - testErc20ConversionProxy = await new Erc20ConversionProxy__factory(signer).deploy( - erc20FeeProxy.address, - chainlinkPath.address, - await signer.getAddress(), - ); - testEthConversionProxy = await new EthConversionProxy__factory(signer).deploy( - ethereumFeeProxy.address, - chainlinkPath.address, - ETH_hash, - ); - - testBatchConversionProxy = batchConversionPaymentsArtifact.connect(network.name, signer); - - // update batch payment proxies, and batch fees - await testBatchConversionProxy.setPaymentErc20Proxy(erc20FeeProxy.address); - await testBatchConversionProxy.setPaymentEthProxy(ethereumFeeProxy.address); - await testBatchConversionProxy.setPaymentErc20ConversionProxy(testErc20ConversionProxy.address); - await testBatchConversionProxy.setPaymentEthConversionProxy(testEthConversionProxy.address); - - await testBatchConversionProxy.setBatchFee(batchFee); - await testBatchConversionProxy.setBatchConversionFee(batchConvFee); - - DAI_address = localERC20AlphaArtifact.getAddress(network.name); - testERC20 = new TestERC20__factory(signer).attach(DAI_address); - - fakeFAU_address = secondLocalERC20AlphaArtifact.getAddress(network.name); - testERC20b = new TestERC20__factory(signer).attach(fakeFAU_address); - batchAddress = testBatchConversionProxy.address; - }); - - beforeEach(async () => { - fromDiffBalanceExpected = BigNumber.from(0); - toDiffBalanceExpected = BigNumber.from(0); - feeDiffBalanceExpected = BigNumber.from(0); - await testERC20.approve(testBatchConversionProxy.address, thousandWith18Decimal, { - from, - }); - await testERC20b.approve(testBatchConversionProxy.address, thousandWith18Decimal, { - from, - }); - fromOldBalance = await testERC20.balanceOf(from); - toOldBalance = await testERC20.balanceOf(to); - feeOldBalance = await testERC20.balanceOf(feeAddress); - batchOldBalance = await testERC20.balanceOf(batchAddress); - }); - - afterEach(async () => { - fromBalance = await testERC20.balanceOf(from); - toBalance = await testERC20.balanceOf(to); - feeBalance = await testERC20.balanceOf(feeAddress); - batchBalance = await testERC20.balanceOf(batchAddress); - const fromDiffBalance = BigNumber.from(fromBalance.toString()) - .sub(fromOldBalance.toString()) - .toString(); - const toDiffBalance = BigNumber.from(toBalance.toString()) - .sub(toOldBalance.toString()) - .toString(); - const feeDiffBalance = BigNumber.from(feeBalance.toString()) - .sub(feeOldBalance.toString()) - .toString(); - const batchDiffBalance = BigNumber.from(batchBalance.toString()) - .sub(batchOldBalance.toString()) - .toString(); - - // Check balance changes - expect(fromDiffBalance).to.equals( - (fromDiffBalanceExpected.toString() !== '0' ? '-' : '') + fromDiffBalanceExpected.toString(), - 'fromDiffBalance', - ); - expect(toDiffBalance).to.equals(toDiffBalanceExpected.toString(), 'toDiffBalance'); - expect(feeDiffBalance).to.equals(feeDiffBalanceExpected.toString(), 'feeDiffBalance'); - expect(batchDiffBalance).to.equals('0', 'batchDiffBalance'); - }); - - const batchFeeToPay = (conversionAmountToPay: BigNumber) => { - return conversionAmountToPay.mul(batchConvFee).div(10000); - }; - - /** - * @notice it gets the conversions including fees to be paid, and it set the convDetail - */ - const initConvToPayAndConvDetail = async ( - _recipient: string, - _path: string[], - _requestAmount: string, - _feeAmount: string, - _maxRateTimespan: number, - _chainlinkPath: ChainlinkConversionPath, - ) => { - conversionToPay = await _chainlinkPath.getConversion(_requestAmount, _path); - conversionFees = await _chainlinkPath.getConversion(_feeAmount, _path); - convDetail = { - recipient: _recipient, - requestAmount: _requestAmount, - path: _path, - paymentReference: referenceExample, - feeAmount: _feeAmount, - maxToSpend: conversionToPay.result.add(conversionFees.result).toString(), - maxRateTimespan: _maxRateTimespan, - }; - }; - - /** - * @notice Used to calcul the expected new ERC20 balance for batch conversion. - * It can also be used for batch IF batchFee == batchConvFee - * @param _conversionsToPay_results is used to calcul batch fees, in case of multiple payments - */ - const calculERC20Balances = ( - _conversionToPay_result: BigNumber, - _conversionFees_result: BigNumber, - _conversionsToPay_results: BigNumber[], - _conversionFees_results: BigNumber[], - ) => { - fromDiffBalanceExpected = fromDiffBalanceExpected - .add(_conversionToPay_result) - .add(_conversionFees_result); - - toDiffBalanceExpected = toDiffBalanceExpected.add(_conversionToPay_result); - feeDiffBalanceExpected = feeDiffBalanceExpected.add(_conversionFees_result); - if (_conversionsToPay_results.length > 0) - calculERC20BatchFeeBalances(_conversionsToPay_results, _conversionFees_results); - }; - - /** - * @notice Used to calcul the expected new ERC20 fee batch balance for batch conversion. - * @param _conversionsToPay_results is used to calcul batch fees, it case of payments multiple - * @dev in case of payments multiple, we sum the amount paid including fees, and then, we calcul the batch fees amount - * because the sum(batchFeeToPay(amountPay[i])) != batchFeeToPay(sum(amountPay[i])) - */ - const calculERC20BatchFeeBalances = ( - _conversionsToPay_results: BigNumber[], - _conversionFees_result: BigNumber[], - ) => { - let sumToPay = BigNumber.from(0); - for (let i = 0; i < _conversionsToPay_results.length; i++) { - sumToPay = sumToPay.add(_conversionsToPay_results[i]).add(_conversionFees_result[i]); - } - fromDiffBalanceExpected = fromDiffBalanceExpected.add(batchFeeToPay(sumToPay)); - feeDiffBalanceExpected = feeDiffBalanceExpected.add(batchFeeToPay(sumToPay)); - }; - - /** - * @notice update convDetail, do an ERC20 conv batch payment and calcul the balances - * @param path to update the convDetail - */ - const transferOneTokenConv = async (path: string[]) => { - await initConvToPayAndConvDetail(to, path, amountInFiat, feesAmountInFiat, 0, chainlinkPath); - - const result = batchConvFunction(argTemplate([convDetail]), feeAddress); - if (logGas) { - const tx = await result; - await tx.wait(1); - const receipt = await tx.wait(); - console.log(`gas consumption: `, receipt.gasUsed.toString()); - } else { - await emitOneTx(expect(result), convDetail, conversionToPay, conversionFees); - } - - calculERC20Balances( - conversionToPay.result, - conversionFees.result, - [conversionToPay.result], - [conversionFees.result], - ); - }; - - /** - * @notice generate nTimes 2 convDetails, do an ERC20 conv batch payment with theses 2*nTimes requests - * and calcul the balances - * @param path2 to update the second convDetail - */ - const transferTokensConv = async (path2: string[], nTimes: number) => { - const coef = 2; - const amountInFiat2 = BigNumber.from(amountInFiat).mul(coef).toString(); - const feesAmountInFiat2 = BigNumber.from(feesAmountInFiat).mul(coef).toString(); - - const conversionToPay2 = await chainlinkPath.getConversion(amountInFiat2, path2); - const conversionFees2 = await chainlinkPath.getConversion(feesAmountInFiat2, path2); - - let convDetail2 = Utils.deepCopy(convDetail); - - convDetail2.path = path2; - convDetail2.requestAmount = amountInFiat2; - convDetail2.feeAmount = feesAmountInFiat2; - convDetail2.maxToSpend = conversionToPay2.result.add(conversionFees2.result).toString(); - - let convDetails: ConversionDetail[] = []; - let conversionsToPay: ConvToPay[] = []; - let conversionsFees: ConvToPay[] = []; - for (let i = 0; i < nTimes; i++) { - convDetails = convDetails.concat([convDetail, convDetail2]); - conversionsToPay = conversionsToPay.concat([conversionToPay, conversionToPay2]); - conversionsFees = conversionsFees.concat([conversionFees, conversionFees2]); - } - const tx = await batchConvFunction(argTemplate(convDetails), feeAddress); - if (logGas) { - const receipt = await tx.wait(); - console.log(`${2 * nTimes} req, gas consumption: `, receipt.gasUsed.toString()); - } - - if ( - convDetail.path[convDetail.path.length - 1] === convDetail2.path[convDetail2.path.length - 1] - ) { - for (let i = 0; i < nTimes - 1; i++) { - calculERC20Balances(conversionToPay.result, conversionFees.result, [], []); - calculERC20Balances(conversionToPay2.result, conversionFees2.result, [], []); - } - calculERC20Balances(conversionToPay.result, conversionFees.result, [], []); - calculERC20Balances( - conversionToPay2.result, - conversionFees2.result, - conversionsToPay.map((ctp) => ctp.result), - conversionsFees.map((ctp) => ctp.result), - ); - } else { - for (let i = 0; i < nTimes - 1; i++) { - calculERC20Balances(conversionToPay.result, conversionFees.result, [], []); - } - const conversionsToPayBis = conversionsToPay.filter((_, i) => i % 2 === 0); - - calculERC20Balances( - conversionToPay.result, - conversionFees.result, - conversionsToPayBis.map((ctp) => ctp.result), - conversionsFees.map((ctp) => ctp.result), - ); - } - }; - - /** - * @notice it contains all the tests related to the ERC20 batch payment, and its context required - * @param erc20Function is the batch function name tested: "batchRouter" or "batchERC20ConversionPaymentsMultiTokens" - */ - const ERC20TestSuite = (erc20Function: string) => { - emitOneTx = ( - result: Chai.Assertion, - convDetail: ConversionDetail, - _conversionToPay = conversionToPay, - _conversionFees = conversionFees, - _testErc20ConversionProxy = testErc20ConversionProxy, - ) => { - return result.to - .emit(testErc20ConversionProxy, 'TransferWithConversionAndReference') - .withArgs( - convDetail.requestAmount, - ethers.utils.getAddress(convDetail.path[0]), - ethers.utils.keccak256(referenceExample), - convDetail.feeAmount, - '0', - ) - .to.emit(testErc20ConversionProxy, 'TransferWithReferenceAndFee') - .withArgs( - ethers.utils.getAddress(DAI_address), - ethers.utils.getAddress(convDetail.recipient), - _conversionToPay.result, - ethers.utils.keccak256(referenceExample), - _conversionFees.result, - feeAddress, - ); - }; - - beforeEach(async () => { - path = [USD_hash, DAI_address]; - initConvToPayAndConvDetail(to, path, amountInFiat, feesAmountInFiat, 0, chainlinkPath); - }); - - const setBatchConvFunction = async (_signer: Signer) => { - if (erc20Function === 'batchRouter') { - batchConvFunction = testBatchConversionProxy.connect(_signer).batchRouter; - argTemplate = (convDetails: ConversionDetail[]) => { - return [ - { - paymentNetworkId: '0', - conversionDetails: convDetails, - cryptoDetails: cryptoDetails1, - }, - ]; - }; - } - if (erc20Function === 'batchERC20ConversionPaymentsMultiTokens') { - batchConvFunction = - testBatchConversionProxy.connect(_signer).batchERC20ConversionPaymentsMultiTokens; - argTemplate = (convDetails: ConversionDetail[]) => { - return convDetails; - }; - } - }; - before(() => { - setBatchConvFunction(signer); - }); - describe(erc20Function, () => { - describe('batchERC20ConversionPaymentsMultiTokens with DAI', async () => { - it('allows to transfer DAI tokens for USD payment', async () => { - await transferOneTokenConv(path); - }); - it('allows to transfer DAI tokens for EUR payment', async () => { - path = [EUR_hash, USD_hash, DAI_address]; - await transferOneTokenConv(path); - }); - it('allows to transfer 2 transactions DAI tokens for USD payment', async function () { - await transferTokensConv(path, 1); - }); - it('allows to transfer DAI tokens for EUR payment', async () => { - path = [EUR_hash, USD_hash, DAI_address]; - await transferOneTokenConv(path); - }); - it('allows to transfer 2 transactions DAI tokens for USD and EUR payments', async function () { - const path2 = [EUR_hash, USD_hash, DAI_address]; - await transferTokensConv(path2, 1); - }); - it('allows to transfer two kinds of tokens for USD', async function () { - const path2 = [USD_hash, fakeFAU_address]; - await transferTokensConv(path2, 1); - }); - }); - }); - - describe('batchERC20ConversionPaymentsMultiTokens with errors', () => { - it('cannot transfer with invalid path', async function () { - const wrongPath = [EUR_hash, ETH_hash, DAI_address]; - convDetail.path = wrongPath; - await expect(batchConvFunction(argTemplate([convDetail]), feeAddress)).to.be.revertedWith( - 'revert No aggregator found', - ); - }); - - it('cannot transfer if max to spend too low', async function () { - convDetail.maxToSpend = conversionToPay.result.add(conversionFees.result).sub(1).toString(); - await expect(batchConvFunction(argTemplate([convDetail]), feeAddress)).to.be.revertedWith( - 'Amount to pay is over the user limit', - ); - }); - - it('cannot transfer if rate is too old', async function () { - convDetail.maxRateTimespan = 10; - - await expect(batchConvFunction(argTemplate([convDetail]), feeAddress)).to.be.revertedWith( - 'aggregator rate is outdated', - ); - }); - - it('Not enough allowance', async function () { - // xSigner connect to the batch function - setBatchConvFunction(xSigner); - await expect(batchConvFunction(argTemplate([convDetail]), feeAddress)).to.be.revertedWith( - 'Insufficient allowance for batch to pay', - ); - // reset: signer connect to the batch function - setBatchConvFunction(signer); - }); - - it('Not enough funds', async function () { - // increase xSigner allowance - await testERC20 - .connect(xSigner) - .approve(testBatchConversionProxy.address, thousandWith18Decimal); - // xSigner connect to the batch function - setBatchConvFunction(xSigner); - - await expect(batchConvFunction(argTemplate([convDetail]), feeAddress)).to.be.revertedWith( - 'not enough funds, including fees', - ); - - // reset: - // - decrease xSigner allowance - // - connect with signer account - await testERC20.connect(xSigner).approve(testBatchConversionProxy.address, '0'); - testERC20.connect(signer); - setBatchConvFunction(signer); - }); - }); - }; - - /** Make sure the existing ERC20 functions from the parent contract BatchPaymentPublic.sol are still working */ - describe('Test BatchErc20Payments functions', () => { - const batchERC20Payments = async (isBatchRouter: boolean, subFunction: string) => { - const amount = 200; - const feeAmount = 20; - let batchFunction: Function; - const tokenAddress = testERC20.address; - let result; - if (isBatchRouter) { - batchFunction = testBatchConversionProxy.batchRouter; - result = batchFunction( - [ - { - paymentNetworkId: subFunction === 'batchERC20PaymentsWithReference' ? 1 : 2, - conversionDetails: [], - cryptoDetails: { - tokenAddresses: [tokenAddress], - recipients: [to], - amounts: [amount], - paymentReferences: [referenceExample], - feeAmounts: [feeAmount], - }, - }, - ], - feeAddress, - ); - } else { - batchFunction = - subFunction === 'batchERC20PaymentsWithReference' - ? testBatchConversionProxy.batchERC20PaymentsWithReference - : testBatchConversionProxy.batchERC20PaymentsMultiTokensWithReference; - result = batchFunction( - subFunction === 'batchERC20PaymentsWithReference' ? tokenAddress : [tokenAddress], - [to], - [amount], - [referenceExample], - [feeAmount], - feeAddress, - ); - } - await expect(result) - .to.emit(testERC20, 'Transfer') - .withArgs(from, batchAddress, amount + feeAmount) - .to.emit(erc20FeeProxy, 'TransferWithReferenceAndFee') - .withArgs( - testERC20.address, - to, - amount, - ethers.utils.keccak256(referenceExample), - feeAmount, - feeAddress, - ) - // batch fee amount from the spender to feeAddress - .to.emit(testERC20, 'Transfer') - .withArgs( - from, - feeAddress, - amount * (batchFee / 10_000), // batch fee amount = 200 * 1% - ); - - calculERC20Balances( - BigNumber.from(amount), - BigNumber.from(feeAmount), - [BigNumber.from(amount)], - [BigNumber.from(feeAmount)], - ); - }; - it('batchERC20PaymentsWithReference transfers token', async function () { - await batchERC20Payments(false, 'batchERC20PaymentsWithReference'); - }); - it('with batchRouter, batchERC20PaymentsWithReference transfers token', async function () { - await batchERC20Payments(true, 'batchERC20PaymentsWithReference'); - }); - - it('batchERC20PaymentsMultiTokensWithReference transfers token', async function () { - await batchERC20Payments(false, 'batchERC20PaymentsMultiTokensWithReference'); - }); - it('with batchRouter, batchERC20PaymentsMultiTokensWithReference transfers token', async function () { - await batchERC20Payments(true, 'batchERC20PaymentsMultiTokensWithReference'); - }); - }); - - /** - * @notice it contains all the tests related to the Eth batch payment, and its context required - * @param ethFunction is the batch function name tested: "batchRouter" or "batchEthConversionPaymentsWithReference" - */ - const EthTestSuite = (ethFunction: string) => { - describe(`Test ETH ${ethFunction} functions`, () => { - let beforeEthBalanceTo: BigNumber; - let beforeEthBalanceFee: BigNumber; - let beforeEthBalance: BigNumber; - let feesToPay: ConvToPay; - let tx: ContractTransaction; - let amountToPayExpected: BigNumber; - let feeToPayExpected: BigNumber; - const amount = BigNumber.from(100000); // usually in USD - const feeAmount = amount.mul(10).div(10000); // usually in USD - let inputs: Array; - const pathUsdEth = [USD_hash, ETH_hash]; - - /** - * @notice it modify the Eth batch inputs if needed, depending of the function used: ethFunction - * @param inputs a list of convDetail - */ - const getEthInputs = (inputs: Array) => { - if (ethFunction !== 'batchEthConversionPaymentsWithReference') { - return [ - { - paymentNetworkId: '3', - conversionDetails: inputs, - cryptoDetails: cryptoDetails1, // not used - }, - ]; - } - return inputs; - }; - - before(() => { - if (ethFunction === 'batchEthConversionPaymentsWithReference') { - batchConvFunction = testBatchConversionProxy.batchEthConversionPaymentsWithReference; - } else if (ethFunction === 'batchRouter') { - batchConvFunction = testBatchConversionProxy.batchRouter; - } - - convDetail = { - recipient: to, - requestAmount: amount, - path: pathUsdEth, - paymentReference: referenceExample, - feeAmount: feeAmount, - maxToSpend: BigNumber.from(0), - maxRateTimespan: BigNumber.from(0), - }; - }); - - describe('success functions', () => { - beforeEach(async () => { - beforeEthBalanceTo = await provider.getBalance(to); - beforeEthBalanceFee = await provider.getBalance(feeAddress); - beforeEthBalance = await provider.getBalance(await signer.getAddress()); - convDetail = { - recipient: to, - requestAmount: amount, - path: pathUsdEth, - paymentReference: referenceExample, - feeAmount: feeAmount, - maxToSpend: BigNumber.from(0), - maxRateTimespan: BigNumber.from(0), - }; - - // basic setup: 1 payment - conversionToPay = await chainlinkPath.getConversion( - convDetail.requestAmount, - convDetail.path, - ); - feesToPay = await chainlinkPath.getConversion(convDetail.feeAmount, convDetail.path); - - amountToPayExpected = conversionToPay.result; - // fees does not include batch conv fees yet - feeToPayExpected = feesToPay.result; - }); - - afterEach(async () => { - tx = await batchConvFunction(getEthInputs(inputs), feeAddress, { - value: BigNumber.from('100000000000000000'), - }); - const receipt = await tx.wait(); - if (logGas) console.log('gas consumption: ', receipt.gasUsed.toString()); - - const afterEthBalance = await provider.getBalance(await signer.getAddress()); - const afterEthBalanceTo = await provider.getBalance(to); - const afterEthBalanceFee = await provider.getBalance(feeAddress); - const proxyBalance = await provider.getBalance(testBatchConversionProxy.address); - const _diffBalance = beforeEthBalance.sub(afterEthBalance); - const _diffBalanceTo = afterEthBalanceTo.sub(beforeEthBalanceTo); - const _diffBalanceFee = afterEthBalanceFee.sub(beforeEthBalanceFee); - const _diffBalanceExpect = receipt.gasUsed - .mul(2 * 10 ** 10) - .add(_diffBalanceTo) - .add(_diffBalanceFee); - expect(_diffBalance).to.equals(_diffBalanceExpect.toString(), 'DiffBalance'); - expect(_diffBalanceTo).to.equals(amountToPayExpected.toString(), 'diffBalanceTo'); - - // feeToPayExpected includes batch conversion fees now - feeToPayExpected = amountToPayExpected - .add(feeToPayExpected) - .mul(batchConvFee) - .div(10000) - .add(feeToPayExpected); - expect(_diffBalanceFee.toString()).to.equals( - feeToPayExpected.toString(), - 'diffBalanceFee', - ); - expect(proxyBalance).to.equals('0', 'proxyBalance'); - }); - - it('batchEthConversionPaymentsWithReference transfer 1 payment in ethers denominated in USD', async function () { - inputs = [convDetail]; - }); - - it('batchEthConversionPaymentsWithReference transfer 3 payment in ethers denominated in USD', async function () { - amountToPayExpected = amountToPayExpected.mul(3); - feeToPayExpected = feeToPayExpected.mul(3); - inputs = [convDetail, convDetail, convDetail]; - }); - - it('batchEthConversionPaymentsWithReference transfer 3 payments in ethers denominated in USD and EUR', async function () { - const EurConvDetail = Utils.deepCopy(convDetail); - EurConvDetail.path = [EUR_hash, USD_hash, ETH_hash]; - - const eurConversionToPay = await chainlinkPath.getConversion( - EurConvDetail.requestAmount, - EurConvDetail.path, - ); - const eurFeesToPay = await chainlinkPath.getConversion( - EurConvDetail.feeAmount, - EurConvDetail.path, - ); - - amountToPayExpected = eurConversionToPay.result.add(amountToPayExpected.mul(2)); - feeToPayExpected = eurFeesToPay.result.add(feeToPayExpected.mul(2)); - inputs = [convDetail, EurConvDetail, convDetail]; - }); - }); - it('batchEthConversionPaymentsWithReference transfer FAIL: not enough funds', async function () { - await expect( - batchConvFunction(getEthInputs([convDetail]), feeAddress, { - value: 10000, - }), - ).to.be.revertedWith('paymentProxy transferExactEthWithReferenceAndFee failed'); - }); - - describe(`Eth BatchPaymentPublic functions: ${ - ethFunction === 'batchRouter' ?? '' - } batchEthPaymentsWithReference`, () => { - it('transfer 1 payment', async function () { - beforeEthBalanceTo = await provider.getBalance(to); - beforeEthBalanceFee = await provider.getBalance(feeAddress); - beforeEthBalance = await provider.getBalance(await signer.getAddress()); - - const cryptoDetails = { - tokenAddresses: [], - recipients: [to], - amounts: [amount], - paymentReferences: [referenceExample], - feeAmounts: [feeAmount], - }; - if (ethFunction === 'batchRouter') { - await testBatchConversionProxy.batchRouter( - [ - { - paymentNetworkId: 4, - conversionDetails: [convDetail], // not used - cryptoDetails: cryptoDetails, - }, - ], - feeAddress, - { value: 1000000000 }, - ); - } else if (ethFunction === 'batchEthConversionPaymentsWithReference') { - await testBatchConversionProxy.batchEthPaymentsWithReference( - cryptoDetails.recipients, - cryptoDetails.amounts, - cryptoDetails.paymentReferences, - cryptoDetails.feeAmounts, - feeAddress, - { value: 1000000000 }, - ); - } - - amountToPayExpected = amount; - feeToPayExpected = feeAmount; - const afterEthBalanceTo = await provider.getBalance(to); - const afterEthBalanceFee = await provider.getBalance(feeAddress); - const proxyBalance = await provider.getBalance(testBatchConversionProxy.address); - const _diffBalanceTo = afterEthBalanceTo.sub(beforeEthBalanceTo); - const _diffBalanceFee = afterEthBalanceFee.sub(beforeEthBalanceFee); - - expect(_diffBalanceTo).to.equals(amountToPayExpected.toString(), 'diffBalanceTo'); - - feeToPayExpected = amountToPayExpected.mul(batchFee).div(10000).add(feeToPayExpected); - expect(_diffBalanceFee.toString()).to.equals( - feeToPayExpected.toString(), - 'diffBalanceFee', - ); - expect(proxyBalance).to.equals('0', 'proxyBalance'); - }); - }); - }); - }; - - ERC20TestSuite('batchRouter'); - ERC20TestSuite('batchERC20ConversionPaymentsMultiTokens'); - EthTestSuite('batchRouter'); - EthTestSuite('batchEthConversionPaymentsWithReference'); -}); diff --git a/packages/smart-contracts/test/contracts/ERC20BatchConversionPayments.test.ts b/packages/smart-contracts/test/contracts/ERC20BatchConversionPayments.test.ts index 54dc1e634..792b84a4b 100644 --- a/packages/smart-contracts/test/contracts/ERC20BatchConversionPayments.test.ts +++ b/packages/smart-contracts/test/contracts/ERC20BatchConversionPayments.test.ts @@ -23,7 +23,7 @@ import Utils from '@requestnetwork/utils'; // set to true to log batch payments's gas consumption const logGas = false; -describe('contract: BatchErc20ConversionPayments', () => { +describe('contract: BatchConversionPayments', () => { let from: string; let to: string; let feeAddress: string; diff --git a/packages/smart-contracts/test/contracts/EthBatchConversionPayments.test.ts b/packages/smart-contracts/test/contracts/EthBatchConversionPayments.test.ts index bcbd8ad0c..6f8ec3efe 100644 --- a/packages/smart-contracts/test/contracts/EthBatchConversionPayments.test.ts +++ b/packages/smart-contracts/test/contracts/EthBatchConversionPayments.test.ts @@ -63,8 +63,9 @@ describe('contract: BatchConversionPayments', () => { let tx: ContractTransaction; let amountToPayExpected: BigNumber; let feeToPayExpected: BigNumber; - const amount = BigNumber.from(100000); // usually in USD - const feeAmount = amount.mul(10).div(10000); // usually in USD + // amount and feeAmount are usually in fiat for conversion inputs, else in ETH + const amount = BigNumber.from(100000); + const feeAmount = amount.mul(10).div(10000); let inputs: Array; const pathUsdEth = [USD_hash, ETH_hash]; @@ -246,9 +247,9 @@ describe('contract: BatchConversionPayments', () => { const cryptoDetails = { tokenAddresses: [], recipients: [to], - amounts: [amount], + amounts: [amount], // in ETH paymentReferences: [referenceExample], - feeAmounts: [feeAmount], + feeAmounts: [feeAmount], // in ETH }; if (isBatchRouter) { await testBatchConversionProxy.batchRouter( From 39199c8f17af504d18e1851b68a37f7f5376d608 Mon Sep 17 00:00:00 2001 From: olivier7delf <55892112+olivier7delf@users.noreply.github.com> Date: Tue, 16 Aug 2022 18:15:00 +0200 Subject: [PATCH 055/105] refacto tests batch conversion ERC20 --- ...s => BatchConversionErc20Payments.test.ts} | 394 ++++++++---------- 1 file changed, 183 insertions(+), 211 deletions(-) rename packages/smart-contracts/test/contracts/{ERC20BatchConversionPayments.test.ts => BatchConversionErc20Payments.test.ts} (68%) diff --git a/packages/smart-contracts/test/contracts/ERC20BatchConversionPayments.test.ts b/packages/smart-contracts/test/contracts/BatchConversionErc20Payments.test.ts similarity index 68% rename from packages/smart-contracts/test/contracts/ERC20BatchConversionPayments.test.ts rename to packages/smart-contracts/test/contracts/BatchConversionErc20Payments.test.ts index 792b84a4b..2278e6959 100644 --- a/packages/smart-contracts/test/contracts/ERC20BatchConversionPayments.test.ts +++ b/packages/smart-contracts/test/contracts/BatchConversionErc20Payments.test.ts @@ -28,10 +28,10 @@ describe('contract: BatchConversionPayments', () => { let to: string; let feeAddress: string; let batchAddress: string; - let signer: Signer; - let xSigner: Signer; + let signer1: Signer; + let signer4: Signer; - // variables used to set up batch conversion proxy, and also requests payment + // constants used to set up batch conversion proxy, and also requests payment const batchFee = 50; const batchConvFee = 100; const amountInFiat = '100000000'; // 1 with 8 decimal @@ -39,7 +39,7 @@ describe('contract: BatchConversionPayments', () => { const thousandWith18Decimal = '1000000000000000000000'; const referenceExample = '0xaaaa'; - // variables to set up proxies and paths + // constants and variables to set up proxies and paths const currencyManager = CurrencyManager.getDefault(); const ETH_hash = currencyManager.fromSymbol('ETH')!.hash; @@ -66,16 +66,12 @@ describe('contract: BatchConversionPayments', () => { let toDiffBalanceExpected: BigNumber; let feeDiffBalanceExpected: BigNumber; - // variables needed for chainlink and + // variables needed for chainlink and conversion payments let path: string[]; - type ConvToPay = [BigNumber, BigNumber] & { - result: BigNumber; - oldestRateTimestamp: BigNumber; - }; - let conversionToPay: ConvToPay; - let conversionFees: ConvToPay; + let conversionToPayBis: BigNumber; + let conversionFeesBis: BigNumber; - // type required by Erc20 conversion batch function input + // type required by Erc20 conversion batch function inputs type ConversionDetail = { recipient: string; requestAmount: BigNumberish; @@ -100,75 +96,6 @@ describe('contract: BatchConversionPayments', () => { /** Format arguments so they can be used by batchConvFunction */ let argTemplate: Function; - before(async () => { - [from, to, feeAddress] = (await ethers.getSigners()).map((s) => s.address); - [signer, xSigner, xSigner, xSigner] = await ethers.getSigners(); - chainlinkPath = chainlinkConversionPath.connect(network.name, signer); - erc20FeeProxy = await new ERC20FeeProxy__factory(signer).deploy(); - ethereumFeeProxy = await new EthereumFeeProxy__factory(signer).deploy(); - testErc20ConversionProxy = await new Erc20ConversionProxy__factory(signer).deploy( - erc20FeeProxy.address, - chainlinkPath.address, - await signer.getAddress(), - ); - testEthConversionProxy = await new EthConversionProxy__factory(signer).deploy( - ethereumFeeProxy.address, - chainlinkPath.address, - ETH_hash, - ); - testBatchConversionProxy = batchConversionPaymentsArtifact.connect(network.name, signer); - - // update batch payment proxies, and batch fees - await testBatchConversionProxy.setPaymentErc20Proxy(erc20FeeProxy.address); - await testBatchConversionProxy.setPaymentEthProxy(ethereumFeeProxy.address); - await testBatchConversionProxy.setPaymentErc20ConversionProxy(testErc20ConversionProxy.address); - await testBatchConversionProxy.setPaymentEthConversionProxy(testEthConversionProxy.address); - - await testBatchConversionProxy.setBatchFee(batchFee); - await testBatchConversionProxy.setBatchConversionFee(batchConvFee); - - // set ERC20 tokens - DAI_address = localERC20AlphaArtifact.getAddress(network.name); - testERC20 = new TestERC20__factory(signer).attach(DAI_address); - - FAU_address = secondLocalERC20AlphaArtifact.getAddress(network.name); - testERC20b = new TestERC20__factory(signer).attach(FAU_address); - batchAddress = testBatchConversionProxy.address; - }); - - beforeEach(async () => { - fromDiffBalanceExpected = BigNumber.from(0); - toDiffBalanceExpected = BigNumber.from(0); - feeDiffBalanceExpected = BigNumber.from(0); - await testERC20.approve(testBatchConversionProxy.address, thousandWith18Decimal, { - from, - }); - await testERC20b.approve(testBatchConversionProxy.address, thousandWith18Decimal, { - from, - }); - // get balances of testERC20 token - fromOldBalance = await testERC20.balanceOf(from); - toOldBalance = await testERC20.balanceOf(to); - feeOldBalance = await testERC20.balanceOf(feeAddress); - - // create a default convDetail - path = [USD_hash, DAI_address]; - getConvToPayAndConvDetail(to, path, amountInFiat, feesAmountInFiat, 0, chainlinkPath); - }); - - afterEach(async () => { - // check balances of testERC20 token - checkBalancesForOneToken( - testERC20, - fromOldBalance, - toOldBalance, - feeOldBalance, - fromDiffBalanceExpected, - toDiffBalanceExpected, - feeDiffBalanceExpected, - ); - }); - /** * @notice it gets the conversions including fees to be paid, and it set the convDetail input */ @@ -180,15 +107,17 @@ describe('contract: BatchConversionPayments', () => { _maxRateTimespan: number, _chainlinkPath: ChainlinkConversionPath, ) => { - conversionToPay = await _chainlinkPath.getConversion(_requestAmount, _path); - conversionFees = await _chainlinkPath.getConversion(_feeAmount, _path); + const conversionToPayFull = await _chainlinkPath.getConversion(_requestAmount, _path); + conversionToPayBis = conversionToPayFull.result; + const conversionFeeFull = await _chainlinkPath.getConversion(_feeAmount, _path); + conversionFeesBis = conversionFeeFull.result; convDetail = { recipient: _recipient, requestAmount: _requestAmount, path: _path, paymentReference: referenceExample, feeAmount: _feeAmount, - maxToSpend: conversionToPay.result.add(conversionFees.result).toString(), + maxToSpend: conversionToPayBis.add(conversionFeesBis).toString(), maxRateTimespan: _maxRateTimespan, }; }; @@ -256,11 +185,11 @@ describe('contract: BatchConversionPayments', () => { /** * It sets the right batch conversion function, with the associated arguments format - * @param isBatchRouter allows to use batchERC20ConversionPaymentsMultiTokens with batchRouter + * @param useBatchRouter allows to use batchERC20ConversionPaymentsMultiTokens with batchRouter * @param _signer */ - const setBatchConvFunction = async (isBatchRouter: boolean, _signer: Signer) => { - if (isBatchRouter) { + const setBatchConvFunction = async (useBatchRouter: boolean, _signer: Signer) => { + if (useBatchRouter) { batchConvFunction = testBatchConversionProxy.connect(_signer).batchRouter; argTemplate = (convDetails: ConversionDetail[]) => { return [ @@ -293,8 +222,8 @@ describe('contract: BatchConversionPayments', () => { const emitOneTx = ( result: Chai.Assertion, _convDetail: ConversionDetail, - _conversionToPay = conversionToPay, - _conversionFees = conversionFees, + _conversionToPay = conversionToPayBis, + _conversionFees = conversionFeesBis, _testErc20ConversionProxy = testErc20ConversionProxy, ) => { return result.to @@ -310,9 +239,9 @@ describe('contract: BatchConversionPayments', () => { .withArgs( ethers.utils.getAddress(DAI_address), ethers.utils.getAddress(_convDetail.recipient), - _conversionToPay.result, + _conversionToPay, ethers.utils.keccak256(referenceExample), - _conversionFees.result, + _conversionFees, feeAddress, ); }; @@ -331,11 +260,11 @@ describe('contract: BatchConversionPayments', () => { const receipt = await tx.wait(); console.log(`gas consumption: `, receipt.gasUsed.toString()); } else { - await emitOneTx(expect(result), convDetail, conversionToPay, conversionFees); + await emitOneTx(expect(result), convDetail, conversionToPayBis, conversionFeesBis); } [fromDiffBalanceExpected, toDiffBalanceExpected, feeDiffBalanceExpected] = - expectedERC20Balances([conversionToPay.result], [conversionFees.result], batchConvFee); + expectedERC20Balances([conversionToPayBis], [conversionFeesBis], batchConvFee); }; /** @@ -348,15 +277,15 @@ describe('contract: BatchConversionPayments', () => { const amountInFiat2 = BigNumber.from(amountInFiat).mul(2).toString(); const feesAmountInFiat2 = BigNumber.from(feesAmountInFiat).mul(2).toString(); - const conversionToPay2 = await chainlinkPath.getConversion(amountInFiat2, path2); - const conversionFees2 = await chainlinkPath.getConversion(feesAmountInFiat2, path2); + const conversionToPayFull2 = await chainlinkPath.getConversion(amountInFiat2, path2); + const conversionFeesFull2 = await chainlinkPath.getConversion(feesAmountInFiat2, path2); let convDetail2 = Utils.deepCopy(convDetail); convDetail2.path = path2; convDetail2.requestAmount = amountInFiat2; convDetail2.feeAmount = feesAmountInFiat2; - convDetail2.maxToSpend = conversionToPay2.result.add(conversionFees2.result).toString(); + convDetail2.maxToSpend = conversionToPayFull2.result.add(conversionFeesFull2.result).toString(); // define the new arg convDetails for the function, // and conversionsToPay & conversionsFees results to calculate the expected balances @@ -366,12 +295,12 @@ describe('contract: BatchConversionPayments', () => { for (let i = 0; i < nTimes; i++) { convDetails = convDetails.concat([convDetail, convDetail2]); conversionsToPay_results = conversionsToPay_results.concat([ - conversionToPay.result, - conversionToPay2.result, + conversionToPayBis, + conversionToPayFull2.result, ]); conversionsFees_results = conversionsFees_results.concat([ - conversionFees.result, - conversionFees2.result, + conversionFeesBis, + conversionFeesFull2.result, ]); } @@ -425,16 +354,137 @@ describe('contract: BatchConversionPayments', () => { }; /** - * @notice it contains all the tests related to the ERC20 batch conversion payment, and its context required - * @param isBatchRouter allows to use the function "batchERC20ConversionPaymentsMultiTokens" + * @notice Use to test one batch payment execution for a given ERC20 batch function (no conversion). + * It tests the ERC20 transfer and fee proxy `TransferWithReferenceAndFee` events + * @param useBatchRouter allows to use a function through the batchRouter or not + * @param erc20Function selects the batch function name tested: "batchERC20PaymentsWithReference" + * or "batchERC20PaymentsMultiTokensWithReference" + */ + const batchERC20Payments = async (useBatchRouter: boolean, erc20Function: string) => { + // set up main variables + const amount = 200000; + const feeAmount = 3000; + const tokenAddress = testERC20.address; + + // Select the batch function and pay + let batchFunction: Function; + if (useBatchRouter) { + batchFunction = testBatchConversionProxy.batchRouter; + await batchFunction( + [ + { + paymentNetworkId: erc20Function === 'batchERC20PaymentsWithReference' ? 1 : 2, + conversionDetails: [], + cryptoDetails: { + tokenAddresses: [tokenAddress], + recipients: [to], + amounts: [amount], + paymentReferences: [referenceExample], + feeAmounts: [feeAmount], + }, + }, + ], + feeAddress, + ); + } else { + batchFunction = + erc20Function === 'batchERC20PaymentsWithReference' + ? testBatchConversionProxy.batchERC20PaymentsWithReference + : testBatchConversionProxy.batchERC20PaymentsMultiTokensWithReference; + await batchFunction( + erc20Function === 'batchERC20PaymentsWithReference' ? tokenAddress : [tokenAddress], + [to], + [amount], + [referenceExample], + [feeAmount], + feeAddress, + ); + } + + [fromDiffBalanceExpected, toDiffBalanceExpected, feeDiffBalanceExpected] = + expectedERC20Balances([BigNumber.from(amount)], [BigNumber.from(feeAmount)], batchFee, false); + }; + + /** + * @notice it contains all the tests related to the ERC20 batch payment, and its context required + * @param useBatchRouter allows to use the function "batchERC20ConversionPaymentsMultiTokens" * through the batchRouter or directly */ - const ERC20ConversionTestSuite = (isBatchRouter: boolean) => { - before(() => { - setBatchConvFunction(isBatchRouter, signer); + for (const useBatchRouter of [true, false]) { + before(async () => { + [from, to, feeAddress] = (await ethers.getSigners()).map((s) => s.address); + [signer1, signer4, signer4, signer4] = await ethers.getSigners(); + chainlinkPath = chainlinkConversionPath.connect(network.name, signer1); + erc20FeeProxy = await new ERC20FeeProxy__factory(signer1).deploy(); + ethereumFeeProxy = await new EthereumFeeProxy__factory(signer1).deploy(); + testErc20ConversionProxy = await new Erc20ConversionProxy__factory(signer1).deploy( + erc20FeeProxy.address, + chainlinkPath.address, + await signer1.getAddress(), + ); + testEthConversionProxy = await new EthConversionProxy__factory(signer1).deploy( + ethereumFeeProxy.address, + chainlinkPath.address, + ETH_hash, + ); + testBatchConversionProxy = batchConversionPaymentsArtifact.connect(network.name, signer1); + + // update batch payment proxies, and batch fees + await testBatchConversionProxy.setPaymentErc20Proxy(erc20FeeProxy.address); + await testBatchConversionProxy.setPaymentEthProxy(ethereumFeeProxy.address); + await testBatchConversionProxy.setPaymentErc20ConversionProxy( + testErc20ConversionProxy.address, + ); + await testBatchConversionProxy.setPaymentEthConversionProxy(testEthConversionProxy.address); + + await testBatchConversionProxy.setBatchFee(batchFee); + await testBatchConversionProxy.setBatchConversionFee(batchConvFee); + + // set ERC20 tokens + DAI_address = localERC20AlphaArtifact.getAddress(network.name); + testERC20 = new TestERC20__factory(signer1).attach(DAI_address); + + FAU_address = secondLocalERC20AlphaArtifact.getAddress(network.name); + testERC20b = new TestERC20__factory(signer1).attach(FAU_address); + batchAddress = testBatchConversionProxy.address; + + setBatchConvFunction(useBatchRouter, signer1); }); - describe(isBatchRouter ? 'Through batchRouter' : 'Without batchRouter', () => { + beforeEach(async () => { + fromDiffBalanceExpected = BigNumber.from(0); + toDiffBalanceExpected = BigNumber.from(0); + feeDiffBalanceExpected = BigNumber.from(0); + await testERC20.approve(testBatchConversionProxy.address, thousandWith18Decimal, { + from, + }); + await testERC20b.approve(testBatchConversionProxy.address, thousandWith18Decimal, { + from, + }); + // get balances of testERC20 token + fromOldBalance = await testERC20.balanceOf(from); + toOldBalance = await testERC20.balanceOf(to); + feeOldBalance = await testERC20.balanceOf(feeAddress); + + // create a default convDetail + path = [USD_hash, DAI_address]; + getConvToPayAndConvDetail(to, path, amountInFiat, feesAmountInFiat, 0, chainlinkPath); + }); + + afterEach(async () => { + // check balances of testERC20 token + checkBalancesForOneToken( + testERC20, + fromOldBalance, + toOldBalance, + feeOldBalance, + fromDiffBalanceExpected, + toDiffBalanceExpected, + feeDiffBalanceExpected, + ); + }); + + describe(useBatchRouter ? 'Through batchRouter' : 'Without batchRouter', () => { describe('batchERC20ConversionPaymentsMultiTokens with DAI', async () => { it('allows to transfer DAI tokens for USD payment', async () => { await onePaymentBatchConv(path); @@ -471,7 +521,7 @@ describe('contract: BatchConversionPayments', () => { }); it('cannot transfer if max to spend too low', async function () { - convDetail.maxToSpend = conversionToPay.result.add(conversionFees.result).sub(1).toString(); + convDetail.maxToSpend = conversionToPayBis.add(conversionFeesBis).sub(1).toString(); await expect(batchConvFunction(argTemplate([convDetail]), feeAddress)).to.be.revertedWith( 'Amount to pay is over the user limit', ); @@ -486,126 +536,48 @@ describe('contract: BatchConversionPayments', () => { }); it('Not enough allowance', async function () { - // xSigner connect to the batch function - setBatchConvFunction(isBatchRouter, xSigner); + // signer4 connect to the batch function + setBatchConvFunction(useBatchRouter, signer4); await expect(batchConvFunction(argTemplate([convDetail]), feeAddress)).to.be.revertedWith( 'Insufficient allowance for batch to pay', ); - // reset: signer connect to the batch function - setBatchConvFunction(isBatchRouter, signer); + // reset: signer1 connect to the batch function + setBatchConvFunction(useBatchRouter, signer1); }); it('Not enough funds', async function () { - // increase xSigner allowance + // increase signer4 allowance await testERC20 - .connect(xSigner) + .connect(signer4) .approve(testBatchConversionProxy.address, thousandWith18Decimal); - // xSigner connect to the batch function - setBatchConvFunction(isBatchRouter, xSigner); + // signer4 connect to the batch function + setBatchConvFunction(useBatchRouter, signer4); await expect(batchConvFunction(argTemplate([convDetail]), feeAddress)).to.be.revertedWith( 'not enough funds, including fees', ); - // reset: - // - decrease xSigner allowance - // - connect with signer account - await testERC20.connect(xSigner).approve(testBatchConversionProxy.address, '0'); - testERC20.connect(signer); - setBatchConvFunction(isBatchRouter, signer); + // reset: decrease signer4 allowance and reconnect with signer1 + await testERC20.connect(signer4).approve(testBatchConversionProxy.address, '0'); + testERC20.connect(signer1); + // reset: signer1 connect to the batch function + setBatchConvFunction(useBatchRouter, signer1); }); }); - }; - - /** - * @notice it does the set up of the main variables, it selects and pays with - * the desired batch EC20 batch function (no conversion), and it checks the ERC20 balances - * @param isBatchRouter allows to use a function through the batchRouter or not - * @param erc20Function selects the batch function name tested: "batchERC20PaymentsWithReference" - * or "batchERC20PaymentsMultiTokensWithReference" - */ - const batchERC20Payments = async (isBatchRouter: boolean, erc20Function: string) => { - // set up main variables - const amount = 200000; - const feeAmount = 3000; - const tokenAddress = testERC20.address; - - // Select the batch function and pay - let batchFunction: Function; - let result; - if (isBatchRouter) { - batchFunction = testBatchConversionProxy.batchRouter; - result = batchFunction( - [ - { - paymentNetworkId: erc20Function === 'batchERC20PaymentsWithReference' ? 1 : 2, - conversionDetails: [], - cryptoDetails: { - tokenAddresses: [tokenAddress], - recipients: [to], - amounts: [amount], - paymentReferences: [referenceExample], - feeAmounts: [feeAmount], - }, - }, - ], - feeAddress, - ); - } else { - batchFunction = - erc20Function === 'batchERC20PaymentsWithReference' - ? testBatchConversionProxy.batchERC20PaymentsWithReference - : testBatchConversionProxy.batchERC20PaymentsMultiTokensWithReference; - result = batchFunction( - erc20Function === 'batchERC20PaymentsWithReference' ? tokenAddress : [tokenAddress], - [to], - [amount], - [referenceExample], - [feeAmount], - feeAddress, - ); - } - // payment, check what is emitted - await expect(result) - .to.emit(testERC20, 'Transfer') - .withArgs(from, batchAddress, amount + feeAmount) - .to.emit(erc20FeeProxy, 'TransferWithReferenceAndFee') - .withArgs( - testERC20.address, - to, - amount, - ethers.utils.keccak256(referenceExample), - feeAmount, - feeAddress, - ) - // batch fee amount from the spender to feeAddress - .to.emit(testERC20, 'Transfer') - .withArgs( - from, - feeAddress, - amount * (batchFee / 10_000), // batch fee amount = 200000 * .5% - ); - [fromDiffBalanceExpected, toDiffBalanceExpected, feeDiffBalanceExpected] = - expectedERC20Balances([BigNumber.from(amount)], [BigNumber.from(feeAmount)], batchFee, false); - }; - /** Make sure the existing ERC20 functions from the parent contract BatchPaymentPublic.sol are still working */ - describe('Test BatchErc20Payments functions', () => { - it('batchERC20PaymentsWithReference transfers token', async function () { - await batchERC20Payments(false, 'batchERC20PaymentsWithReference'); - }); - it('with batchRouter, batchERC20PaymentsWithReference transfers token', async function () { - await batchERC20Payments(true, 'batchERC20PaymentsWithReference'); - }); + /** Make sure the existing ERC20 functions from the parent contract BatchPaymentPublic.sol are still working */ + describe('Test BatchErc20Payments functions', () => { + it(`${ + useBatchRouter ? 'with batchRouter, ' : '' + }batchERC20PaymentsWithReference transfers token`, async function () { + await batchERC20Payments(useBatchRouter, 'batchERC20PaymentsWithReference'); + }); - it('batchERC20PaymentsMultiTokensWithReference transfers token', async function () { - await batchERC20Payments(false, 'batchERC20PaymentsMultiTokensWithReference'); - }); - it('with batchRouter, batchERC20PaymentsMultiTokensWithReference transfers token', async function () { - await batchERC20Payments(true, 'batchERC20PaymentsMultiTokensWithReference'); + it(`${ + useBatchRouter ? 'with batchRouter, ' : '' + }batchERC20PaymentsMultiTokensWithReference transfers token`, async function () { + await batchERC20Payments(useBatchRouter, 'batchERC20PaymentsMultiTokensWithReference'); + }); }); - }); - - ERC20ConversionTestSuite(true); - ERC20ConversionTestSuite(false); + } }); From 30e61e939dadc2ae8e9e2df60f216e80bd863a6d Mon Sep 17 00:00:00 2001 From: olivier7delf <55892112+olivier7delf@users.noreply.github.com> Date: Wed, 17 Aug 2022 10:37:17 +0200 Subject: [PATCH 056/105] refacto tests: batch conversion Eth --- ....ts => BatchConversionEthPayments.test.ts} | 146 +++++++++--------- 1 file changed, 74 insertions(+), 72 deletions(-) rename packages/smart-contracts/test/contracts/{EthBatchConversionPayments.test.ts => BatchConversionEthPayments.test.ts} (77%) diff --git a/packages/smart-contracts/test/contracts/EthBatchConversionPayments.test.ts b/packages/smart-contracts/test/contracts/BatchConversionEthPayments.test.ts similarity index 77% rename from packages/smart-contracts/test/contracts/EthBatchConversionPayments.test.ts rename to packages/smart-contracts/test/contracts/BatchConversionEthPayments.test.ts index 6f8ec3efe..658ed1621 100644 --- a/packages/smart-contracts/test/contracts/EthBatchConversionPayments.test.ts +++ b/packages/smart-contracts/test/contracts/BatchConversionEthPayments.test.ts @@ -40,11 +40,9 @@ describe('contract: BatchConversionPayments', () => { let ethereumFeeProxy: EthereumFeeProxy; let chainlinkPath: ChainlinkConversionPath; - type ConvToPay = [BigNumber, BigNumber] & { - result: BigNumber; - oldestRateTimestamp: BigNumber; - }; - let conversionToPay: ConvToPay; + let conversionToPay: BigNumber; + let feesToPay: BigNumber; + type ConversionDetail = { recipient: string; requestAmount: BigNumberish; @@ -56,11 +54,12 @@ describe('contract: BatchConversionPayments', () => { }; let convDetail: ConversionDetail; + let tx: ContractTransaction; + let beforeEthBalanceTo: BigNumber; let beforeEthBalanceFee: BigNumber; let beforeEthBalance: BigNumber; - let feesToPay: ConvToPay; - let tx: ContractTransaction; + let amountToPayExpected: BigNumber; let feeToPayExpected: BigNumber; // amount and feeAmount are usually in fiat for conversion inputs, else in ETH @@ -69,52 +68,6 @@ describe('contract: BatchConversionPayments', () => { let inputs: Array; const pathUsdEth = [USD_hash, ETH_hash]; - before(async () => { - [from, to, feeAddress] = (await ethers.getSigners()).map((s) => s.address); - from; - [signer] = await ethers.getSigners(); - chainlinkPath = chainlinkConversionPath.connect(network.name, signer); - ethereumFeeProxy = await new EthereumFeeProxy__factory(signer).deploy(); - testEthConversionProxy = await new EthConversionProxy__factory(signer).deploy( - ethereumFeeProxy.address, - chainlinkPath.address, - ETH_hash, - ); - - testBatchConversionProxy = batchConversionPaymentsArtifact.connect(network.name, signer); - - // update batch payment proxies, and batch fees - await testBatchConversionProxy.setPaymentEthProxy(ethereumFeeProxy.address); - await testBatchConversionProxy.setPaymentEthConversionProxy(testEthConversionProxy.address); - await testBatchConversionProxy.setBatchFee(batchFee); - await testBatchConversionProxy.setBatchConversionFee(batchConvFee); - - convDetail = { - recipient: to, - requestAmount: amount, - path: pathUsdEth, - paymentReference: referenceExample, - feeAmount: feeAmount, - maxToSpend: BigNumber.from(0), - maxRateTimespan: BigNumber.from(0), - }; - - // basic setup: 1 payment - conversionToPay = await chainlinkPath.getConversion(convDetail.requestAmount, convDetail.path); - feesToPay = await chainlinkPath.getConversion(convDetail.feeAmount, convDetail.path); - }); - - beforeEach(async () => { - beforeEthBalanceTo = await provider.getBalance(to); - beforeEthBalanceFee = await provider.getBalance(feeAddress); - beforeEthBalance = await provider.getBalance(await signer.getAddress()); - - // expected balances - amountToPayExpected = conversionToPay.result; - // fees does not include batch fees yet - feeToPayExpected = feesToPay.result; - }); - /** * @notice Function batch conversion, it can be the batchRouter function, used with conversion args, * or directly batchERC20ConversionPaymentsMultiTokens @@ -128,11 +81,11 @@ describe('contract: BatchConversionPayments', () => { /** * @notice it modify the Eth batch conversion inputs if needed, depending it is * directly or through batchRouter - * @param isBatchRouter + * @param useBatchRouter * @param inputs a list of convDetail */ - const getEthConvInputs = (isBatchRouter: boolean, inputs: Array) => { - if (isBatchRouter) { + const getEthConvInputs = (useBatchRouter: boolean, inputs: Array) => { + if (useBatchRouter) { return [ { paymentNetworkId: '3', @@ -143,7 +96,7 @@ describe('contract: BatchConversionPayments', () => { amounts: [], paymentReferences: [], feeAmounts: [], - }, // not used + }, // cryptoDetails is not used }, ]; } @@ -183,24 +136,76 @@ describe('contract: BatchConversionPayments', () => { * @notice it contains all the tests related to the Eth batch payment, and its context required. * It tests the 2 functions directly, or through batchRouter function. * Functions: batchEthConversionPaymentsWithReference, and batchEthPaymentsWithReference - * @param isBatchRouter + * @param useBatchRouter */ - const EthTestSuite = (isBatchRouter: boolean) => { + for (const useBatchRouter of [true, false]) { describe(`Test ETH batch functions ${ - isBatchRouter ? 'through batchRouter' : 'without batchRouter' + useBatchRouter ? 'through batchRouter' : 'without batchRouter' }`, () => { - before(() => { - if (isBatchRouter) { + before(async () => { + [from, to, feeAddress] = (await ethers.getSigners()).map((s) => s.address); + from; + [signer] = await ethers.getSigners(); + chainlinkPath = chainlinkConversionPath.connect(network.name, signer); + ethereumFeeProxy = await new EthereumFeeProxy__factory(signer).deploy(); + testEthConversionProxy = await new EthConversionProxy__factory(signer).deploy( + ethereumFeeProxy.address, + chainlinkPath.address, + ETH_hash, + ); + + testBatchConversionProxy = batchConversionPaymentsArtifact.connect(network.name, signer); + + // update batch payment proxies, and batch fees + await testBatchConversionProxy.setPaymentEthProxy(ethereumFeeProxy.address); + await testBatchConversionProxy.setPaymentEthConversionProxy(testEthConversionProxy.address); + await testBatchConversionProxy.setBatchFee(batchFee); + await testBatchConversionProxy.setBatchConversionFee(batchConvFee); + + convDetail = { + recipient: to, + requestAmount: amount, + path: pathUsdEth, + paymentReference: referenceExample, + feeAmount: feeAmount, + maxToSpend: BigNumber.from(0), + maxRateTimespan: BigNumber.from(0), + }; + + // basic setup: 1 payment + const conversionToPayFull = await chainlinkPath.getConversion( + convDetail.requestAmount, + convDetail.path, + ); + conversionToPay = conversionToPayFull.result; + const feesToPayFull = await chainlinkPath.getConversion( + convDetail.feeAmount, + convDetail.path, + ); + feesToPay = feesToPayFull.result; + + if (useBatchRouter) { batchConvFunction = testBatchConversionProxy.batchRouter; } else { batchConvFunction = testBatchConversionProxy.batchEthConversionPaymentsWithReference; } }); + beforeEach(async () => { + beforeEthBalanceTo = await provider.getBalance(to); + beforeEthBalanceFee = await provider.getBalance(feeAddress); + beforeEthBalance = await provider.getBalance(await signer.getAddress()); + + // expected balances, it can be modified for each test + amountToPayExpected = conversionToPay; + // fees does not include batch fees yet + feeToPayExpected = feesToPay; + }); + describe('success functions', () => { it('batchEthConversionPaymentsWithReference transfer 1 payment in ethers denominated in USD', async function () { inputs = [convDetail]; - tx = await batchConvFunction(getEthConvInputs(isBatchRouter, inputs), feeAddress, { + tx = await batchConvFunction(getEthConvInputs(useBatchRouter, inputs), feeAddress, { value: BigNumber.from('100000000000000000'), }); await checkEthBalances(amountToPayExpected, feeToPayExpected); @@ -210,7 +215,7 @@ describe('contract: BatchConversionPayments', () => { amountToPayExpected = amountToPayExpected.mul(3); feeToPayExpected = feeToPayExpected.mul(3); inputs = [convDetail, convDetail, convDetail]; - tx = await batchConvFunction(getEthConvInputs(isBatchRouter, inputs), feeAddress, { + tx = await batchConvFunction(getEthConvInputs(useBatchRouter, inputs), feeAddress, { value: BigNumber.from('100000000000000000'), }); await checkEthBalances(amountToPayExpected, feeToPayExpected); @@ -233,7 +238,7 @@ describe('contract: BatchConversionPayments', () => { feeToPayExpected = eurFeesToPay.result.add(feeToPayExpected.mul(2)); inputs = [convDetail, EurConvDetail, convDetail]; - tx = await batchConvFunction(getEthConvInputs(isBatchRouter, inputs), feeAddress, { + tx = await batchConvFunction(getEthConvInputs(useBatchRouter, inputs), feeAddress, { value: BigNumber.from('100000000000000000'), }); await checkEthBalances(amountToPayExpected, feeToPayExpected); @@ -251,7 +256,7 @@ describe('contract: BatchConversionPayments', () => { paymentReferences: [referenceExample], feeAmounts: [feeAmount], // in ETH }; - if (isBatchRouter) { + if (useBatchRouter) { await testBatchConversionProxy.batchRouter( [ { @@ -295,7 +300,7 @@ describe('contract: BatchConversionPayments', () => { describe('revert functions', () => { it('batchEthConversionPaymentsWithReference transfer FAIL: not enough funds', async function () { await expect( - batchConvFunction(getEthConvInputs(isBatchRouter, [convDetail]), feeAddress, { + batchConvFunction(getEthConvInputs(useBatchRouter, [convDetail]), feeAddress, { value: 10000, }), ).to.be.revertedWith('paymentProxy transferExactEthWithReferenceAndFee failed'); @@ -311,7 +316,7 @@ describe('contract: BatchConversionPayments', () => { // it contains the function being just executed, and still processing let batchEthPayments; - if (isBatchRouter) { + if (useBatchRouter) { batchEthPayments = testBatchConversionProxy.batchRouter( [ { @@ -337,8 +342,5 @@ describe('contract: BatchConversionPayments', () => { }); }); }); - }; - - EthTestSuite(true); - EthTestSuite(false); + } }); From d7e0b2554dda3c9cd35fe258b4730a48d9b04742 Mon Sep 17 00:00:00 2001 From: olivier7delf <55892112+olivier7delf@users.noreply.github.com> Date: Wed, 17 Aug 2022 15:16:46 +0200 Subject: [PATCH 057/105] test erc20 can be run multiple time - revoke these approvals --- .../src/payment/any-to-erc20-proxy.ts | 9 +---- .../test/payment/erc20.test.ts | 39 ++++++++++++------- 2 files changed, 26 insertions(+), 22 deletions(-) diff --git a/packages/payment-processor/src/payment/any-to-erc20-proxy.ts b/packages/payment-processor/src/payment/any-to-erc20-proxy.ts index 6fbcb32d0..ac45dc9c0 100644 --- a/packages/payment-processor/src/payment/any-to-erc20-proxy.ts +++ b/packages/payment-processor/src/payment/any-to-erc20-proxy.ts @@ -133,13 +133,8 @@ export function prepAnyToErc20ProxyRequest( // Check request validateConversionFeeProxyRequest(request, path, amount, feeAmountOverride); - const { - paymentReference, - paymentAddress, - feeAddress, - feeAmount, - maxRateTimespan, - } = getRequestPaymentValues(request); + const { paymentReference, paymentAddress, feeAddress, feeAmount, maxRateTimespan } = + getRequestPaymentValues(request); const amountToPay = padAmountForChainlink(getAmountToPay(request, amount), requestCurrency); const feeToPay = padAmountForChainlink(feeAmountOverride || feeAmount || 0, requestCurrency); diff --git a/packages/payment-processor/test/payment/erc20.test.ts b/packages/payment-processor/test/payment/erc20.test.ts index a3dc06f48..8f6e42579 100644 --- a/packages/payment-processor/test/payment/erc20.test.ts +++ b/packages/payment-processor/test/payment/erc20.test.ts @@ -14,15 +14,24 @@ import { hasErc20Approval, checkErc20Allowance, } from '../../src/payment/erc20'; +import { + getProxyAddress, + revokeErc20Approval, +} from '@requestnetwork/payment-processor/src/payment/utils'; +import { Erc20PaymentNetwork } from '@requestnetwork/payment-detection'; /* eslint-disable @typescript-eslint/no-unused-expressions */ const erc20ContractAddress = '0x9FBDa871d559710256a2502A2517b794B482Db40'; const paymentAddress = '0xf17f52151EbEF6C7334FAD080c5704D77216b732'; const feeAddress = '0xC5fdf4076b8F3A5357c5E395ab970B5B54098Fef'; +const feeProxyAddress = erc20FeeProxyArtifact.getAddress('private'); const mnemonic = 'candy maple cake sugar pudding cream honey rich smooth crumble sweet treat'; const provider = new providers.JsonRpcProvider('http://localhost:8545'); const wallet = Wallet.fromMnemonic(mnemonic).connect(provider); +const otherWallet = new Wallet( + '0x8d5366123cb560bb606379f90a0bfd4769eecc0557f1b362dcae9012b548b1e5', +).connect(provider); const erc20FeeProxyRequest: ClientTypes.IRequestData = { balance: { @@ -111,6 +120,19 @@ const erc20ProxyRequest: ClientTypes.IRequestData = { }; describe('hasErc20approval & approveErc20', () => { + beforeAll(async () => { + // revoke erc20FeeProxy approval + await revokeErc20Approval(feeProxyAddress, erc20ContractAddress, otherWallet); + // revoke erc20Proxy approval + await revokeErc20Approval( + getProxyAddress( + erc20ProxyRequest, + Erc20PaymentNetwork.ERC20ProxyPaymentDetector.getDeploymentInformation, + ), + erc20ContractAddress, + otherWallet, + ); + }); describe('ERC20 fee proxy payment network', () => { it('should consider override parameters', async () => { const spy = jest.fn(); @@ -128,19 +150,12 @@ describe('hasErc20approval & approveErc20', () => { wallet.sendTransaction = originalSendTransaction; }); it('can check and approve', async () => { - // use another address so it doesn't mess with other tests. - const otherWallet = new Wallet( - '0x8d5366123cb560bb606379f90a0bfd4769eecc0557f1b362dcae9012b548b1e5', - ).connect(provider); - const feeProxyAddres = erc20FeeProxyArtifact.getAddress('private'); let hasApproval = await hasErc20Approval(erc20FeeProxyRequest, otherWallet.address, provider); - // Warning: this test can run only once! - // 'already has approval' expect(hasApproval).toBe(false); expect( await checkErc20Allowance( otherWallet.address, - feeProxyAddres, + feeProxyAddress, provider, erc20ContractAddress, 1, @@ -153,7 +168,7 @@ describe('hasErc20approval & approveErc20', () => { expect( await checkErc20Allowance( otherWallet.address, - feeProxyAddres, + feeProxyAddress, provider, erc20ContractAddress, 1, @@ -179,13 +194,7 @@ describe('hasErc20approval & approveErc20', () => { wallet.sendTransaction = originalSendTransaction; }); it('can check and approve', async () => { - // use another address so it doesn't mess with other tests. - const otherWallet = new Wallet( - '0x8d5366123cb560bb606379f90a0bfd4769eecc0557f1b362dcae9012b548b1e5', - ).connect(provider); let hasApproval = await hasErc20Approval(erc20ProxyRequest, otherWallet.address, provider); - // Warning: this test can run only once! - // 'already has approval' expect(hasApproval).toBe(false); await approveErc20(erc20ProxyRequest, otherWallet); hasApproval = await hasErc20Approval(erc20ProxyRequest, otherWallet.address, provider); From bff175f81e1c5a2981bb5b832e96b005d9f0095e Mon Sep 17 00:00:00 2001 From: olivier7delf <55892112+olivier7delf@users.noreply.github.com> Date: Wed, 17 Aug 2022 16:07:46 +0200 Subject: [PATCH 058/105] test swap-erc-20-fee-proxy revoke approval --- .../test/payment/swap-erc20-fee-proxy.test.ts | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/packages/payment-processor/test/payment/swap-erc20-fee-proxy.test.ts b/packages/payment-processor/test/payment/swap-erc20-fee-proxy.test.ts index 5a21bbe3a..0a4ac5b7a 100644 --- a/packages/payment-processor/test/payment/swap-erc20-fee-proxy.test.ts +++ b/packages/payment-processor/test/payment/swap-erc20-fee-proxy.test.ts @@ -13,6 +13,8 @@ import { getErc20Balance } from '../../src/payment/erc20'; import { approveErc20ForSwapToPayIfNeeded } from '../../src/payment/swap-erc20'; import { ERC20__factory } from '@requestnetwork/smart-contracts/types'; import { ISwapSettings, swapErc20FeeProxyRequest } from '../../src/payment/swap-erc20-fee-proxy'; +import { erc20SwapToPayArtifact } from '@requestnetwork/smart-contracts'; +import { revokeErc20Approval } from '@requestnetwork/payment-processor/src/payment/utils'; /* eslint-disable no-magic-numbers */ /* eslint-disable @typescript-eslint/no-unused-expressions */ @@ -77,6 +79,14 @@ const validSwapSettings: ISwapSettings = { }; describe('swap-erc20-fee-proxy', () => { + beforeAll(async () => { + // revoke erc20SwapToPay approval + await revokeErc20Approval( + erc20SwapToPayArtifact.getAddress(validRequest.currencyInfo.network!), + alphaErc20Address, + wallet.provider, + ); + }); describe('encodeSwapErc20FeeRequest', () => { it('should throw an error if the request is not erc20', async () => { const request = Utils.deepCopy(validRequest) as ClientTypes.IRequestData; @@ -137,7 +147,8 @@ describe('swap-erc20-fee-proxy', () => { }, ); expect(spy).toHaveBeenCalledWith({ - data: '0x8d09fe2b000000000000000000000000f17f52151ebef6c7334fad080c5704d77216b732000000000000000000000000000000000000000000000000000000000000006400000000000000000000000000000000000000000000000000000000000000cc000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001600000000000000000000000000000000000000000000000000000000000000002000000000000000000000000c5fdf4076b8f3a5357c5e395ab970b5b54098fef000000000000000000000000000000000000000000000000000000009af4c3db000000000000000000000000000000000000000000000000000000000000000200000000000000000000000038cf23c52bb4b13f051aec09580a2de845a7fa350000000000000000000000009fbda871d559710256a2502a2517b794b482db40000000000000000000000000000000000000000000000000000000000000000886dfbccad783599a000000000000000000000000000000000000000000000000', + data: + '0x8d09fe2b000000000000000000000000f17f52151ebef6c7334fad080c5704d77216b732000000000000000000000000000000000000000000000000000000000000006400000000000000000000000000000000000000000000000000000000000000cc000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001600000000000000000000000000000000000000000000000000000000000000002000000000000000000000000c5fdf4076b8f3a5357c5e395ab970b5b54098fef000000000000000000000000000000000000000000000000000000009af4c3db000000000000000000000000000000000000000000000000000000000000000200000000000000000000000038cf23c52bb4b13f051aec09580a2de845a7fa350000000000000000000000009fbda871d559710256a2502a2517b794b482db40000000000000000000000000000000000000000000000000000000000000000886dfbccad783599a000000000000000000000000000000000000000000000000', gasPrice: '20000000000', to: '0xA4392264a2d8c998901D10C154C91725b1BF0158', value: 0, From b6b854c5507cd14dd9dc78fabe0b3a5b19010d72 Mon Sep 17 00:00:00 2001 From: olivier7delf <55892112+olivier7delf@users.noreply.github.com> Date: Thu, 18 Aug 2022 15:00:54 +0200 Subject: [PATCH 059/105] refacto any-to-erc20-proxy to create the function checkRequestAndGetPathAndCurrency --- .../src/payment/any-to-erc20-proxy.ts | 52 ++++++++++++++----- 1 file changed, 39 insertions(+), 13 deletions(-) diff --git a/packages/payment-processor/src/payment/any-to-erc20-proxy.ts b/packages/payment-processor/src/payment/any-to-erc20-proxy.ts index ac45dc9c0..71c740a24 100644 --- a/packages/payment-processor/src/payment/any-to-erc20-proxy.ts +++ b/packages/payment-processor/src/payment/any-to-erc20-proxy.ts @@ -1,6 +1,10 @@ import { constants, ContractTransaction, Signer, providers, BigNumberish, BigNumber } from 'ethers'; -import { CurrencyManager, UnsupportedCurrencyError } from '@requestnetwork/currency'; +import { + CurrencyDefinition, + CurrencyManager, + UnsupportedCurrencyError, +} from '@requestnetwork/currency'; import { AnyToERC20PaymentDetector } from '@requestnetwork/payment-detection'; import { Erc20ConversionProxy__factory } from '@requestnetwork/smart-contracts/types'; import { ClientTypes, RequestLogicTypes } from '@requestnetwork/types'; @@ -84,20 +88,15 @@ export function encodePayAnyToErc20ProxyRequest( ]); } -export function prepAnyToErc20ProxyRequest( +/** + * It checks paymentSettings values, it get request's path and requestCurrency + */ +export function checkRequestAndGetPathAndCurrency( request: ClientTypes.IRequestData, paymentSettings: IConversionPaymentSettings, amount?: BigNumberish, feeAmountOverride?: BigNumberish, -): { - path: string[]; - paymentReference: string; - paymentAddress: string; - feeAddress: string | undefined; - maxRateTimespan: string | undefined; - amountToPay: BigNumber; - feeToPay: BigNumber; -} { +): { path: string[]; requestCurrency: CurrencyDefinition } { if (!paymentSettings.currency) { throw new Error('currency must be provided in the paymentSettings'); } @@ -132,9 +131,36 @@ export function prepAnyToErc20ProxyRequest( // Check request validateConversionFeeProxyRequest(request, path, amount, feeAmountOverride); + return { path, requestCurrency }; +} +export function prepAnyToErc20ProxyRequest( + request: ClientTypes.IRequestData, + paymentSettings: IConversionPaymentSettings, + amount?: BigNumberish, + feeAmountOverride?: BigNumberish, +): { + path: string[]; + paymentReference: string; + paymentAddress: string; + feeAddress: string | undefined; + maxRateTimespan: string | undefined; + amountToPay: BigNumber; + feeToPay: BigNumber; +} { + const { path, requestCurrency } = checkRequestAndGetPathAndCurrency( + request, + paymentSettings, + amount, + feeAmountOverride, + ); - const { paymentReference, paymentAddress, feeAddress, feeAmount, maxRateTimespan } = - getRequestPaymentValues(request); + const { + paymentReference, + paymentAddress, + feeAddress, + feeAmount, + maxRateTimespan, + } = getRequestPaymentValues(request); const amountToPay = padAmountForChainlink(getAmountToPay(request, amount), requestCurrency); const feeToPay = padAmountForChainlink(feeAmountOverride || feeAmount || 0, requestCurrency); From 8876f913da71bd2074cc97db12eeba49366277c9 Mon Sep 17 00:00:00 2001 From: olivier7delf <55892112+olivier7delf@users.noreply.github.com> Date: Thu, 18 Aug 2022 15:02:11 +0200 Subject: [PATCH 060/105] prettier --- .../payment-processor/src/payment/any-to-erc20-proxy.ts | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/packages/payment-processor/src/payment/any-to-erc20-proxy.ts b/packages/payment-processor/src/payment/any-to-erc20-proxy.ts index 71c740a24..c25c28d91 100644 --- a/packages/payment-processor/src/payment/any-to-erc20-proxy.ts +++ b/packages/payment-processor/src/payment/any-to-erc20-proxy.ts @@ -154,13 +154,8 @@ export function prepAnyToErc20ProxyRequest( feeAmountOverride, ); - const { - paymentReference, - paymentAddress, - feeAddress, - feeAmount, - maxRateTimespan, - } = getRequestPaymentValues(request); + const { paymentReference, paymentAddress, feeAddress, feeAmount, maxRateTimespan } = + getRequestPaymentValues(request); const amountToPay = padAmountForChainlink(getAmountToPay(request, amount), requestCurrency); const feeToPay = padAmountForChainlink(feeAmountOverride || feeAmount || 0, requestCurrency); From 503e6eaeb79e2f333267a9b1e1ca3623ea1fc0e4 Mon Sep 17 00:00:00 2001 From: olivier7delf <55892112+olivier7delf@users.noreply.github.com> Date: Thu, 18 Aug 2022 15:03:40 +0200 Subject: [PATCH 061/105] prettier test swap --- .../test/payment/swap-erc20-fee-proxy.test.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/payment-processor/test/payment/swap-erc20-fee-proxy.test.ts b/packages/payment-processor/test/payment/swap-erc20-fee-proxy.test.ts index 0a4ac5b7a..ece65849d 100644 --- a/packages/payment-processor/test/payment/swap-erc20-fee-proxy.test.ts +++ b/packages/payment-processor/test/payment/swap-erc20-fee-proxy.test.ts @@ -147,8 +147,7 @@ describe('swap-erc20-fee-proxy', () => { }, ); expect(spy).toHaveBeenCalledWith({ - data: - '0x8d09fe2b000000000000000000000000f17f52151ebef6c7334fad080c5704d77216b732000000000000000000000000000000000000000000000000000000000000006400000000000000000000000000000000000000000000000000000000000000cc000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001600000000000000000000000000000000000000000000000000000000000000002000000000000000000000000c5fdf4076b8f3a5357c5e395ab970b5b54098fef000000000000000000000000000000000000000000000000000000009af4c3db000000000000000000000000000000000000000000000000000000000000000200000000000000000000000038cf23c52bb4b13f051aec09580a2de845a7fa350000000000000000000000009fbda871d559710256a2502a2517b794b482db40000000000000000000000000000000000000000000000000000000000000000886dfbccad783599a000000000000000000000000000000000000000000000000', + data: '0x8d09fe2b000000000000000000000000f17f52151ebef6c7334fad080c5704d77216b732000000000000000000000000000000000000000000000000000000000000006400000000000000000000000000000000000000000000000000000000000000cc000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001600000000000000000000000000000000000000000000000000000000000000002000000000000000000000000c5fdf4076b8f3a5357c5e395ab970b5b54098fef000000000000000000000000000000000000000000000000000000009af4c3db000000000000000000000000000000000000000000000000000000000000000200000000000000000000000038cf23c52bb4b13f051aec09580a2de845a7fa350000000000000000000000009fbda871d559710256a2502a2517b794b482db40000000000000000000000000000000000000000000000000000000000000000886dfbccad783599a000000000000000000000000000000000000000000000000', gasPrice: '20000000000', to: '0xA4392264a2d8c998901D10C154C91725b1BF0158', value: 0, From 24862d169bef895eb1f5cdf7c03d24d5cf3aa451 Mon Sep 17 00:00:00 2001 From: olivier7delf <55892112+olivier7delf@users.noreply.github.com> Date: Thu, 18 Aug 2022 15:04:17 +0200 Subject: [PATCH 062/105] batch conversion proxy functions and approvals --- .../src/payment/batch-proxy-conv.ts | 491 ++++++------------ .../src/payment/batch-proxy.ts | 2 +- 2 files changed, 161 insertions(+), 332 deletions(-) diff --git a/packages/payment-processor/src/payment/batch-proxy-conv.ts b/packages/payment-processor/src/payment/batch-proxy-conv.ts index a0c794be1..dceb3b871 100644 --- a/packages/payment-processor/src/payment/batch-proxy-conv.ts +++ b/packages/payment-processor/src/payment/batch-proxy-conv.ts @@ -1,25 +1,23 @@ -import { ContractTransaction, Signer, providers, constants, BigNumber, BigNumberish } from 'ethers'; -import { batchPaymentsArtifact } from '@requestnetwork/smart-contracts'; -import { BatchConversionPayments__factory, BatchPayments__factory } from '@requestnetwork/smart-contracts/types'; -import { ClientTypes, PaymentTypes } from '@requestnetwork/types'; +import { ContractTransaction, Signer, providers, BigNumber, BigNumberish } from 'ethers'; +import { batchConversionPaymentsArtifact } from '@requestnetwork/smart-contracts'; +import { BatchConversionPayments__factory } from '../../../smart-contracts/types'; +import { ClientTypes } from '@requestnetwork/types'; import { ITransactionOverrides } from './transaction-overrides'; import { comparePnTypeAndVersion, - getAmountToPay, getPaymentNetworkExtension, getProvider, getRequestPaymentValues, getSigner, - validateErc20FeeProxyRequest, } from './utils'; -import { validateEthFeeProxyRequest } from './eth-fee-proxy'; import { IPreparedTransaction } from './prepared-transaction'; -import { checkErc20Allowance, encodeApproveAnyErc20 } from './erc20'; import { IConversionPaymentSettings } from './index'; -import { prepAnyToErc20ProxyRequest } from './any-to-erc20-proxy'; +import { checkRequestAndGetPathAndCurrency } from './any-to-erc20-proxy'; +import { getBatchArgs } from './batch-proxy'; +import { checkErc20Allowance, encodeApproveAnyErc20 } from './erc20'; -// used by batch smart contract -export type ConversionDetail = { +// Types used by batch conversion smart contract +type ConversionDetail = { recipient: string; requestAmount: BigNumberish; path: string[]; @@ -29,7 +27,7 @@ export type ConversionDetail = { maxRateTimespan: BigNumberish; }; -export type CryptoDetails = { +type CryptoDetails = { tokenAddresses: Array; recipients: Array; amounts: Array; @@ -37,402 +35,228 @@ export type CryptoDetails = { feeAmounts: Array; }; -const emptyConversionDetail = { - recipient: '', - requestAmount: 0, - path: [], - paymentReference: '', - feeAmount: 0, - maxToSpend: 0, - maxRateTimespan: 0, -}; - -const emptyCryptoDetails = { - tokenAddresses: [], - recipients: [], - amounts: [], - paymentReferences: [], - feeAmounts: [], -}; - -export type MetaDetail = { +type MetaDetail = { paymentNetworkId: number; conversionDetails: ConversionDetail[]; cryptoDetails: CryptoDetails; }; -// used only for batch payment processor +/** + * Type used by batch conversion payment processor + * It contains requests, paymentSettings, amount and feeAmount, + * having the same PN, version, and batchFee + */ type EnrichedRequest = { - paymentNetworkId: number; // ref in batchConversionPayment.sol - requests: ClientTypes.IRequestData[]; - paymentSettings?: IConversionPaymentSettings[]; - amount?: BigNumberish[]; - feeAmount?: BigNumberish[]; - version?: string; - batchFee?: number; + paymentNetworkId: 0 | 2; // ref in batchConversionPayment.sol + request: ClientTypes.IRequestData; + paymentSettings?: IConversionPaymentSettings; + amount?: BigNumberish; + feeAmount?: BigNumberish; }; -/** TODO UPDATE - * ERC20 Batch Proxy payment details: - * batch of request with the same payment network type: ERC20 - * batch of request with the same payment network version - * 2 modes available: single token or multi tokens - * It requires batch proxy's approval - * - * Eth Batch Proxy payment details: - * batch of request with the same payment network type - * batch of request with the same payment network version - * -> Eth batch proxy accepts requests with 2 id: ethProxy and ethFeeProxy - * but only call ethFeeProxy. It can impact payment detection - */ - /** * Processes a transaction to pay a batch of requests with an ERC20 or ETH currency that is different from the request currency (eg. fiat). - * The payment is made by the ERC20, or ETH fee proxy contract. + * The payment is made through ERC20 or ERC20Conversion proxies + * It can be used with a Multisig contract * @param enrichedRequests List of EnrichedRequest + * @param version of the batch conversion proxy * @param signerOrProvider the Web3 provider, or signer. Defaults to window.ethereum. * @param overrides optionally, override default transaction values, like gas. + * @dev we only implement batchRouter using the ERC20 normal and conversion functions: + * batchERC20ConversionPaymentsMultiTokens, and batchERC20PaymentsMultiTokensWithReference. + * It implies that paymentNetworkId take only theses values: 0 or 2 + * Next steps: + * - Enable ETH payment: normal and conversion + * - Enable gas optimizaton: implement the others batch functions */ -export async function payBatchConvProxyRequest( +export async function payBatchConversionProxyRequest( enrichedRequests: EnrichedRequest[], + version: string, signerOrProvider: providers.Provider | Signer = getProvider(), overrides?: ITransactionOverrides, ): Promise { - const { data, to, value } = prepareBatchConvPaymentTransaction(enrichedRequests); + const { data, to, value } = prepareBatchConversionPaymentTransaction(enrichedRequests, version); const signer = getSigner(signerOrProvider); return signer.sendTransaction({ data, to, value, ...overrides }); } /** - * Prepate the transaction to pay a batch of requests through the batch proxy contract, can be used with a Multisig contract. - * Requests paymentType must be "ETH" or "ERC20" + * Prepate the transaction to pay a batch of requests through the batch conversion proxy contract, + * it can be used with a Multisig contract. */ -export function prepareBatchConvPaymentTransaction( +export function prepareBatchConversionPaymentTransaction( enrichedRequests: EnrichedRequest[], + version: string, ): IPreparedTransaction { - // we only implement batchRouter and the ERC20 normal and conversion functions - // batchERC20ConversionPaymentsMultiTokens, and batchERC20PaymentsMultiTokensWithReference - // => paymentNetworkId take only theses values: 0 or 2 - // later, to do gas optimizaton, we will implement the others batch functions, - - // revoir cette partie, il faut créer deux obj dict, un avec paymentNetworkId 0 (puis 2) ayant toutes les requests, feeAddress - let clusteredEnrichedRequest = [] - for (let i = 0; i < enrichedRequests.length; i++) { - if (enrichedRequests[i].paymentNetworkId === 0) { - clusteredEnrichedRequest.push({paymentNetworkId: 0, request: enrichedRequests[i], feeAddress: enrichedRequests[i].}) - } else if (enrichedRequests[i].paymentNetworkId === 2) { - clusteredEnrichedRequest.push({paymentNetworkId: 0, request: enrichedRequests[i]}) - } - } - // create MetaDetails input - const metaDetails = []; - let ERC20ConversionInput: ConversionDetail[]; - let ERC20Input: CryptoDetails; - for (let i = 0; i < enrichedRequests.length; i++) { - const enrichedRequest = enrichedRequests[i]; - if (enrichedRequest.paymentNetworkId === 0) { - ERC20ConversionInput = batchERC20ConversionPaymentsMultiTokensInput(enrichedRequest); - metaDetails.push({ - paymentNetworkId: 0, - conversionDetails: ERC20ConversionInput, - cryptoDetails: emptyCryptoDetails, - }); - } else if (enrichedRequests[i].paymentNetworkId === 2) { - ERC20Input = batchERC20PaymentsMultiTokensWithReferenceInput(enrichedRequest); - metaDetails.push({ - paymentNetworkId: 2, - conversionDetails: [emptyConversionDetail], - cryptoDetails: ERC20Input, - }); - } else { - throw new Error('paymentNetworkId must be equal to 0 or 2'); - } - } - console.log('metaDetails', metaDetails); - metaDetails; - - const encodedTx = encodePayBatchRequest(requests); - const proxyAddress = getBatchProxyAddress(requests[0], version); - // let totalAmount = 0; - - // if (requests[0].currencyInfo.type === 'ETH') { - // const { amountsToPay, feesToPay } = getBatchArgs(requests); - - // const amountToPay = amountsToPay.reduce((sum, current) => sum.add(current), BigNumber.from(0)); - // const batchFeeToPay = BigNumber.from(amountToPay).mul(batchFee).div(1000); - // const feeToPay = feesToPay.reduce( - // (sum, current) => sum.add(current), - // BigNumber.from(batchFeeToPay), - // ); - // totalAmount = amountToPay.add(feeToPay).toNumber(); - } - + const encodedTx = encodePayBatchConversionRequest(enrichedRequests); + const proxyAddress = getBatchConversionProxyAddress(enrichedRequests[0].request, version); return { data: encodedTx, to: proxyAddress, - value: totalAmount, - }; -} - -function batchERC20ConversionPaymentsMultiTokensInput( - enrichedRequest: EnrichedRequest, -): ConversionDetail[] { - // encodePayAnyToErc20ProxyRequest look what they check - for (let i = 0; i < enrichedRequest.requests.length; i++) { - const { - path, - paymentReference, - paymentAddress, - feeAddress, - maxRateTimespan, - amountToPay, - feeToPay, - } = prepAnyToErc20ProxyRequest(request, enrichedRequest.paymentSettings, enrichedRequest.amount, enrichedRequest.feeAmountOverride); - - } - - return []; -} - -function batchERC20PaymentsMultiTokensWithReferenceInput(enrichedRequest: EnrichedRequest): CryptoDetails { - return { - tokenAddresses: [], - recipients: [], - amounts: [], - paymentReferences: [], - feeAmounts: [], + value: 0, }; } -function samePaymentNetwork(requests: ClientTypes.IRequestData[]): void { - const pn = getPaymentNetworkExtension(requests[0]); - for (let i = 0; i < requests.length; i++) { - validateErc20FeeProxyRequest(requests[i]); - if (!comparePnTypeAndVersion(pn, requests[i])) { - throw new Error(`Every payment network type and version must be identical`); - } - } -} - /** - * Encodes the call to pay a batch of requests through the ERC20Bacth or ETHBatch proxy contract, - * can be used with a Multisig contract. - * @param requests list of ECR20 requests to pay - * @dev pn version of the requests is checked to avoid paying with two differents proxies (e.g: erc20proxy v1 and v2) + * Encodes the call to pay a batch conversion of requests through ERC20 or ERC20Conversion proxies + * It can be used with a Multisig contract. + * @param enrichedRequests list of ECR20 requests to pay */ - export function encodePayBatchConversion(metaDetails: MetaDetail[]): string { +export function encodePayBatchConversionRequest(enrichedRequests: EnrichedRequest[]): string { + const extension = getPaymentNetworkExtension(enrichedRequests[0].request); + if (!extension) { + throw new Error('no payment network found'); + } - const proxyContract = BatchConversionPayments__factory.createInterface(); + const { feeAddress } = extension.values; + const metaDetails: MetaDetail[] = []; - + // variable and constant to get info about each payment network (pn) + let pn0FirstRequest: ClientTypes.IRequestData | undefined; + const pn2requests = []; - if (isMultiTokens) { - return proxyContract.encodeFunctionData('batchERC20PaymentsMultiTokensWithReference', [ - tokenAddresses, - paymentAddresses, - amountsToPay, - paymentReferences, - feesToPay, - feeAddressUsed, - ]); - } else { - return proxyContract.encodeFunctionData('batchERC20PaymentsWithReference', [ - tokenAddresses[0], - paymentAddresses, - amountsToPay, - paymentReferences, - feesToPay, - feeAddressUsed, - ]); - } - } else { - tokenAddresses; - return proxyContract.encodeFunctionData('batchEthPaymentsWithReference', [ - paymentAddresses, - amountsToPay, - paymentReferences, - feesToPay, - feeAddressUsed, - ]); - } -} + const conversionDetails: ConversionDetail[] = []; -/** - * Encodes the call to pay a batch of requests through the ERC20Bacth or ETHBatch proxy contract, - * can be used with a Multisig contract. - * @param requests list of ECR20 requests to pay - * @dev pn version of the requests is checked to avoid paying with two differents proxies (e.g: erc20proxy v1 and v2) - */ -export function encodePayBatchRequest(requests: ClientTypes.IRequestData[]): string { - const { - tokenAddresses, - paymentAddresses, - amountsToPay, - paymentReferences, - feesToPay, - feeAddressUsed, - } = getBatchArgs(requests); - - const proxyContract = BatchPayments__factory.createInterface(); - - if (requests[0].currencyInfo.type === 'ERC20') { - let isMultiTokens = false; - for (let i = 0; tokenAddresses.length; i++) { - if (tokenAddresses[0] !== tokenAddresses[i]) { - isMultiTokens = true; - break; + for (let i = 0; i < enrichedRequests.length; i++) { + if (enrichedRequests[i].paymentNetworkId === 0) { + // set pn0FirstRequest only if it is undefined + pn0FirstRequest = pn0FirstRequest ?? enrichedRequests[i].request; + if ( + !comparePnTypeAndVersion( + getPaymentNetworkExtension(pn0FirstRequest), + enrichedRequests[i].request, + ) + ) { + throw new Error(`Every payment network type and version must be identical`); } - } + conversionDetails.push(getInputConversionDetail(enrichedRequests[i])); + } else if (enrichedRequests[i].paymentNetworkId === 2) { + pn2requests.push(enrichedRequests[i].request); - const pn = getPaymentNetworkExtension(requests[0]); - for (let i = 0; i < requests.length; i++) { - validateErc20FeeProxyRequest(requests[i]); - if (!comparePnTypeAndVersion(pn, requests[i])) { + if (!comparePnTypeAndVersion(getPaymentNetworkExtension(pn2requests[0]), pn2requests[-1])) { throw new Error(`Every payment network type and version must be identical`); } } - - if (isMultiTokens) { - return proxyContract.encodeFunctionData('batchERC20PaymentsMultiTokensWithReference', [ - tokenAddresses, - paymentAddresses, - amountsToPay, - paymentReferences, - feesToPay, - feeAddressUsed, - ]); - } else { - return proxyContract.encodeFunctionData('batchERC20PaymentsWithReference', [ - tokenAddresses[0], - paymentAddresses, - amountsToPay, - paymentReferences, - feesToPay, - feeAddressUsed, - ]); - } - } else { - tokenAddresses; - return proxyContract.encodeFunctionData('batchEthPaymentsWithReference', [ - paymentAddresses, - amountsToPay, - paymentReferences, - feesToPay, - feeAddressUsed, - ]); } + // get cryptoDetails values + const { tokenAddresses, paymentAddresses, amountsToPay, paymentReferences, feesToPay } = + getBatchArgs(pn2requests); + + // add conversionDetails to metaDetails + metaDetails.push({ + paymentNetworkId: 0, + conversionDetails: conversionDetails, + cryptoDetails: { + tokenAddresses: [], + recipients: [], + amounts: [], + paymentReferences: [], + feeAmounts: [], + }, // cryptoDetails is not used with paymentNetworkId 0 + }); + + // add cryptpoDetails to metaDetails + metaDetails.push({ + paymentNetworkId: 2, + conversionDetails: [], + cryptoDetails: { + tokenAddresses: tokenAddresses, + recipients: paymentAddresses, + amounts: amountsToPay, + paymentReferences: paymentReferences, + feeAmounts: feesToPay, + }, + }); + + const proxyContract = BatchConversionPayments__factory.createInterface(); + return proxyContract.encodeFunctionData('batchRouter', [metaDetails, feeAddress]); } /** - * Get batch arguments - * @param requests List of requests - * @returns List with the args required by batch Eth and Erc20 functions, - * @dev tokenAddresses returned is for batch Erc20 functions + * It get the conversion detail values from one enriched request + * @param enrichedRequest + * @returns */ -function getBatchArgs( - requests: ClientTypes.IRequestData[], -): { - tokenAddresses: Array; - paymentAddresses: Array; - amountsToPay: Array; - paymentReferences: Array; - feesToPay: Array; - feeAddressUsed: string; -} { - const tokenAddresses: Array = []; - const paymentAddresses: Array = []; - const amountsToPay: Array = []; - const paymentReferences: Array = []; - const feesToPay: Array = []; - let feeAddressUsed = constants.AddressZero; +function getInputConversionDetail(enrichedRequest: EnrichedRequest): ConversionDetail { + const paymentSettings = enrichedRequest.paymentSettings; + if (!paymentSettings) throw Error('the first enrichedRequest has no version'); + const { path } = checkRequestAndGetPathAndCurrency(enrichedRequest.request, paymentSettings); - const paymentType = requests[0].currencyInfo.type; - for (let i = 0; i < requests.length; i++) { - if (paymentType === 'ETH') { - validateEthFeeProxyRequest(requests[i]); - } else if (paymentType === 'ERC20') { - validateErc20FeeProxyRequest(requests[i]); - } else { - throw new Error(`paymentType ${paymentType} is not supported for batch payment`); - } - - const tokenAddress = requests[i].currencyInfo.value; - const { paymentReference, paymentAddress, feeAddress, feeAmount } = getRequestPaymentValues( - requests[i], - ); + const { paymentReference, paymentAddress, feeAmount, maxRateTimespan } = getRequestPaymentValues( + enrichedRequest.request, + ); - tokenAddresses.push(tokenAddress); - paymentAddresses.push(paymentAddress); - amountsToPay.push(getAmountToPay(requests[i])); - paymentReferences.push(`0x${paymentReference}`); - feesToPay.push(BigNumber.from(feeAmount || 0)); - feeAddressUsed = feeAddress || constants.AddressZero; - } + const requestAmount = BigNumber.from(enrichedRequest.request.expectedAmount).sub( + enrichedRequest.request.balance?.balance || 0, + ); + const maxToSpend = BigNumber.from(paymentSettings.maxToSpend); return { - tokenAddresses, - paymentAddresses, - amountsToPay, - paymentReferences, - feesToPay, - feeAddressUsed, + recipient: paymentAddress, + requestAmount: requestAmount, + path: path, + paymentReference: paymentReference, + feeAmount: BigNumber.from(feeAmount), + maxToSpend: maxToSpend, + maxRateTimespan: BigNumber.from(maxRateTimespan), }; } /** - * Get Batch contract Address + * Get Batch conversion contract Address * @param request - * @param version version of the batch proxy, which can be different from request pn version + * @param version version of the batch conversion proxy */ -export function getBatchProxyAddress(request: ClientTypes.IRequestData, version: string): string { - const pn = getPaymentNetworkExtension(request); - const pnId = (pn?.id as unknown) as PaymentTypes.PAYMENT_NETWORK_ID; - if (!pnId) { - throw new Error('No payment network Id'); - } - - const proxyAddress = batchPaymentsArtifact.getAddress(request.currencyInfo.network!, version); +export function getBatchConversionProxyAddress( + request: ClientTypes.IRequestData, + version: string, +): string { + const network = request.currencyInfo.network; + if (!network) throw new Error('the request has no network within currencyInfo'); + const proxyAddress = batchConversionPaymentsArtifact.getAddress(network, version); if (!proxyAddress) { - throw new Error(`No deployment found for network ${pn}, version ${pn?.version}`); + throw new Error( + `No deployment found on the network ${network}, associated with the version ${version}`, + ); } return proxyAddress; } /** - * ERC20 Batch proxy approvals methods + * ERC20 Batch conversion proxy approvals methods */ /** - * Processes the approval transaction of the targeted ERC20 with batch proxy. + * Processes the approval transaction of the targeted ERC20 with batch conversion proxy. * @param request request to pay * @param account account that will be used to pay the request - * @param version version of the batch proxy, which can be different from request pn version + * @param version version of the batch conversion proxy, which can be different from request pn version * @param signerOrProvider the Web3 provider, or signer. Defaults to window.ethereum. * @param overrides optionally, override default transaction values, like gas. */ -export async function approveErc20BatchIfNeeded( +export async function approveErc20BatchConversionIfNeeded( request: ClientTypes.IRequestData, account: string, version: string, signerOrProvider: providers.Provider | Signer = getProvider(), overrides?: ITransactionOverrides, ): Promise { - if (!(await hasErc20BatchApproval(request, account, version, signerOrProvider))) { - return approveErc20Batch(request, version, getSigner(signerOrProvider), overrides); + if (!(await hasErc20BatchConversionApproval(request, account, version, signerOrProvider))) { + return approveErc20BatchConversion(request, version, getSigner(signerOrProvider), overrides); } } /** - * Checks if the batch proxy has the necessary allowance from a given account + * Checks if the batch conversion proxy has the necessary allowance from a given account * to pay a given request with ERC20 batch * @param request request to pay * @param account account that will be used to pay the request - * @param version version of the batch proxy, which can be different from request pn version + * @param version version of the batch conversion proxy, which can be different from request pn version * @param signerOrProvider the Web3 provider, or signer. Defaults to window.ethereum. */ -export async function hasErc20BatchApproval( +export async function hasErc20BatchConversionApproval( request: ClientTypes.IRequestData, account: string, version: string, @@ -440,7 +264,7 @@ export async function hasErc20BatchApproval( ): Promise { return checkErc20Allowance( account, - getBatchProxyAddress(request, version), + getBatchConversionProxyAddress(request, version), signerOrProvider, request.currencyInfo.value, request.expectedAmount, @@ -448,20 +272,25 @@ export async function hasErc20BatchApproval( } /** - * Processes the transaction to approve the batch proxy to spend signer's tokens to pay + * Processes the transaction to approve the batch conversion proxy to spend signer's tokens to pay * the request in its payment currency. Can be used with a Multisig contract. * @param request request to pay - * @param version version of the batch proxy, which can be different from request pn version + * @param version version of the batch conversion proxy, which can be different from request pn version * @param signerOrProvider the Web3 provider, or signer. Defaults to window.ethereum. * @param overrides optionally, override default transaction values, like gas. */ -export async function approveErc20Batch( +export async function approveErc20BatchConversion( request: ClientTypes.IRequestData, version: string, signerOrProvider: providers.Provider | Signer = getProvider(), overrides?: ITransactionOverrides, ): Promise { - const preparedTx = prepareApproveErc20Batch(request, version, signerOrProvider, overrides); + const preparedTx = prepareApproveErc20BatchConversion( + request, + version, + signerOrProvider, + overrides, + ); const signer = getSigner(signerOrProvider); const tx = await signer.sendTransaction(preparedTx); return tx; @@ -471,17 +300,17 @@ export async function approveErc20Batch( * Prepare the transaction to approve the proxy to spend signer's tokens to pay * the request in its payment currency. Can be used with a Multisig contract. * @param request request to pay - * @param version version of the batch proxy, which can be different from request pn version + * @param version version of the batch conversion proxy, which can be different from request pn version * @param signerOrProvider the Web3 provider, or signer. Defaults to window.ethereum. * @param overrides optionally, override default transaction values, like gas. */ -export function prepareApproveErc20Batch( +export function prepareApproveErc20BatchConversion( request: ClientTypes.IRequestData, version: string, signerOrProvider: providers.Provider | Signer = getProvider(), overrides?: ITransactionOverrides, ): IPreparedTransaction { - const encodedTx = encodeApproveErc20Batch(request, version, signerOrProvider); + const encodedTx = encodeApproveErc20BatchConversion(request, version, signerOrProvider); const tokenAddress = request.currencyInfo.value; return { data: encodedTx, @@ -492,18 +321,18 @@ export function prepareApproveErc20Batch( } /** - * Encodes the transaction to approve the batch proxy to spend signer's tokens to pay + * Encodes the transaction to approve the batch conversion proxy to spend signer's tokens to pay * the request in its payment currency. Can be used with a Multisig contract. * @param request request to pay - * @param version version of the batch proxy, which can be different from request pn version + * @param version version of the batch conversion proxy, which can be different from request pn version * @param signerOrProvider the Web3 provider, or signer. Defaults to window.ethereum. */ -export function encodeApproveErc20Batch( +export function encodeApproveErc20BatchConversion( request: ClientTypes.IRequestData, version: string, signerOrProvider: providers.Provider | Signer = getProvider(), ): string { - const proxyAddress = getBatchProxyAddress(request, version); + const proxyAddress = getBatchConversionProxyAddress(request, version); return encodeApproveAnyErc20( request.currencyInfo.value, diff --git a/packages/payment-processor/src/payment/batch-proxy.ts b/packages/payment-processor/src/payment/batch-proxy.ts index 12a7f0298..5a5070a1b 100644 --- a/packages/payment-processor/src/payment/batch-proxy.ts +++ b/packages/payment-processor/src/payment/batch-proxy.ts @@ -158,7 +158,7 @@ export function encodePayBatchRequest(requests: ClientTypes.IRequestData[]): str * @returns List with the args required by batch Eth and Erc20 functions, * @dev tokenAddresses returned is for batch Erc20 functions */ -function getBatchArgs(requests: ClientTypes.IRequestData[]): { +export function getBatchArgs(requests: ClientTypes.IRequestData[]): { tokenAddresses: Array; paymentAddresses: Array; amountsToPay: Array; From d3054757be9e1d9693bcffd31d60943bf08a0123 Mon Sep 17 00:00:00 2001 From: olivier7delf <55892112+olivier7delf@users.noreply.github.com> Date: Thu, 18 Aug 2022 19:31:52 +0200 Subject: [PATCH 063/105] test batch erc20 --- .../src/payment/batch-proxy-conv.ts | 22 +- .../payment-processor/src/payment/utils.ts | 9 +- .../payment/any-to-erc20-batch-proxy.test.ts | 418 ++++++++++++++++++ .../test/payment/erc20-batch-proxy.test.ts | 2 +- 4 files changed, 443 insertions(+), 8 deletions(-) create mode 100644 packages/payment-processor/test/payment/any-to-erc20-batch-proxy.test.ts diff --git a/packages/payment-processor/src/payment/batch-proxy-conv.ts b/packages/payment-processor/src/payment/batch-proxy-conv.ts index dceb3b871..ba24b14ae 100644 --- a/packages/payment-processor/src/payment/batch-proxy-conv.ts +++ b/packages/payment-processor/src/payment/batch-proxy-conv.ts @@ -46,7 +46,7 @@ type MetaDetail = { * It contains requests, paymentSettings, amount and feeAmount, * having the same PN, version, and batchFee */ -type EnrichedRequest = { +export type EnrichedRequest = { paymentNetworkId: 0 | 2; // ref in batchConversionPayment.sol request: ClientTypes.IRequestData; paymentSettings?: IConversionPaymentSettings; @@ -119,6 +119,10 @@ export function encodePayBatchConversionRequest(enrichedRequests: EnrichedReques const conversionDetails: ConversionDetail[] = []; for (let i = 0; i < enrichedRequests.length; i++) { + const iExtension = getPaymentNetworkExtension(enrichedRequests[i].request); + if (!iExtension) { + throw new Error('no payment network found'); + } if (enrichedRequests[i].paymentNetworkId === 0) { // set pn0FirstRequest only if it is undefined pn0FirstRequest = pn0FirstRequest ?? enrichedRequests[i].request; @@ -134,14 +138,24 @@ export function encodePayBatchConversionRequest(enrichedRequests: EnrichedReques } else if (enrichedRequests[i].paymentNetworkId === 2) { pn2requests.push(enrichedRequests[i].request); - if (!comparePnTypeAndVersion(getPaymentNetworkExtension(pn2requests[0]), pn2requests[-1])) { + if ( + !comparePnTypeAndVersion( + getPaymentNetworkExtension(pn2requests[0]), + enrichedRequests[i].request, + ) + ) { throw new Error(`Every payment network type and version must be identical`); } } } // get cryptoDetails values - const { tokenAddresses, paymentAddresses, amountsToPay, paymentReferences, feesToPay } = - getBatchArgs(pn2requests); + const { + tokenAddresses, + paymentAddresses, + amountsToPay, + paymentReferences, + feesToPay, + } = getBatchArgs(pn2requests); // add conversionDetails to metaDetails metaDetails.push({ diff --git a/packages/payment-processor/src/payment/utils.ts b/packages/payment-processor/src/payment/utils.ts index f3ac234fe..727ed3a1c 100644 --- a/packages/payment-processor/src/payment/utils.ts +++ b/packages/payment-processor/src/payment/utils.ts @@ -82,7 +82,9 @@ export function getPaymentNetworkExtension( * of a Request. * @param request */ -export function getRequestPaymentValues(request: ClientTypes.IRequestData): { +export function getRequestPaymentValues( + request: ClientTypes.IRequestData, +): { paymentAddress: string; paymentReference: string; feeAmount?: string; @@ -192,8 +194,9 @@ export function validateRequest( request: ClientTypes.IRequestData, paymentNetworkId: PaymentTypes.PAYMENT_NETWORK_ID, ): void { - const { feeAmount, feeAddress, expectedFlowRate, expectedStartDate } = - getRequestPaymentValues(request); + const { feeAmount, feeAddress, expectedFlowRate, expectedStartDate } = getRequestPaymentValues( + request, + ); let extension = request.extensions[paymentNetworkId]; // FIXME: updating the extension: not needed anymore when "invoicing" will use only ethFeeProxy diff --git a/packages/payment-processor/test/payment/any-to-erc20-batch-proxy.test.ts b/packages/payment-processor/test/payment/any-to-erc20-batch-proxy.test.ts new file mode 100644 index 000000000..e0b883257 --- /dev/null +++ b/packages/payment-processor/test/payment/any-to-erc20-batch-proxy.test.ts @@ -0,0 +1,418 @@ +import { Wallet, providers } from 'ethers'; + +import { + ClientTypes, + ExtensionTypes, + IdentityTypes, + PaymentTypes, + RequestLogicTypes, +} from '@requestnetwork/types'; +import Utils from '@requestnetwork/utils'; + +// conv +// import { IConversionPaymentSettings } from '../../src/index'; +// import { currencyManager } from './shared'; +import { + EnrichedRequest, + getBatchConversionProxyAddress, + payBatchConversionProxyRequest, +} from '../../src/payment/batch-proxy-conv'; +import { sameCurrencyValue } from './erc20-batch-proxy.test'; + +/* eslint-disable no-magic-numbers */ +/* eslint-disable @typescript-eslint/no-unused-expressions */ + +// Cf. ERC20Alpha in TestERC20.sol +const erc20ContractAddress = '0x38cF23C52Bb4B13F051Aec09580a2dE845a7FA35'; +// const alphaPaymentSettings: IConversionPaymentSettings = { +// currency: { +// type: RequestLogicTypes.CURRENCY.ERC20, +// value: erc20ContractAddress, +// network: 'private', +// }, +// maxToSpend: BigNumber.from(2).pow(256).sub(1), +// currencyManager, +// }; + +// const batchConvFee = 100; +const batchConvVersion = '0.1.0'; +const DAITokenAddress = '0x38cF23C52Bb4B13F051Aec09580a2dE845a7FA35'; +const FAUTokenAddress = '0x9FBDa871d559710256a2502A2517b794B482Db40'; // TestERC20 address +const mnemonic = 'candy maple cake sugar pudding cream honey rich smooth crumble sweet treat'; +const paymentAddress = '0xf17f52151EbEF6C7334FAD080c5704D77216b732'; +const feeAddress = '0xC5fdf4076b8F3A5357c5E395ab970B5B54098Fef'; +const provider = new providers.JsonRpcProvider('http://localhost:8545'); +const wallet = Wallet.fromMnemonic(mnemonic).connect(provider); + +const validEuroRequest: ClientTypes.IRequestData = { + balance: { + balance: '0', + events: [], + }, + contentData: {}, + creator: { + type: IdentityTypes.TYPE.ETHEREUM_ADDRESS, + value: wallet.address, + }, + currency: 'EUR', + currencyInfo: { + type: RequestLogicTypes.CURRENCY.ISO4217, + value: 'EUR', + }, + + events: [], + expectedAmount: '100', + extensions: { + [PaymentTypes.PAYMENT_NETWORK_ID.ANY_TO_ERC20_PROXY]: { + events: [], + id: ExtensionTypes.ID.PAYMENT_NETWORK_ANY_TO_ERC20_PROXY, + type: ExtensionTypes.TYPE.PAYMENT_NETWORK, + values: { + feeAddress, + feeAmount: '2', + paymentAddress, + salt: 'salt', + network: 'private', + tokensAccepted: [erc20ContractAddress], + }, + version: '0.1.0', + }, + }, + extensionsData: [], + meta: { + transactionManagerMeta: {}, + }, + pending: null, + requestId: 'abcd', + state: RequestLogicTypes.STATE.CREATED, + timestamp: 0, + version: '1.0', +}; +validEuroRequest; //todo delete + +const validRequest: ClientTypes.IRequestData = { + balance: { + balance: '0', + events: [], + }, + contentData: {}, + creator: { + type: IdentityTypes.TYPE.ETHEREUM_ADDRESS, + value: wallet.address, + }, + currency: 'DAI', + currencyInfo: { + network: 'private', + type: RequestLogicTypes.CURRENCY.ERC20 as any, + value: DAITokenAddress, + }, + events: [], + expectedAmount: '1000', + extensions: { + [PaymentTypes.PAYMENT_NETWORK_ID.ERC20_FEE_PROXY_CONTRACT]: { + events: [], + id: ExtensionTypes.ID.PAYMENT_NETWORK_ERC20_FEE_PROXY_CONTRACT, + type: ExtensionTypes.TYPE.PAYMENT_NETWORK, + values: { + feeAddress, + feeAmount: '2', + paymentAddress: paymentAddress, + salt: 'salt', + }, + version: '0.1.0', + }, + }, + extensionsData: [], + meta: { + transactionManagerMeta: {}, + }, + pending: null, + requestId: 'abcd', + state: RequestLogicTypes.STATE.CREATED, + timestamp: 0, + version: '1.0', +}; + +const fauValidRequest = Utils.deepCopy(validRequest) as ClientTypes.IRequestData; +fauValidRequest.currencyInfo = { + network: 'private', + type: RequestLogicTypes.CURRENCY.ERC20 as any, + value: FAUTokenAddress, +}; + +const enrichedRequests: EnrichedRequest[] = [ + { + paymentNetworkId: 2, + request: validRequest, + }, + { + paymentNetworkId: 2, + request: fauValidRequest, + }, +]; + +// paymentNetworkId: 0 | 2; // ref in batchConversionPayment.sol +// request: ClientTypes.IRequestData; +// paymentSettings?: IConversionPaymentSettings; +// amount?: BigNumberish; +// feeAmount?: BigNumberish; + +const getData = ( + request1: ClientTypes.IRequestData, + request2: ClientTypes.IRequestData, +): string => { + if (sameCurrencyValue(request1, validRequest) && sameCurrencyValue(request2, validRequest)) { + return '0xf0fa379f0000000000000000000000000000000000000000000000000000000000000040000000000000000000000000c5fdf4076b8f3a5357c5e395ab970b5b54098fef000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000c000000000000000000000000000000000000000000000000000000000000000e00000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000012000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000016000000000000000000000000000000000000000000000000000000000000001c000000000000000000000000000000000000000000000000000000000000002a0000000000000000000000000000000000000000000000000000000000000000200000000000000000000000038cf23c52bb4b13f051aec09580a2de845a7fa350000000000000000000000009fbda871d559710256a2502a2517b794b482db400000000000000000000000000000000000000000000000000000000000000002000000000000000000000000f17f52151ebef6c7334fad080c5704d77216b732000000000000000000000000f17f52151ebef6c7334fad080c5704d77216b732000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000003e800000000000000000000000000000000000000000000000000000000000003e8000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000000886dfbccad783599a000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000886dfbccad783599a000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000002'; + } else if ( + sameCurrencyValue(request1, validRequest) && + sameCurrencyValue(request2, fauValidRequest) + ) { + return '0xfa73314200000000000000000000000000000000000000000000000000000000000000c00000000000000000000000000000000000000000000000000000000000000120000000000000000000000000000000000000000000000000000000000000018000000000000000000000000000000000000000000000000000000000000001e000000000000000000000000000000000000000000000000000000000000002c0000000000000000000000000c5fdf4076b8f3a5357c5e395ab970b5b54098fef000000000000000000000000000000000000000000000000000000000000000200000000000000000000000038cf23c52bb4b13f051aec09580a2de845a7fa350000000000000000000000009fbda871d559710256a2502a2517b794b482db400000000000000000000000000000000000000000000000000000000000000002000000000000000000000000f17f52151ebef6c7334fad080c5704d77216b732000000000000000000000000f17f52151ebef6c7334fad080c5704d77216b732000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000003e800000000000000000000000000000000000000000000000000000000000003e8000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000000886dfbccad783599a000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000886dfbccad783599a000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000002'; + } else { + throw 'wrong requests'; + } +}; + +const testSuite = ( + suiteName: string, + requestTemplate1: ClientTypes.IRequestData, + requestTemplate2: ClientTypes.IRequestData, +) => { + describe(suiteName, () => { + let request1: ClientTypes.IRequestData; + let request2: ClientTypes.IRequestData; + + beforeEach(() => { + jest.restoreAllMocks(); + request1 = Utils.deepCopy(requestTemplate1) as ClientTypes.IRequestData; + + request2 = Utils.deepCopy(requestTemplate2) as ClientTypes.IRequestData; + request1; + }); + + it('should throw an error if the request is not erc20', async () => { + request2.currencyInfo.type = RequestLogicTypes.CURRENCY.ETH; + const wrongEnrichedRequests = Utils.deepCopy(enrichedRequests); + wrongEnrichedRequests.push({ + paymentNetworkId: 2, + request: request2, + }); + await expect( + payBatchConversionProxyRequest(wrongEnrichedRequests, batchConvVersion, wallet), + ).rejects.toThrowError( + 'request cannot be processed, or is not an pn-erc20-fee-proxy-contract request', + ); + }); + + it("should throw an error if one request's currencyInfo has no value", async () => { + request2.currencyInfo.type = RequestLogicTypes.CURRENCY.ETH; + const wrongEnrichedRequests = Utils.deepCopy(enrichedRequests); + wrongEnrichedRequests.push({ + paymentNetworkId: 2, + request: request2, + }); + await expect( + payBatchConversionProxyRequest(wrongEnrichedRequests, batchConvVersion, wallet), + ).rejects.toThrowError( + 'request cannot be processed, or is not an pn-erc20-fee-proxy-contract request', + ); + }); + + it("should throw an error if one request's currencyInfo has no network", async () => { + request2.currencyInfo.network = ''; + const wrongEnrichedRequests = Utils.deepCopy(enrichedRequests); + wrongEnrichedRequests.push({ + paymentNetworkId: 2, + request: request2, + }); + await expect( + payBatchConversionProxyRequest(wrongEnrichedRequests, batchConvVersion, wallet), + ).rejects.toThrowError( + 'request cannot be processed, or is not an pn-erc20-fee-proxy-contract request', + ); + }); + + it('should throw an error if request has no extension', async () => { + request2.extensions = [] as any; + const wrongEnrichedRequests = Utils.deepCopy(enrichedRequests); + wrongEnrichedRequests.push({ + paymentNetworkId: 2, + request: request2, + }); + await expect( + payBatchConversionProxyRequest(wrongEnrichedRequests, batchConvVersion, wallet), + ).rejects.toThrowError('no payment network found'); + }); + + it('should throw an error if there is a wrong version mapping', async () => { + request2.extensions = { + [PaymentTypes.PAYMENT_NETWORK_ID.ERC20_FEE_PROXY_CONTRACT]: { + ...validRequest.extensions[PaymentTypes.PAYMENT_NETWORK_ID.ERC20_FEE_PROXY_CONTRACT], + version: '0.3.0', + }, + }; + const wrongEnrichedRequests = Utils.deepCopy(enrichedRequests); + wrongEnrichedRequests.push({ + paymentNetworkId: 2, + request: request2, + }); + await expect( + payBatchConversionProxyRequest(wrongEnrichedRequests, batchConvVersion, wallet), + ).rejects.toThrowError('Every payment network type and version must be identical'); + }); + + describe('payBatchProxyRequest', () => { + it('should consider override parameters', async () => { + const spy = jest.fn(); + const originalSendTransaction = wallet.sendTransaction.bind(wallet); + wallet.sendTransaction = spy; + await payBatchConversionProxyRequest(enrichedRequests, batchConvVersion, wallet, { + gasPrice: '20000000000', + }); + expect(spy).toHaveBeenCalledWith({ + data: getData(request1, request2), + gasPrice: '20000000000', + to: getBatchConversionProxyAddress(request1, '0.1.0'), + value: 0, + }); + wallet.sendTransaction = originalSendTransaction; + }); + }); + // it('should pay an ERC20 request with fees', async () => { + // // first approve the contract + // const tmpRequest = Utils.deepCopy(request1); + // let amount = 1000; + // const isMultiToken = !sameCurrencyValue(request1, request2); + + // if (!isMultiToken) { + // amount = 2 * amount; + // tmpRequest.expectedAmount = amount.toString(); + // } else { + // const ApprovalTx2 = await approveErc20BatchIfNeeded( + // request2, + // wallet.address, + // batchConvVersion, + // wallet, + // ); + // if (ApprovalTx2) { + // await ApprovalTx2.wait(1); + // } + // } + + // const approvalTx = await approveErc20BatchIfNeeded( + // tmpRequest, + // wallet.address, + // batchConvVersion, + // wallet, + // ); + + // if (approvalTx) { + // await approvalTx.wait(1); + // } + // request1.extensions['pn-erc20-fee-proxy-contract'].values.feeAmount = '6'; + + // // get the balance + // const balanceEthBefore = await wallet.getBalance(); + // const balanceErc20Before = await getErc20Balance(request1, wallet.address, provider); + // const feeBalanceErc20Before = await getErc20Balance(request1, feeAddress, provider); + + // const balanceErc20Before2 = await getErc20Balance(request2, wallet.address, provider); + // const feeBalanceErc20Before2 = await getErc20Balance(request2, feeAddress, provider); + + // // Batch payment + // const tx = await payBatchProxyRequest([request1, request2], batchConvVersion, wallet, batchFee); + // const confirmedTx = await tx.wait(1); + + // const balanceEthAfter = await wallet.getBalance(); + // const balanceErc20After = await getErc20Balance(request1, wallet.address, provider); + // const feeBalanceErc20After = await getErc20Balance(request1, feeAddress, provider); + + // expect(confirmedTx.status).toBe(1); + // expect(tx.hash).not.toBeUndefined(); + // expect(balanceEthAfter.lte(balanceEthBefore)).toBeTruthy(); // 'ETH balance should be lower' + + // let feeAmount = 6 + 10 + (2 + 10); + // if (isMultiToken) { + // feeAmount = 6 + 10; // (2 + 10) will be sent on the 2nd token fee address + // const balanceErc20After2 = await getErc20Balance(request2, wallet.address, provider); + // const feeBalanceErc20After2 = await getErc20Balance(request2, feeAddress, provider); + // // compare request 2 balances + // expect( + // BigNumber.from(balanceErc20After2).eq( + // BigNumber.from(balanceErc20Before2).sub(amount + (2 + 10)), + // ), + // ); + // expect( + // BigNumber.from(feeBalanceErc20After2).eq( + // BigNumber.from(feeBalanceErc20Before2).add(2 + 10), + // ), + // ); + // } + + // // compare request 1 balances + // expect( + // BigNumber.from(balanceErc20After).eq( + // BigNumber.from(balanceErc20Before).sub(amount + feeAmount), + // ), + // ); + // expect( + // BigNumber.from(feeBalanceErc20After).eq( + // BigNumber.from(feeBalanceErc20Before).add(feeAmount), + // ), + // ); + // }); + // }); + // describe('prepareBatchPaymentTransaction', () => { + // it('should consider the version mapping', () => { + // expect( + // prepareBatchPaymentTransaction( + // [ + // { + // ...request1, + // extensions: { + // [PaymentTypes.PAYMENT_NETWORK_ID.ERC20_FEE_PROXY_CONTRACT]: { + // ...request1.extensions[ + // PaymentTypes.PAYMENT_NETWORK_ID.ERC20_FEE_PROXY_CONTRACT + // ], + // version: '0.1.0', + // }, + // }, + // } as any, + // { + // ...request2, + // extensions: { + // [PaymentTypes.PAYMENT_NETWORK_ID.ERC20_FEE_PROXY_CONTRACT]: { + // ...request2.extensions[ + // PaymentTypes.PAYMENT_NETWORK_ID.ERC20_FEE_PROXY_CONTRACT + // ], + // version: '0.1.0', + // }, + // }, + // } as any, + // ], + // batchConvVersion, + // batchFee, + // ).to, + // ).toBe(batchPaymentsArtifact.getAddress('private', '0.1.0')); + // }); + // }); + }); +}; + +describe('erc20-batch-proxy BIS', () => { + beforeEach(() => { + jest.restoreAllMocks(); + }); + // describe('getRequestPaymentValues', () => { + // it('handles ERC20', () => { + // const values = getRequestPaymentValues(validRequest); + // expect(values.feeAddress).toBe(feeAddress); + // expect(values.feeAmount).toBe('2'); + // expect(values.paymentAddress).toBe(paymentAddress); + // expect(values.paymentReference).toBe('86dfbccad783599a'); + // }); + // }); + + testSuite('encodePayErc20BatchRequest for one type of ERC20', validRequest, validRequest); + // testSuite('encodePayErc20BatchRequest using two different ERC20', validRequest, fauValidRequest); +}); diff --git a/packages/payment-processor/test/payment/erc20-batch-proxy.test.ts b/packages/payment-processor/test/payment/erc20-batch-proxy.test.ts index dfe015db1..037147c34 100644 --- a/packages/payment-processor/test/payment/erc20-batch-proxy.test.ts +++ b/packages/payment-processor/test/payment/erc20-batch-proxy.test.ts @@ -82,7 +82,7 @@ fauValidRequest.currencyInfo = { value: FAUTokenAddress, }; -const sameCurrencyValue = ( +export const sameCurrencyValue = ( requestA: ClientTypes.IRequestData, requestB: ClientTypes.IRequestData, ): boolean => { From 239c216a4e14d315772f062b4acac4b4bdb43dcc Mon Sep 17 00:00:00 2001 From: olivier7delf <55892112+olivier7delf@users.noreply.github.com> Date: Thu, 25 Aug 2022 12:26:09 +0200 Subject: [PATCH 064/105] tests batch conversion payment done --- .../src/payment/any-to-erc20-proxy.ts | 10 +- ...atch-proxy-conv.ts => batch-conv-proxy.ts} | 265 +++--- .../src/payment/batch-proxy.ts | 9 +- .../payment-processor/src/payment/erc20.ts | 4 +- .../payment-processor/src/payment/index.ts | 15 + .../payment-processor/src/payment/utils.ts | 1 - .../payment/any-to-erc20-batch-proxy.test.ts | 796 ++++++++++++------ .../test/payment/any-to-erc20-proxy.test.ts | 3 +- .../test/payment/erc20-batch-proxy.test.ts | 2 +- .../test/payment/erc20-escrow-payment.test.ts | 4 + .../payment-processor/test/payment/shared.ts | 9 +- 11 files changed, 757 insertions(+), 361 deletions(-) rename packages/payment-processor/src/payment/{batch-proxy-conv.ts => batch-conv-proxy.ts} (57%) diff --git a/packages/payment-processor/src/payment/any-to-erc20-proxy.ts b/packages/payment-processor/src/payment/any-to-erc20-proxy.ts index c25c28d91..e585b2bdf 100644 --- a/packages/payment-processor/src/payment/any-to-erc20-proxy.ts +++ b/packages/payment-processor/src/payment/any-to-erc20-proxy.ts @@ -74,7 +74,6 @@ export function encodePayAnyToErc20ProxyRequest( amountToPay, feeToPay, } = prepAnyToErc20ProxyRequest(request, paymentSettings, amount, feeAmountOverride); - const proxyContract = Erc20ConversionProxy__factory.createInterface(); return proxyContract.encodeFunctionData('transferFromWithReferenceAndFee', [ paymentAddress, @@ -154,8 +153,13 @@ export function prepAnyToErc20ProxyRequest( feeAmountOverride, ); - const { paymentReference, paymentAddress, feeAddress, feeAmount, maxRateTimespan } = - getRequestPaymentValues(request); + const { + paymentReference, + paymentAddress, + feeAddress, + feeAmount, + maxRateTimespan, + } = getRequestPaymentValues(request); const amountToPay = padAmountForChainlink(getAmountToPay(request, amount), requestCurrency); const feeToPay = padAmountForChainlink(feeAmountOverride || feeAmount || 0, requestCurrency); diff --git a/packages/payment-processor/src/payment/batch-proxy-conv.ts b/packages/payment-processor/src/payment/batch-conv-proxy.ts similarity index 57% rename from packages/payment-processor/src/payment/batch-proxy-conv.ts rename to packages/payment-processor/src/payment/batch-conv-proxy.ts index ba24b14ae..2e74b64d2 100644 --- a/packages/payment-processor/src/payment/batch-proxy-conv.ts +++ b/packages/payment-processor/src/payment/batch-conv-proxy.ts @@ -1,7 +1,7 @@ import { ContractTransaction, Signer, providers, BigNumber, BigNumberish } from 'ethers'; import { batchConversionPaymentsArtifact } from '@requestnetwork/smart-contracts'; -import { BatchConversionPayments__factory } from '../../../smart-contracts/types'; -import { ClientTypes } from '@requestnetwork/types'; +import { BatchConversionPayments__factory } from '@requestnetwork/smart-contracts/types'; +import { ClientTypes, ExtensionTypes, RequestLogicTypes } from '@requestnetwork/types'; import { ITransactionOverrides } from './transaction-overrides'; import { comparePnTypeAndVersion, @@ -10,8 +10,9 @@ import { getRequestPaymentValues, getSigner, } from './utils'; +import { padAmountForChainlink } from '@requestnetwork/payment-detection'; import { IPreparedTransaction } from './prepared-transaction'; -import { IConversionPaymentSettings } from './index'; +import { EnrichedRequest, IConversionPaymentSettings } from './index'; import { checkRequestAndGetPathAndCurrency } from './any-to-erc20-proxy'; import { getBatchArgs } from './batch-proxy'; import { checkErc20Allowance, encodeApproveAnyErc20 } from './erc20'; @@ -42,31 +43,19 @@ type MetaDetail = { }; /** - * Type used by batch conversion payment processor - * It contains requests, paymentSettings, amount and feeAmount, - * having the same PN, version, and batchFee - */ -export type EnrichedRequest = { - paymentNetworkId: 0 | 2; // ref in batchConversionPayment.sol - request: ClientTypes.IRequestData; - paymentSettings?: IConversionPaymentSettings; - amount?: BigNumberish; - feeAmount?: BigNumberish; -}; - -/** - * Processes a transaction to pay a batch of requests with an ERC20 or ETH currency that is different from the request currency (eg. fiat). + * Processes a transaction to pay a batch of requests with an ERC20 currency + * that is different from the request currency (eg. fiat) * The payment is made through ERC20 or ERC20Conversion proxies * It can be used with a Multisig contract - * @param enrichedRequests List of EnrichedRequest - * @param version of the batch conversion proxy - * @param signerOrProvider the Web3 provider, or signer. Defaults to window.ethereum. - * @param overrides optionally, override default transaction values, like gas. - * @dev we only implement batchRouter using the ERC20 normal and conversion functions: + * @param enrichedRequests List of EnrichedRequest to pay + * @param version Version of the batch conversion proxy + * @param signerOrProvider The Web3 provider, or signer. Defaults to window.ethereum. + * @param overrides Optionally, override default transaction values, like gas. + * @dev We only implement batchRouter using the ERC20 functions: * batchERC20ConversionPaymentsMultiTokens, and batchERC20PaymentsMultiTokensWithReference. * It implies that paymentNetworkId take only theses values: 0 or 2 * Next steps: - * - Enable ETH payment: normal and conversion + * - Enable ETH payment within batchRouter function: with/out conversion * - Enable gas optimizaton: implement the others batch functions */ export async function payBatchConversionProxyRequest( @@ -81,15 +70,21 @@ export async function payBatchConversionProxyRequest( } /** - * Prepate the transaction to pay a batch of requests through the batch conversion proxy contract, + * Prepare the transaction to pay a batch of requests through the batch conversion proxy contract, * it can be used with a Multisig contract. + * @param enrichedRequests List of EnrichedRequest to pay + * @param version Version of the batch conversion proxy */ export function prepareBatchConversionPaymentTransaction( enrichedRequests: EnrichedRequest[], version: string, ): IPreparedTransaction { const encodedTx = encodePayBatchConversionRequest(enrichedRequests); - const proxyAddress = getBatchConversionProxyAddress(enrichedRequests[0].request, version); + const proxyAddress = getBatchConversionProxyAddress( + enrichedRequests[0].request, + version, + enrichedRequests[0].paymentSettings, + ); return { data: encodedTx, to: proxyAddress, @@ -103,21 +98,24 @@ export function prepareBatchConversionPaymentTransaction( * @param enrichedRequests list of ECR20 requests to pay */ export function encodePayBatchConversionRequest(enrichedRequests: EnrichedRequest[]): string { + // Get fee address const extension = getPaymentNetworkExtension(enrichedRequests[0].request); if (!extension) { throw new Error('no payment network found'); } - const { feeAddress } = extension.values; + //**** Create and fill batchRouter function argument: metaDetails ****// + const metaDetails: MetaDetail[] = []; - // variable and constant to get info about each payment network (pn) + // Variable and constants to get info about each payment network (pn) let pn0FirstRequest: ClientTypes.IRequestData | undefined; - const pn2requests = []; - + const pn2requests: ClientTypes.IRequestData[] = []; + // Constant storing conversion info const conversionDetails: ConversionDetail[] = []; + // Iterate throught each enrichedRequests to do checking and retrieve info for (let i = 0; i < enrichedRequests.length; i++) { const iExtension = getPaymentNetworkExtension(enrichedRequests[i].request); if (!iExtension) { @@ -126,18 +124,27 @@ export function encodePayBatchConversionRequest(enrichedRequests: EnrichedReques if (enrichedRequests[i].paymentNetworkId === 0) { // set pn0FirstRequest only if it is undefined pn0FirstRequest = pn0FirstRequest ?? enrichedRequests[i].request; + if (!(iExtension.id === ExtensionTypes.ID.PAYMENT_NETWORK_ANY_TO_ERC20_PROXY)) + throw new Error( + 'request cannot be processed, or is not an pn-erc20-fee-proxy-contract request', + ); + if ( !comparePnTypeAndVersion( getPaymentNetworkExtension(pn0FirstRequest), enrichedRequests[i].request, ) - ) { + ) throw new Error(`Every payment network type and version must be identical`); - } + if ( + ![RequestLogicTypes.CURRENCY.ISO4217, RequestLogicTypes.CURRENCY.ERC20].includes( + enrichedRequests[i].request.currencyInfo.type, + ) + ) + throw new Error(`wrong request currencyInfo type`); conversionDetails.push(getInputConversionDetail(enrichedRequests[i])); } else if (enrichedRequests[i].paymentNetworkId === 2) { pn2requests.push(enrichedRequests[i].request); - if ( !comparePnTypeAndVersion( getPaymentNetworkExtension(pn2requests[0]), @@ -148,54 +155,60 @@ export function encodePayBatchConversionRequest(enrichedRequests: EnrichedReques } } } - // get cryptoDetails values - const { - tokenAddresses, - paymentAddresses, - amountsToPay, - paymentReferences, - feesToPay, - } = getBatchArgs(pn2requests); - // add conversionDetails to metaDetails - metaDetails.push({ - paymentNetworkId: 0, - conversionDetails: conversionDetails, - cryptoDetails: { - tokenAddresses: [], - recipients: [], - amounts: [], - paymentReferences: [], - feeAmounts: [], - }, // cryptoDetails is not used with paymentNetworkId 0 - }); + // Add conversionDetails to metaDetails + if (pn0FirstRequest) { + metaDetails.push({ + paymentNetworkId: 0, + conversionDetails: conversionDetails, + cryptoDetails: { + tokenAddresses: [], + recipients: [], + amounts: [], + paymentReferences: [], + feeAmounts: [], + }, // cryptoDetails is not used with paymentNetworkId 0 + }); + } - // add cryptpoDetails to metaDetails - metaDetails.push({ - paymentNetworkId: 2, - conversionDetails: [], - cryptoDetails: { - tokenAddresses: tokenAddresses, - recipients: paymentAddresses, - amounts: amountsToPay, - paymentReferences: paymentReferences, - feeAmounts: feesToPay, - }, - }); + // Get values and add cryptpoDetails to metaDetails + if (pn2requests.length > 0) { + const { + tokenAddresses, + paymentAddresses, + amountsToPay, + paymentReferences, + feesToPay, + } = getBatchArgs(pn2requests, 'ERC20'); + metaDetails.push({ + paymentNetworkId: 2, + conversionDetails: [], + cryptoDetails: { + tokenAddresses: tokenAddresses, + recipients: paymentAddresses, + amounts: amountsToPay, + paymentReferences: paymentReferences, + feeAmounts: feesToPay, + }, + }); + } const proxyContract = BatchConversionPayments__factory.createInterface(); return proxyContract.encodeFunctionData('batchRouter', [metaDetails, feeAddress]); } /** - * It get the conversion detail values from one enriched request - * @param enrichedRequest - * @returns + * Get the conversion detail values from one enriched request + * @param enrichedRequest enrichedRequest to pay */ function getInputConversionDetail(enrichedRequest: EnrichedRequest): ConversionDetail { const paymentSettings = enrichedRequest.paymentSettings; - if (!paymentSettings) throw Error('the first enrichedRequest has no version'); - const { path } = checkRequestAndGetPathAndCurrency(enrichedRequest.request, paymentSettings); + if (!paymentSettings) throw Error('the enrichedRequest has no paymentSettings'); + + const { path, requestCurrency } = checkRequestAndGetPathAndCurrency( + enrichedRequest.request, + paymentSettings, + ); const { paymentReference, paymentAddress, feeAmount, maxRateTimespan } = getRequestPaymentValues( enrichedRequest.request, @@ -204,37 +217,44 @@ function getInputConversionDetail(enrichedRequest: EnrichedRequest): ConversionD const requestAmount = BigNumber.from(enrichedRequest.request.expectedAmount).sub( enrichedRequest.request.balance?.balance || 0, ); - const maxToSpend = BigNumber.from(paymentSettings.maxToSpend); + const padRequestAmount = padAmountForChainlink(requestAmount, requestCurrency); + const padFeeAmount = padAmountForChainlink(feeAmount || 0, requestCurrency); return { recipient: paymentAddress, - requestAmount: requestAmount, + requestAmount: padRequestAmount, path: path, - paymentReference: paymentReference, - feeAmount: BigNumber.from(feeAmount), - maxToSpend: maxToSpend, - maxRateTimespan: BigNumber.from(maxRateTimespan), + paymentReference: `0x${paymentReference}`, + feeAmount: BigNumber.from(padFeeAmount), + maxToSpend: BigNumber.from(paymentSettings.maxToSpend), + maxRateTimespan: BigNumber.from(maxRateTimespan || 0), }; } /** - * Get Batch conversion contract Address - * @param request - * @param version version of the batch conversion proxy + * Gets batch conversion contract Address + * @param request request for an ERC20 payment with/out conversion + * @param version of the batch conversion proxy + * @param paymentSettings paymentSettings is necessary for conversion payment */ export function getBatchConversionProxyAddress( request: ClientTypes.IRequestData, version: string, + paymentSettings?: IConversionPaymentSettings, ): string { - const network = request.currencyInfo.network; - if (!network) throw new Error('the request has no network within currencyInfo'); - const proxyAddress = batchConversionPaymentsArtifact.getAddress(network, version); + // Get the network + let network = request.currencyInfo.network; + if (paymentSettings?.currency?.network) { + network = paymentSettings.currency.network; + } + if (!network) throw new Error('Cannot pay with a currency missing a network'); - if (!proxyAddress) { + // Get the proxy address + const proxyAddress = batchConversionPaymentsArtifact.getAddress(network, version); + if (!proxyAddress) throw new Error( `No deployment found on the network ${network}, associated with the version ${version}`, ); - } return proxyAddress; } @@ -244,10 +264,11 @@ export function getBatchConversionProxyAddress( /** * Processes the approval transaction of the targeted ERC20 with batch conversion proxy. - * @param request request to pay + * @param request request for an ERC20 payment with/out conversion * @param account account that will be used to pay the request * @param version version of the batch conversion proxy, which can be different from request pn version * @param signerOrProvider the Web3 provider, or signer. Defaults to window.ethereum. + * @param paymentSettings paymentSettings is necessary for conversion payment approval * @param overrides optionally, override default transaction values, like gas. */ export async function approveErc20BatchConversionIfNeeded( @@ -255,32 +276,49 @@ export async function approveErc20BatchConversionIfNeeded( account: string, version: string, signerOrProvider: providers.Provider | Signer = getProvider(), + paymentSettings?: IConversionPaymentSettings, overrides?: ITransactionOverrides, ): Promise { - if (!(await hasErc20BatchConversionApproval(request, account, version, signerOrProvider))) { - return approveErc20BatchConversion(request, version, getSigner(signerOrProvider), overrides); + if ( + !(await hasErc20BatchConversionApproval( + request, + account, + version, + signerOrProvider, + paymentSettings, + )) + ) { + return approveErc20BatchConversion( + request, + version, + getSigner(signerOrProvider), + paymentSettings, + overrides, + ); } } /** * Checks if the batch conversion proxy has the necessary allowance from a given account - * to pay a given request with ERC20 batch - * @param request request to pay + * to pay a given request with ERC20 batch conversion proxy + * @param request request for an ERC20 payment with/out conversion * @param account account that will be used to pay the request - * @param version version of the batch conversion proxy, which can be different from request pn version + * @param version version of the batch conversion proxy * @param signerOrProvider the Web3 provider, or signer. Defaults to window.ethereum. + * @param paymentSettings paymentSettings is necessary for conversion payment approval */ export async function hasErc20BatchConversionApproval( request: ClientTypes.IRequestData, account: string, version: string, signerOrProvider: providers.Provider | Signer = getProvider(), + paymentSettings?: IConversionPaymentSettings, ): Promise { return checkErc20Allowance( account, - getBatchConversionProxyAddress(request, version), + getBatchConversionProxyAddress(request, version, paymentSettings), signerOrProvider, - request.currencyInfo.value, + getTokenAddress(request, paymentSettings), request.expectedAmount, ); } @@ -288,21 +326,24 @@ export async function hasErc20BatchConversionApproval( /** * Processes the transaction to approve the batch conversion proxy to spend signer's tokens to pay * the request in its payment currency. Can be used with a Multisig contract. - * @param request request to pay + * @param request request for an ERC20 payment with/out conversion * @param version version of the batch conversion proxy, which can be different from request pn version * @param signerOrProvider the Web3 provider, or signer. Defaults to window.ethereum. + * @param paymentSettings paymentSettings is necessary for conversion payment approval * @param overrides optionally, override default transaction values, like gas. */ export async function approveErc20BatchConversion( request: ClientTypes.IRequestData, version: string, signerOrProvider: providers.Provider | Signer = getProvider(), + paymentSettings?: IConversionPaymentSettings, overrides?: ITransactionOverrides, ): Promise { const preparedTx = prepareApproveErc20BatchConversion( request, version, signerOrProvider, + paymentSettings, overrides, ); const signer = getSigner(signerOrProvider); @@ -313,22 +354,28 @@ export async function approveErc20BatchConversion( /** * Prepare the transaction to approve the proxy to spend signer's tokens to pay * the request in its payment currency. Can be used with a Multisig contract. - * @param request request to pay - * @param version version of the batch conversion proxy, which can be different from request pn version + * @param request request for an ERC20 payment with/out conversion + * @param version version of the batch conversion proxy * @param signerOrProvider the Web3 provider, or signer. Defaults to window.ethereum. + * @param paymentSettings paymentSettings is necessary for conversion payment approval * @param overrides optionally, override default transaction values, like gas. */ export function prepareApproveErc20BatchConversion( request: ClientTypes.IRequestData, version: string, signerOrProvider: providers.Provider | Signer = getProvider(), + paymentSettings?: IConversionPaymentSettings, overrides?: ITransactionOverrides, ): IPreparedTransaction { - const encodedTx = encodeApproveErc20BatchConversion(request, version, signerOrProvider); - const tokenAddress = request.currencyInfo.value; + const encodedTx = encodeApproveErc20BatchConversion( + request, + version, + signerOrProvider, + paymentSettings, + ); return { data: encodedTx, - to: tokenAddress, + to: getTokenAddress(request, paymentSettings), value: 0, ...overrides, }; @@ -337,20 +384,34 @@ export function prepareApproveErc20BatchConversion( /** * Encodes the transaction to approve the batch conversion proxy to spend signer's tokens to pay * the request in its payment currency. Can be used with a Multisig contract. - * @param request request to pay - * @param version version of the batch conversion proxy, which can be different from request pn version + * @param request request for an ERC20 payment with/out conversion + * @param version version of the batch conversion proxy * @param signerOrProvider the Web3 provider, or signer. Defaults to window.ethereum. + * @param paymentSettings paymentSettings is necessary for conversion payment approval */ export function encodeApproveErc20BatchConversion( request: ClientTypes.IRequestData, version: string, signerOrProvider: providers.Provider | Signer = getProvider(), + paymentSettings?: IConversionPaymentSettings, ): string { - const proxyAddress = getBatchConversionProxyAddress(request, version); - + const proxyAddress = getBatchConversionProxyAddress(request, version, paymentSettings); return encodeApproveAnyErc20( - request.currencyInfo.value, + getTokenAddress(request, paymentSettings), proxyAddress, getSigner(signerOrProvider), ); } + +/** + * Get the address of the token to interact with, + * if it is a conversion payment, the info is inside paymentSettings + * @param request request for an ERC20 payment with/out conversion + * @param paymentSettings paymentSettings is necessary for conversion payment + * */ +function getTokenAddress( + request: ClientTypes.IRequestData, + paymentSettings?: IConversionPaymentSettings, +): string { + return paymentSettings ? paymentSettings.currency!.value : request.currencyInfo.value; +} diff --git a/packages/payment-processor/src/payment/batch-proxy.ts b/packages/payment-processor/src/payment/batch-proxy.ts index 5a5070a1b..1b2862145 100644 --- a/packages/payment-processor/src/payment/batch-proxy.ts +++ b/packages/payment-processor/src/payment/batch-proxy.ts @@ -158,7 +158,10 @@ export function encodePayBatchRequest(requests: ClientTypes.IRequestData[]): str * @returns List with the args required by batch Eth and Erc20 functions, * @dev tokenAddresses returned is for batch Erc20 functions */ -export function getBatchArgs(requests: ClientTypes.IRequestData[]): { +export function getBatchArgs( + requests: ClientTypes.IRequestData[], + forcedPaymentType?: 'ETH' | 'ERC20', +): { tokenAddresses: Array; paymentAddresses: Array; amountsToPay: Array; @@ -173,7 +176,7 @@ export function getBatchArgs(requests: ClientTypes.IRequestData[]): { const feesToPay: Array = []; let feeAddressUsed = constants.AddressZero; - const paymentType = requests[0].currencyInfo.type; + const paymentType = forcedPaymentType ?? requests[0].currencyInfo.type; for (let i = 0; i < requests.length; i++) { if (paymentType === 'ETH') { validateEthFeeProxyRequest(requests[i]); @@ -213,7 +216,7 @@ export function getBatchArgs(requests: ClientTypes.IRequestData[]): { */ export function getBatchProxyAddress(request: ClientTypes.IRequestData, version: string): string { const pn = getPaymentNetworkExtension(request); - const pnId = pn?.id as unknown as PaymentTypes.PAYMENT_NETWORK_ID; + const pnId = (pn?.id as unknown) as PaymentTypes.PAYMENT_NETWORK_ID; if (!pnId) { throw new Error('No payment network Id'); } diff --git a/packages/payment-processor/src/payment/erc20.ts b/packages/payment-processor/src/payment/erc20.ts index 73cc58c49..bf96523c4 100644 --- a/packages/payment-processor/src/payment/erc20.ts +++ b/packages/payment-processor/src/payment/erc20.ts @@ -164,8 +164,8 @@ export function encodeApproveErc20( request: ClientTypes.IRequestData, signerOrProvider: providers.Provider | Signer = getProvider(), ): string { - const paymentNetworkId = getPaymentNetworkExtension(request) - ?.id as unknown as PaymentTypes.PAYMENT_NETWORK_ID; + const paymentNetworkId = (getPaymentNetworkExtension(request) + ?.id as unknown) as PaymentTypes.PAYMENT_NETWORK_ID; if (!paymentNetworkId) { throw new Error('No payment network Id'); } diff --git a/packages/payment-processor/src/payment/index.ts b/packages/payment-processor/src/payment/index.ts index 774b0c186..b6ed5eb19 100644 --- a/packages/payment-processor/src/payment/index.ts +++ b/packages/payment-processor/src/payment/index.ts @@ -336,3 +336,18 @@ const throwIfNotWeb3 = (request: ClientTypes.IRequestData) => { throw new UnsupportedPaymentChain(request.currencyInfo.network); } }; + +/** + * Input of batch conversion payment processor + * It contains requests, paymentSettings, amount and feeAmount. + * Currently, these requests must have the same PN, version, and batchFee + * Also used in Invoicing repository. + * @dev next step: paymentNetworkId could get more values options, see the "ref" + */ +export interface EnrichedRequest { + paymentNetworkId: 0 | 2; // ref in batchConversionPayment.sol + request: ClientTypes.IRequestData; + paymentSettings?: IConversionPaymentSettings; + amount?: BigNumberish; + feeAmount?: BigNumberish; +} diff --git a/packages/payment-processor/src/payment/utils.ts b/packages/payment-processor/src/payment/utils.ts index 727ed3a1c..0605470cb 100644 --- a/packages/payment-processor/src/payment/utils.ts +++ b/packages/payment-processor/src/payment/utils.ts @@ -293,7 +293,6 @@ export function validateConversionFeeProxyRequest( PaymentTypes.PAYMENT_NETWORK_ID.ANY_TO_ERC20_PROXY, ); const { tokensAccepted } = getRequestPaymentValues(request); - const requestCurrencyHash = path[0]; if (requestCurrencyHash.toLowerCase() !== getCurrencyHash(request.currencyInfo).toLowerCase()) { throw new Error(`The first entry of the path does not match the request currency`); diff --git a/packages/payment-processor/test/payment/any-to-erc20-batch-proxy.test.ts b/packages/payment-processor/test/payment/any-to-erc20-batch-proxy.test.ts index e0b883257..c50398b31 100644 --- a/packages/payment-processor/test/payment/any-to-erc20-batch-proxy.test.ts +++ b/packages/payment-processor/test/payment/any-to-erc20-batch-proxy.test.ts @@ -1,4 +1,4 @@ -import { Wallet, providers } from 'ethers'; +import { Wallet, providers, BigNumber } from 'ethers'; import { ClientTypes, @@ -7,34 +7,41 @@ import { PaymentTypes, RequestLogicTypes, } from '@requestnetwork/types'; +import { getErc20Balance } from '../../src/payment/erc20'; import Utils from '@requestnetwork/utils'; - -// conv -// import { IConversionPaymentSettings } from '../../src/index'; -// import { currencyManager } from './shared'; +import { revokeErc20Approval } from '@requestnetwork/payment-processor/src/payment/utils'; +import { EnrichedRequest, IConversionPaymentSettings } from '../../src/index'; +import { currencyManager } from './shared'; import { - EnrichedRequest, + approveErc20BatchConversionIfNeeded, getBatchConversionProxyAddress, payBatchConversionProxyRequest, -} from '../../src/payment/batch-proxy-conv'; -import { sameCurrencyValue } from './erc20-batch-proxy.test'; + prepareBatchConversionPaymentTransaction, +} from '../../src/payment/batch-conv-proxy'; +import { sameCurrencyValue } from './shared'; +import { batchConversionPaymentsArtifact } from '@requestnetwork/smart-contracts'; +import { UnsupportedCurrencyError } from '@requestnetwork/currency'; +import { ERC20__factory } from '@requestnetwork/smart-contracts/types'; /* eslint-disable no-magic-numbers */ /* eslint-disable @typescript-eslint/no-unused-expressions */ // Cf. ERC20Alpha in TestERC20.sol const erc20ContractAddress = '0x38cF23C52Bb4B13F051Aec09580a2dE845a7FA35'; -// const alphaPaymentSettings: IConversionPaymentSettings = { -// currency: { -// type: RequestLogicTypes.CURRENCY.ERC20, -// value: erc20ContractAddress, -// network: 'private', -// }, -// maxToSpend: BigNumber.from(2).pow(256).sub(1), -// currencyManager, -// }; - -// const batchConvFee = 100; +const alphaPaymentSettings: IConversionPaymentSettings = { + currency: { + type: RequestLogicTypes.CURRENCY.ERC20, + value: erc20ContractAddress, + network: 'private', + }, + maxToSpend: BigNumber.from(2).pow(250).sub(1), // is updated later + currencyManager, +}; + +/** Used to to calculate batch fees */ +const tenThousand = 10000; +const batchFee = 30; +const batchConvFee = 30; const batchConvVersion = '0.1.0'; const DAITokenAddress = '0x38cF23C52Bb4B13F051Aec09580a2dE845a7FA35'; const FAUTokenAddress = '0x9FBDa871d559710256a2502A2517b794B482Db40'; // TestERC20 address @@ -44,6 +51,8 @@ const feeAddress = '0xC5fdf4076b8F3A5357c5E395ab970B5B54098Fef'; const provider = new providers.JsonRpcProvider('http://localhost:8545'); const wallet = Wallet.fromMnemonic(mnemonic).connect(provider); +const EURExpectedAmount = 100; +const EURFeeAmount = 2; const validEuroRequest: ClientTypes.IRequestData = { balance: { balance: '0', @@ -61,7 +70,7 @@ const validEuroRequest: ClientTypes.IRequestData = { }, events: [], - expectedAmount: '100', + expectedAmount: EURExpectedAmount, extensions: { [PaymentTypes.PAYMENT_NETWORK_ID.ANY_TO_ERC20_PROXY]: { events: [], @@ -69,7 +78,7 @@ const validEuroRequest: ClientTypes.IRequestData = { type: ExtensionTypes.TYPE.PAYMENT_NETWORK, values: { feeAddress, - feeAmount: '2', + feeAmount: EURFeeAmount, paymentAddress, salt: 'salt', network: 'private', @@ -88,8 +97,9 @@ const validEuroRequest: ClientTypes.IRequestData = { timestamp: 0, version: '1.0', }; -validEuroRequest; //todo delete +const expectedAmount = 100000; +const feeAmount = 100; const validRequest: ClientTypes.IRequestData = { balance: { balance: '0', @@ -107,7 +117,7 @@ const validRequest: ClientTypes.IRequestData = { value: DAITokenAddress, }, events: [], - expectedAmount: '1000', + expectedAmount: expectedAmount, extensions: { [PaymentTypes.PAYMENT_NETWORK_ID.ERC20_FEE_PROXY_CONTRACT]: { events: [], @@ -115,7 +125,7 @@ const validRequest: ClientTypes.IRequestData = { type: ExtensionTypes.TYPE.PAYMENT_NETWORK, values: { feeAddress, - feeAmount: '2', + feeAmount: feeAmount, paymentAddress: paymentAddress, salt: 'salt', }, @@ -140,128 +150,439 @@ fauValidRequest.currencyInfo = { value: FAUTokenAddress, }; -const enrichedRequests: EnrichedRequest[] = [ - { - paymentNetworkId: 2, - request: validRequest, - }, - { - paymentNetworkId: 2, - request: fauValidRequest, - }, -]; - -// paymentNetworkId: 0 | 2; // ref in batchConversionPayment.sol -// request: ClientTypes.IRequestData; -// paymentSettings?: IConversionPaymentSettings; -// amount?: BigNumberish; -// feeAmount?: BigNumberish; +let request1: ClientTypes.IRequestData; +let request2: ClientTypes.IRequestData; +let enrichedRequests: EnrichedRequest[] = []; + +/** + * Calcul the expected amount to pay for X euro into Y tokens + * @param amount in fiat: EUR + */ +const expectedConversionAmount = (amount: number): BigNumber => { + // token decimals 10**18 + // amount amount / 100 + // AggEurUsd.sol x 1.20 + // AggDaiUsd.sol / 1.01 + return BigNumber.from(10).pow(18).mul(amount).div(100).mul(120).div(100).mul(100).div(101); +}; -const getData = ( +/** + * Gets the encoding depending of two ERC20 (no conversion) requests predefined: + * validRequest and fauValidRequest + */ +const expectedEncoding = ( request1: ClientTypes.IRequestData, request2: ClientTypes.IRequestData, ): string => { if (sameCurrencyValue(request1, validRequest) && sameCurrencyValue(request2, validRequest)) { - return '0xf0fa379f0000000000000000000000000000000000000000000000000000000000000040000000000000000000000000c5fdf4076b8f3a5357c5e395ab970b5b54098fef000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000c000000000000000000000000000000000000000000000000000000000000000e00000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000012000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000016000000000000000000000000000000000000000000000000000000000000001c000000000000000000000000000000000000000000000000000000000000002a0000000000000000000000000000000000000000000000000000000000000000200000000000000000000000038cf23c52bb4b13f051aec09580a2de845a7fa350000000000000000000000009fbda871d559710256a2502a2517b794b482db400000000000000000000000000000000000000000000000000000000000000002000000000000000000000000f17f52151ebef6c7334fad080c5704d77216b732000000000000000000000000f17f52151ebef6c7334fad080c5704d77216b732000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000003e800000000000000000000000000000000000000000000000000000000000003e8000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000000886dfbccad783599a000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000886dfbccad783599a000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000002'; - } else if ( - sameCurrencyValue(request1, validRequest) && - sameCurrencyValue(request2, fauValidRequest) - ) { - return '0xfa73314200000000000000000000000000000000000000000000000000000000000000c00000000000000000000000000000000000000000000000000000000000000120000000000000000000000000000000000000000000000000000000000000018000000000000000000000000000000000000000000000000000000000000001e000000000000000000000000000000000000000000000000000000000000002c0000000000000000000000000c5fdf4076b8f3a5357c5e395ab970b5b54098fef000000000000000000000000000000000000000000000000000000000000000200000000000000000000000038cf23c52bb4b13f051aec09580a2de845a7fa350000000000000000000000009fbda871d559710256a2502a2517b794b482db400000000000000000000000000000000000000000000000000000000000000002000000000000000000000000f17f52151ebef6c7334fad080c5704d77216b732000000000000000000000000f17f52151ebef6c7334fad080c5704d77216b732000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000003e800000000000000000000000000000000000000000000000000000000000003e8000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000000886dfbccad783599a000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000886dfbccad783599a000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000002'; + return '0xf0fa379f0000000000000000000000000000000000000000000000000000000000000040000000000000000000000000c5fdf4076b8f3a5357c5e395ab970b5b54098fef00000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000016000000000000000000000000000000000000000000000000000000000000001c000000000000000000000000000000000000000000000000000000000000002a0000000000000000000000000000000000000000000000000000000000000000200000000000000000000000038cf23c52bb4b13f051aec09580a2de845a7fa3500000000000000000000000038cf23c52bb4b13f051aec09580a2de845a7fa350000000000000000000000000000000000000000000000000000000000000002000000000000000000000000f17f52151ebef6c7334fad080c5704d77216b732000000000000000000000000f17f52151ebef6c7334fad080c5704d77216b732000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000186a000000000000000000000000000000000000000000000000000000000000186a0000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000000886dfbccad783599a000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000886dfbccad783599a000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000640000000000000000000000000000000000000000000000000000000000000064'; } else { - throw 'wrong requests'; + return '0xf0fa379f0000000000000000000000000000000000000000000000000000000000000040000000000000000000000000c5fdf4076b8f3a5357c5e395ab970b5b54098fef00000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000016000000000000000000000000000000000000000000000000000000000000001c000000000000000000000000000000000000000000000000000000000000002a0000000000000000000000000000000000000000000000000000000000000000200000000000000000000000038cf23c52bb4b13f051aec09580a2de845a7fa350000000000000000000000009fbda871d559710256a2502a2517b794b482db400000000000000000000000000000000000000000000000000000000000000002000000000000000000000000f17f52151ebef6c7334fad080c5704d77216b732000000000000000000000000f17f52151ebef6c7334fad080c5704d77216b732000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000186a000000000000000000000000000000000000000000000000000000000000186a0000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000000886dfbccad783599a000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000886dfbccad783599a000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000640000000000000000000000000000000000000000000000000000000000000064'; } }; -const testSuite = ( - suiteName: string, - requestTemplate1: ClientTypes.IRequestData, - requestTemplate2: ClientTypes.IRequestData, -) => { - describe(suiteName, () => { - let request1: ClientTypes.IRequestData; - let request2: ClientTypes.IRequestData; - - beforeEach(() => { - jest.restoreAllMocks(); - request1 = Utils.deepCopy(requestTemplate1) as ClientTypes.IRequestData; - - request2 = Utils.deepCopy(requestTemplate2) as ClientTypes.IRequestData; - request1; - }); +describe(`[Conversion]: erc20-batch-conversion-proxy`, () => { + beforeAll(async () => { + // revoke DAI approval + await revokeErc20Approval( + getBatchConversionProxyAddress(validEuroRequest, batchConvVersion, alphaPaymentSettings), + erc20ContractAddress, + wallet, + ); + // revoke FAU approval + await revokeErc20Approval( + getBatchConversionProxyAddress(fauValidRequest, batchConvVersion), + erc20ContractAddress, + wallet, + ); + // maxToSpend should be around the amountToPay * 1.03, it depends of the front end + // we do a simplification for the purpose of the test with: requestedAmount < maxToSpend < payeerBalance + alphaPaymentSettings.maxToSpend = '10000000000000000000000000000'; + }); - it('should throw an error if the request is not erc20', async () => { - request2.currencyInfo.type = RequestLogicTypes.CURRENCY.ETH; - const wrongEnrichedRequests = Utils.deepCopy(enrichedRequests); - wrongEnrichedRequests.push({ - paymentNetworkId: 2, + beforeEach(() => { + jest.restoreAllMocks(); + request1 = Utils.deepCopy(validEuroRequest) as ClientTypes.IRequestData; + request2 = Utils.deepCopy(validEuroRequest) as ClientTypes.IRequestData; + enrichedRequests = [ + { + paymentNetworkId: 0, + request: request1, + paymentSettings: alphaPaymentSettings, + }, + { + paymentNetworkId: 0, request: request2, - }); + paymentSettings: alphaPaymentSettings, + }, + ]; + }); + + describe('Throw an error', () => { + it('should throw an error if the token is not accepted', async () => { await expect( - payBatchConversionProxyRequest(wrongEnrichedRequests, batchConvVersion, wallet), + payBatchConversionProxyRequest( + [ + { + paymentNetworkId: 0, + request: request1, + paymentSettings: { + ...alphaPaymentSettings, + currency: { + ...alphaPaymentSettings.currency, + value: '0x775eb53d00dd0acd3ec1696472105d579b9b386b', + }, + } as IConversionPaymentSettings, + }, + ], + batchConvVersion, + wallet, + ), ).rejects.toThrowError( - 'request cannot be processed, or is not an pn-erc20-fee-proxy-contract request', + new UnsupportedCurrencyError({ + value: '0x775eb53d00dd0acd3ec1696472105d579b9b386b', + network: 'private', + }), ); }); - it("should throw an error if one request's currencyInfo has no value", async () => { + it('should throw an error if request has no extension', async () => { + const request = Utils.deepCopy(validEuroRequest); + request.extensions = [] as any; + + await expect( + payBatchConversionProxyRequest( + [ + { + paymentNetworkId: 0, + request: request, + paymentSettings: alphaPaymentSettings, + }, + ], + batchConvVersion, + wallet, + ), + ).rejects.toThrowError('no payment network found'); + }); + // Tests specific to batch conversion ERC20 + it('should throw an error if the request is not erc20', async () => { request2.currencyInfo.type = RequestLogicTypes.CURRENCY.ETH; - const wrongEnrichedRequests = Utils.deepCopy(enrichedRequests); - wrongEnrichedRequests.push({ - paymentNetworkId: 2, - request: request2, - }); await expect( - payBatchConversionProxyRequest(wrongEnrichedRequests, batchConvVersion, wallet), - ).rejects.toThrowError( - 'request cannot be processed, or is not an pn-erc20-fee-proxy-contract request', - ); + payBatchConversionProxyRequest(enrichedRequests, batchConvVersion, wallet), + ).rejects.toThrowError(`wrong request currencyInfo type`); }); - - it("should throw an error if one request's currencyInfo has no network", async () => { - request2.currencyInfo.network = ''; - const wrongEnrichedRequests = Utils.deepCopy(enrichedRequests); - wrongEnrichedRequests.push({ - paymentNetworkId: 2, - request: request2, - }); + it("should throw an error if one request's currencyInfo has no value", async () => { + request2.currencyInfo.value = ''; await expect( - payBatchConversionProxyRequest(wrongEnrichedRequests, batchConvVersion, wallet), - ).rejects.toThrowError( - 'request cannot be processed, or is not an pn-erc20-fee-proxy-contract request', - ); + payBatchConversionProxyRequest(enrichedRequests, batchConvVersion, wallet), + ).rejects.toThrowError("The currency symbol '' is unknown or not supported"); }); - it('should throw an error if request has no extension', async () => { request2.extensions = [] as any; - const wrongEnrichedRequests = Utils.deepCopy(enrichedRequests); - wrongEnrichedRequests.push({ - paymentNetworkId: 2, - request: request2, - }); await expect( - payBatchConversionProxyRequest(wrongEnrichedRequests, batchConvVersion, wallet), + payBatchConversionProxyRequest(enrichedRequests, batchConvVersion, wallet), ).rejects.toThrowError('no payment network found'); }); - it('should throw an error if there is a wrong version mapping', async () => { request2.extensions = { - [PaymentTypes.PAYMENT_NETWORK_ID.ERC20_FEE_PROXY_CONTRACT]: { - ...validRequest.extensions[PaymentTypes.PAYMENT_NETWORK_ID.ERC20_FEE_PROXY_CONTRACT], + [PaymentTypes.PAYMENT_NETWORK_ID.ANY_TO_ERC20_PROXY]: { + ...request2.extensions[PaymentTypes.PAYMENT_NETWORK_ID.ANY_TO_ERC20_PROXY], version: '0.3.0', }, }; - const wrongEnrichedRequests = Utils.deepCopy(enrichedRequests); - wrongEnrichedRequests.push({ - paymentNetworkId: 2, - request: request2, - }); await expect( - payBatchConversionProxyRequest(wrongEnrichedRequests, batchConvVersion, wallet), + payBatchConversionProxyRequest(enrichedRequests, batchConvVersion, wallet), ).rejects.toThrowError('Every payment network type and version must be identical'); }); + }); + + describe('payment', () => { + it('should consider override parameters', async () => { + const spy = jest.fn(); + const originalSendTransaction = wallet.sendTransaction.bind(wallet); + wallet.sendTransaction = spy; + await payBatchConversionProxyRequest( + [ + { + paymentNetworkId: 0, + request: request1, + paymentSettings: alphaPaymentSettings, + }, + ], + batchConvVersion, + wallet, + { gasPrice: '20000000000' }, + ); + expect(spy).toHaveBeenCalledWith({ + data: + '0xf0fa379f0000000000000000000000000000000000000000000000000000000000000040000000000000000000000000c5fdf4076b8f3a5357c5e395ab970b5b54098fef0000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000024000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000020000000000000000000000000f17f52151ebef6c7334fad080c5704d77216b7320000000000000000000000000000000000000000000000000000000005f5e10000000000000000000000000000000000000000000000000000000000000000e0000000000000000000000000000000000000000000000000000000000000016000000000000000000000000000000000000000000000000000000000001e84800000000000000000000000000000000000000000204fce5e3e250261100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000300000000000000000000000017b4158805772ced11225e77339f90beb5aae968000000000000000000000000775eb53d00dd0acd3ec1696472105d579b9b386b00000000000000000000000038cf23c52bb4b13f051aec09580a2de845a7fa35000000000000000000000000000000000000000000000000000000000000000886dfbccad783599a00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000c000000000000000000000000000000000000000000000000000000000000000e00000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000012000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000', + gasPrice: '20000000000', + to: getBatchConversionProxyAddress(request1, '0.1.0', alphaPaymentSettings), + value: 0, + }); + wallet.sendTransaction = originalSendTransaction; + }); + it('should convert and pay a request in EUR with ERC20', async () => { + // Approve the contract + const approvalTx = await approveErc20BatchConversionIfNeeded( + validEuroRequest, + wallet.address, + batchConvVersion, + wallet.provider, + alphaPaymentSettings, + ); + expect(approvalTx).toBeDefined(); + if (approvalTx) { + await approvalTx.wait(1); + } + + // Get the balances to compare after payment + const balanceEthBefore = await wallet.getBalance(); + const balanceTokenBefore = await ERC20__factory.connect( + erc20ContractAddress, + provider, + ).balanceOf(wallet.address); + + // Convert and pay + const tx = await payBatchConversionProxyRequest( + [ + { + paymentNetworkId: 0, + request: validEuroRequest, + paymentSettings: alphaPaymentSettings, + }, + ], + batchConvVersion, + wallet, + ); + const confirmedTx = await tx.wait(1); + expect(confirmedTx.status).toEqual(1); + expect(tx.hash).toBeDefined(); + // Get the new balances + const balanceEthAfter = await wallet.getBalance(); + const balanceTokenAfter = await ERC20__factory.connect( + erc20ContractAddress, + provider, + ).balanceOf(wallet.address); + // Check each balance + const amountToPay = expectedConversionAmount(EURExpectedAmount); + const feeToPay = expectedConversionAmount(EURFeeAmount); + const expectedAmountToPay = amountToPay + .add(feeToPay) + .mul(tenThousand + batchConvFee) + .div(tenThousand); + expect(BigNumber.from(balanceEthBefore).sub(balanceEthAfter).toNumber()).toBeGreaterThan(0); + expect( + BigNumber.from(balanceTokenBefore).sub(BigNumber.from(balanceTokenAfter)), + // Here is simplified approximation of the calcul + // expectedAmount: 1.00 + // feeAmount: + .02 + // = 1.02 + // AggEurUsd.sol x 1.20 + // AggDaiUsd.sol / 1.01 + // batchConvFee x 1.003 + // (exact result) = 1.215516831683168316 (over 18 decimals for this ERC20) + ).toEqual(expectedAmountToPay); + }); + it('should convert and pay two requests in EUR with ERC20', async () => { + // Get the balances to compare after payment + const balanceEthBefore = await wallet.getBalance(); + const balanceTokenBefore = await ERC20__factory.connect( + erc20ContractAddress, + provider, + ).balanceOf(wallet.address); + + // Convert and pay + const tx = await payBatchConversionProxyRequest( + [ + { + paymentNetworkId: 0, + request: validEuroRequest, + paymentSettings: alphaPaymentSettings, + }, + { + paymentNetworkId: 0, + request: validEuroRequest, + paymentSettings: alphaPaymentSettings, + }, + ], + batchConvVersion, + wallet, + ); + const confirmedTx = await tx.wait(1); + expect(confirmedTx.status).toEqual(1); + expect(tx.hash).toBeDefined(); + // Get the new balances + const balanceEthAfter = await wallet.getBalance(); + const balanceTokenAfter = await ERC20__factory.connect( + erc20ContractAddress, + provider, + ).balanceOf(wallet.address); + // Check each balance + const amountToPay = expectedConversionAmount(EURExpectedAmount).mul(2); // multiply by the number of requests: 2 + const feeToPay = expectedConversionAmount(EURFeeAmount).mul(2); // multiply by the number of requests: 2 + const expectedAmout = amountToPay + .add(feeToPay) + .mul(tenThousand + batchConvFee) + .div(tenThousand); + expect(BigNumber.from(balanceEthBefore).sub(balanceEthAfter).toNumber()).toBeGreaterThan(0); + expect(BigNumber.from(balanceTokenBefore).sub(BigNumber.from(balanceTokenAfter))).toEqual( + expectedAmout, + ); + }); + it('should convert and pay two requests in EUR with ERC20 and one ERC20 payment', async () => { + // Get the balances to compare after payment + const balanceEthBefore = await wallet.getBalance(); + const balanceTokenBefore = await ERC20__factory.connect( + erc20ContractAddress, + provider, + ).balanceOf(wallet.address); + + // Convert the two first requests and pay the three requests + const tx = await payBatchConversionProxyRequest( + [ + { + paymentNetworkId: 0, + request: validEuroRequest, + paymentSettings: alphaPaymentSettings, + }, + { + paymentNetworkId: 0, + request: validEuroRequest, + paymentSettings: alphaPaymentSettings, + }, + { + paymentNetworkId: 2, + request: validRequest, + }, + ], + batchConvVersion, + wallet, + ); + const confirmedTx = await tx.wait(1); + expect(confirmedTx.status).toEqual(1); + expect(tx.hash).toBeDefined(); + // Get the new balances + const balanceEthAfter = await wallet.getBalance(); + const balanceTokenAfter = await ERC20__factory.connect( + erc20ContractAddress, + provider, + ).balanceOf(wallet.address); + + // Check each balance + // amountToPay without fees + let amountToPay = expectedConversionAmount(EURExpectedAmount).mul(2); // multiply by the number of conversion requests: 2 + const feeToPay = expectedConversionAmount(EURFeeAmount).mul(2); // multiply by the number of conversion requests: 2 + // amountToPay with fees + amountToPay = amountToPay + .add(feeToPay) + .mul(tenThousand + batchConvFee) + .div(tenThousand); + + const noConvExpectedAmount = BigNumber.from(validRequest.expectedAmount); + const noConvAmountToPay = noConvExpectedAmount + .add(feeAmount) + .mul(tenThousand + batchFee) + .div(tenThousand); + + expect(BigNumber.from(balanceEthBefore).sub(balanceEthAfter).toNumber()).toBeGreaterThan(0); + expect(BigNumber.from(balanceTokenBefore).sub(BigNumber.from(balanceTokenAfter))).toEqual( + amountToPay.add(noConvAmountToPay), + ); + }); + }); +}); + +/** + * Test only ERC20 requests. No Conversion + * @param _request1 ERC20 request to test/pay, no conversion + * @param _request2 ERC20 request to test/pay, no conversion + */ +const testERC20Batch = ( + testDescription: string, + _request1: ClientTypes.IRequestData, + _request2: ClientTypes.IRequestData, +) => { + describe(`[No conversion]: erc20-batch-conversion-proxy ${testDescription}`, () => { + beforeAll(async () => { + // revoke DAI approval + await revokeErc20Approval( + getBatchConversionProxyAddress(validRequest, batchConvVersion), + erc20ContractAddress, + wallet, + ); + // revoke FAU approval + await revokeErc20Approval( + getBatchConversionProxyAddress(fauValidRequest, batchConvVersion), + erc20ContractAddress, + wallet, + ); + }); + + beforeEach(() => { + request1 = Utils.deepCopy(_request1); + request2 = Utils.deepCopy(_request2); + enrichedRequests = [ + { + paymentNetworkId: 2, + request: request1, + }, + { + paymentNetworkId: 2, + request: request2, + }, + ]; + }); + + describe('Throw an error', () => { + it('should throw an error if the request is not erc20', async () => { + request2.currencyInfo.type = RequestLogicTypes.CURRENCY.ETH; + await expect( + payBatchConversionProxyRequest(enrichedRequests, batchConvVersion, wallet), + ).rejects.toThrowError( + 'request cannot be processed, or is not an pn-erc20-fee-proxy-contract request', + ); + }); + + it("should throw an error if one request's currencyInfo has no value", async () => { + request2.currencyInfo.value = ''; + await expect( + payBatchConversionProxyRequest(enrichedRequests, batchConvVersion, wallet), + ).rejects.toThrowError( + 'request cannot be processed, or is not an pn-erc20-fee-proxy-contract request', + ); + }); + + it("should throw an error if one request's currencyInfo has no network", async () => { + request2.currencyInfo.network = ''; + await expect( + payBatchConversionProxyRequest(enrichedRequests, batchConvVersion, wallet), + ).rejects.toThrowError( + 'request cannot be processed, or is not an pn-erc20-fee-proxy-contract request', + ); + }); + + it('should throw an error if request has no extension', async () => { + request2.extensions = [] as any; + await expect( + payBatchConversionProxyRequest(enrichedRequests, batchConvVersion, wallet), + ).rejects.toThrowError('no payment network found'); + }); - describe('payBatchProxyRequest', () => { + it('should throw an error if there is a wrong version mapping', async () => { + request2.extensions = { + [PaymentTypes.PAYMENT_NETWORK_ID.ERC20_FEE_PROXY_CONTRACT]: { + ...validRequest.extensions[PaymentTypes.PAYMENT_NETWORK_ID.ERC20_FEE_PROXY_CONTRACT], + version: '0.3.0', + }, + }; + await expect( + payBatchConversionProxyRequest(enrichedRequests, batchConvVersion, wallet), + ).rejects.toThrowError('Every payment network type and version must be identical'); + }); + }); + + describe('payBatchConversionProxyRequest', () => { it('should consider override parameters', async () => { const spy = jest.fn(); const originalSendTransaction = wallet.sendTransaction.bind(wallet); @@ -270,149 +591,130 @@ const testSuite = ( gasPrice: '20000000000', }); expect(spy).toHaveBeenCalledWith({ - data: getData(request1, request2), + data: expectedEncoding(request1, request2), gasPrice: '20000000000', to: getBatchConversionProxyAddress(request1, '0.1.0'), value: 0, }); wallet.sendTransaction = originalSendTransaction; }); + it(`should pay 2 ERC20 requests with fees`, async () => { + // first approve the contract + const tmpRequest1 = Utils.deepCopy(request1); + const isMultiToken = !sameCurrencyValue(request1, request2); + console.log('isMultiToken', isMultiToken); + let amount = BigNumber.from(request1.expectedAmount); + if (!isMultiToken) { + amount = amount.add(BigNumber.from(request2.expectedAmount)); + tmpRequest1.expectedAmount = amount.toString(); + } else { + const ApprovalTx2 = await approveErc20BatchConversionIfNeeded( + request2, + wallet.address, + batchConvVersion, + wallet, + ); + if (ApprovalTx2) { + await ApprovalTx2.wait(1); + } + } + const approvalTx1 = await approveErc20BatchConversionIfNeeded( + tmpRequest1, + wallet.address, + batchConvVersion, + wallet, + ); + + if (approvalTx1) { + await approvalTx1.wait(1); + } + + // get the balance + const balanceEthBefore = await wallet.getBalance(); + const balanceErc20Before = await getErc20Balance(request1, wallet.address, provider); + const feeBalanceErc20Before = await getErc20Balance(request1, feeAddress, provider); + + const balanceErc20Before2 = await getErc20Balance(request2, wallet.address, provider); + const feeBalanceErc20Before2 = await getErc20Balance(request2, feeAddress, provider); + + // Batch payment + const tx = await payBatchConversionProxyRequest(enrichedRequests, batchConvVersion, wallet); + const confirmedTx = await tx.wait(1); + + const balanceEthAfter = await wallet.getBalance(); + const balanceErc20After = await getErc20Balance(request1, wallet.address, provider); + const feeBalanceErc20After = await getErc20Balance(request1, feeAddress, provider); + + expect(confirmedTx.status).toBe(1); + expect(tx.hash).not.toBeUndefined(); + expect(balanceEthAfter.lte(balanceEthBefore)).toBeTruthy(); // 'ETH balance should be lower' + + let feeAmountExpected = + feeAmount + + (expectedAmount * batchFee) / tenThousand + + (feeAmount + (expectedAmount * batchFee) / tenThousand); + if (isMultiToken) { + feeAmountExpected = feeAmount + (expectedAmount * batchFee) / tenThousand; // Will be sent on the 2nd token fee address + const balanceErc20After2 = await getErc20Balance(request2, wallet.address, provider); + const feeBalanceErc20After2 = await getErc20Balance(request2, feeAddress, provider); + // Compare request2 balances + expect(BigNumber.from(balanceErc20After2)).toEqual( + BigNumber.from(balanceErc20Before2).sub(expectedAmount + feeAmountExpected), + ); + expect(BigNumber.from(feeBalanceErc20After2)).toEqual( + BigNumber.from(feeBalanceErc20Before2).add(feeAmountExpected), + ); + } + // compare request 1 balances + expect(BigNumber.from(balanceErc20After)).toEqual( + BigNumber.from(balanceErc20Before).sub(amount.add(feeAmountExpected)), + ); + expect(BigNumber.from(feeBalanceErc20After)).toEqual( + BigNumber.from(feeBalanceErc20Before).add(feeAmountExpected), + ); + }); + }); + + describe('prepareBatchPaymentTransaction', () => { + it('should consider the version mapping', () => { + expect( + prepareBatchConversionPaymentTransaction( + [ + { + paymentNetworkId: 2, + request: { + ...request1, + extensions: { + [PaymentTypes.PAYMENT_NETWORK_ID.ERC20_FEE_PROXY_CONTRACT]: { + ...request1.extensions[ + PaymentTypes.PAYMENT_NETWORK_ID.ERC20_FEE_PROXY_CONTRACT + ], + version: '0.1.0', + }, + }, + } as any, + } as EnrichedRequest, + { + request: { + ...request2, + extensions: { + [PaymentTypes.PAYMENT_NETWORK_ID.ERC20_FEE_PROXY_CONTRACT]: { + ...request2.extensions[ + PaymentTypes.PAYMENT_NETWORK_ID.ERC20_FEE_PROXY_CONTRACT + ], + version: '0.1.0', + }, + }, + } as any, + } as EnrichedRequest, + ], + batchConvVersion, + ).to, + ).toBe(batchConversionPaymentsArtifact.getAddress('private', '0.1.0')); + }); }); - // it('should pay an ERC20 request with fees', async () => { - // // first approve the contract - // const tmpRequest = Utils.deepCopy(request1); - // let amount = 1000; - // const isMultiToken = !sameCurrencyValue(request1, request2); - - // if (!isMultiToken) { - // amount = 2 * amount; - // tmpRequest.expectedAmount = amount.toString(); - // } else { - // const ApprovalTx2 = await approveErc20BatchIfNeeded( - // request2, - // wallet.address, - // batchConvVersion, - // wallet, - // ); - // if (ApprovalTx2) { - // await ApprovalTx2.wait(1); - // } - // } - - // const approvalTx = await approveErc20BatchIfNeeded( - // tmpRequest, - // wallet.address, - // batchConvVersion, - // wallet, - // ); - - // if (approvalTx) { - // await approvalTx.wait(1); - // } - // request1.extensions['pn-erc20-fee-proxy-contract'].values.feeAmount = '6'; - - // // get the balance - // const balanceEthBefore = await wallet.getBalance(); - // const balanceErc20Before = await getErc20Balance(request1, wallet.address, provider); - // const feeBalanceErc20Before = await getErc20Balance(request1, feeAddress, provider); - - // const balanceErc20Before2 = await getErc20Balance(request2, wallet.address, provider); - // const feeBalanceErc20Before2 = await getErc20Balance(request2, feeAddress, provider); - - // // Batch payment - // const tx = await payBatchProxyRequest([request1, request2], batchConvVersion, wallet, batchFee); - // const confirmedTx = await tx.wait(1); - - // const balanceEthAfter = await wallet.getBalance(); - // const balanceErc20After = await getErc20Balance(request1, wallet.address, provider); - // const feeBalanceErc20After = await getErc20Balance(request1, feeAddress, provider); - - // expect(confirmedTx.status).toBe(1); - // expect(tx.hash).not.toBeUndefined(); - // expect(balanceEthAfter.lte(balanceEthBefore)).toBeTruthy(); // 'ETH balance should be lower' - - // let feeAmount = 6 + 10 + (2 + 10); - // if (isMultiToken) { - // feeAmount = 6 + 10; // (2 + 10) will be sent on the 2nd token fee address - // const balanceErc20After2 = await getErc20Balance(request2, wallet.address, provider); - // const feeBalanceErc20After2 = await getErc20Balance(request2, feeAddress, provider); - // // compare request 2 balances - // expect( - // BigNumber.from(balanceErc20After2).eq( - // BigNumber.from(balanceErc20Before2).sub(amount + (2 + 10)), - // ), - // ); - // expect( - // BigNumber.from(feeBalanceErc20After2).eq( - // BigNumber.from(feeBalanceErc20Before2).add(2 + 10), - // ), - // ); - // } - - // // compare request 1 balances - // expect( - // BigNumber.from(balanceErc20After).eq( - // BigNumber.from(balanceErc20Before).sub(amount + feeAmount), - // ), - // ); - // expect( - // BigNumber.from(feeBalanceErc20After).eq( - // BigNumber.from(feeBalanceErc20Before).add(feeAmount), - // ), - // ); - // }); - // }); - // describe('prepareBatchPaymentTransaction', () => { - // it('should consider the version mapping', () => { - // expect( - // prepareBatchPaymentTransaction( - // [ - // { - // ...request1, - // extensions: { - // [PaymentTypes.PAYMENT_NETWORK_ID.ERC20_FEE_PROXY_CONTRACT]: { - // ...request1.extensions[ - // PaymentTypes.PAYMENT_NETWORK_ID.ERC20_FEE_PROXY_CONTRACT - // ], - // version: '0.1.0', - // }, - // }, - // } as any, - // { - // ...request2, - // extensions: { - // [PaymentTypes.PAYMENT_NETWORK_ID.ERC20_FEE_PROXY_CONTRACT]: { - // ...request2.extensions[ - // PaymentTypes.PAYMENT_NETWORK_ID.ERC20_FEE_PROXY_CONTRACT - // ], - // version: '0.1.0', - // }, - // }, - // } as any, - // ], - // batchConvVersion, - // batchFee, - // ).to, - // ).toBe(batchPaymentsArtifact.getAddress('private', '0.1.0')); - // }); - // }); }); }; -describe('erc20-batch-proxy BIS', () => { - beforeEach(() => { - jest.restoreAllMocks(); - }); - // describe('getRequestPaymentValues', () => { - // it('handles ERC20', () => { - // const values = getRequestPaymentValues(validRequest); - // expect(values.feeAddress).toBe(feeAddress); - // expect(values.feeAmount).toBe('2'); - // expect(values.paymentAddress).toBe(paymentAddress); - // expect(values.paymentReference).toBe('86dfbccad783599a'); - // }); - // }); - - testSuite('encodePayErc20BatchRequest for one type of ERC20', validRequest, validRequest); - // testSuite('encodePayErc20BatchRequest using two different ERC20', validRequest, fauValidRequest); -}); +testERC20Batch('Same tokens', validRequest, validRequest); +testERC20Batch('Different tokens', validRequest, fauValidRequest); diff --git a/packages/payment-processor/test/payment/any-to-erc20-proxy.test.ts b/packages/payment-processor/test/payment/any-to-erc20-proxy.test.ts index d4a81bb9f..820b57fac 100644 --- a/packages/payment-processor/test/payment/any-to-erc20-proxy.test.ts +++ b/packages/payment-processor/test/payment/any-to-erc20-proxy.test.ts @@ -142,7 +142,8 @@ describe('conversion-erc20-fee-proxy', () => { }, ); expect(spy).toHaveBeenCalledWith({ - data: '0x3af2c012000000000000000000000000f17f52151ebef6c7334fad080c5704d77216b7320000000000000000000000000000000000000000000000000000000005f5e1000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000018000000000000000000000000000000000000000000000000000000000001e8480000000000000000000000000c5fdf4076b8f3a5357c5e395ab970b5b54098fefffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000300000000000000000000000017b4158805772ced11225e77339f90beb5aae968000000000000000000000000775eb53d00dd0acd3ec1696472105d579b9b386b00000000000000000000000038cf23c52bb4b13f051aec09580a2de845a7fa35000000000000000000000000000000000000000000000000000000000000000886dfbccad783599a000000000000000000000000000000000000000000000000', + data: + '0x3af2c012000000000000000000000000f17f52151ebef6c7334fad080c5704d77216b7320000000000000000000000000000000000000000000000000000000005f5e1000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000018000000000000000000000000000000000000000000000000000000000001e8480000000000000000000000000c5fdf4076b8f3a5357c5e395ab970b5b54098fefffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000300000000000000000000000017b4158805772ced11225e77339f90beb5aae968000000000000000000000000775eb53d00dd0acd3ec1696472105d579b9b386b00000000000000000000000038cf23c52bb4b13f051aec09580a2de845a7fa35000000000000000000000000000000000000000000000000000000000000000886dfbccad783599a000000000000000000000000000000000000000000000000', gasPrice: '20000000000', to: '0xdE5491f774F0Cb009ABcEA7326342E105dbb1B2E', value: 0, diff --git a/packages/payment-processor/test/payment/erc20-batch-proxy.test.ts b/packages/payment-processor/test/payment/erc20-batch-proxy.test.ts index 037147c34..dfe015db1 100644 --- a/packages/payment-processor/test/payment/erc20-batch-proxy.test.ts +++ b/packages/payment-processor/test/payment/erc20-batch-proxy.test.ts @@ -82,7 +82,7 @@ fauValidRequest.currencyInfo = { value: FAUTokenAddress, }; -export const sameCurrencyValue = ( +const sameCurrencyValue = ( requestA: ClientTypes.IRequestData, requestB: ClientTypes.IRequestData, ): boolean => { diff --git a/packages/payment-processor/test/payment/erc20-escrow-payment.test.ts b/packages/payment-processor/test/payment/erc20-escrow-payment.test.ts index b56074781..26af5217c 100644 --- a/packages/payment-processor/test/payment/erc20-escrow-payment.test.ts +++ b/packages/payment-processor/test/payment/erc20-escrow-payment.test.ts @@ -193,6 +193,10 @@ describe('erc20-escrow-payment tests:', () => { const escrowAfterBalance = await getErc20Balance(request, escrowAddress); const feeAfterBalance = await getErc20Balance(request, feeAddress); + const lala = (aa?: string): { aa?: string } => { + return { aa }; + }; + console.log('lala', lala); // Expect payer ERC20 balance should be lower. expect(BigNumber.from(payerAfterBalance).eq(BigNumber.from(payerBeforeBalance).sub(102))); // Expect fee ERC20 balance should be higher. diff --git a/packages/payment-processor/test/payment/shared.ts b/packages/payment-processor/test/payment/shared.ts index 734381a88..791b99883 100644 --- a/packages/payment-processor/test/payment/shared.ts +++ b/packages/payment-processor/test/payment/shared.ts @@ -1,5 +1,5 @@ import { CurrencyManager, CurrencyDefinition } from '@requestnetwork/currency'; -import { RequestLogicTypes } from '@requestnetwork/types'; +import { RequestLogicTypes, ClientTypes } from '@requestnetwork/types'; export const currencyManager = new CurrencyManager([ ...CurrencyManager.getDefaultList(), @@ -24,3 +24,10 @@ export const currencyManager = new CurrencyManager([ type: RequestLogicTypes.CURRENCY.ERC20, })), ]); + +export const sameCurrencyValue = ( + requestA: ClientTypes.IRequestData, + requestB: ClientTypes.IRequestData, +): boolean => { + return requestA.currencyInfo.value === requestB.currencyInfo.value; +}; From 0d44f525641cb9cae06ecdf8e3fc4c03ddbe1ef9 Mon Sep 17 00:00:00 2001 From: olivier7delf <55892112+olivier7delf@users.noreply.github.com> Date: Thu, 25 Aug 2022 12:30:18 +0200 Subject: [PATCH 065/105] prettier --- .../payment-processor/src/payment/any-to-erc20-proxy.ts | 9 ++------- .../payment-processor/src/payment/batch-conv-proxy.ts | 9 ++------- packages/payment-processor/src/payment/batch-proxy.ts | 2 +- packages/payment-processor/src/payment/erc20.ts | 4 ++-- packages/payment-processor/src/payment/utils.ts | 9 +++------ .../test/payment/any-to-erc20-batch-proxy.test.ts | 3 +-- .../test/payment/any-to-erc20-proxy.test.ts | 3 +-- 7 files changed, 12 insertions(+), 27 deletions(-) diff --git a/packages/payment-processor/src/payment/any-to-erc20-proxy.ts b/packages/payment-processor/src/payment/any-to-erc20-proxy.ts index e585b2bdf..03f7197a3 100644 --- a/packages/payment-processor/src/payment/any-to-erc20-proxy.ts +++ b/packages/payment-processor/src/payment/any-to-erc20-proxy.ts @@ -153,13 +153,8 @@ export function prepAnyToErc20ProxyRequest( feeAmountOverride, ); - const { - paymentReference, - paymentAddress, - feeAddress, - feeAmount, - maxRateTimespan, - } = getRequestPaymentValues(request); + const { paymentReference, paymentAddress, feeAddress, feeAmount, maxRateTimespan } = + getRequestPaymentValues(request); const amountToPay = padAmountForChainlink(getAmountToPay(request, amount), requestCurrency); const feeToPay = padAmountForChainlink(feeAmountOverride || feeAmount || 0, requestCurrency); diff --git a/packages/payment-processor/src/payment/batch-conv-proxy.ts b/packages/payment-processor/src/payment/batch-conv-proxy.ts index 2e74b64d2..d892336ff 100644 --- a/packages/payment-processor/src/payment/batch-conv-proxy.ts +++ b/packages/payment-processor/src/payment/batch-conv-proxy.ts @@ -173,13 +173,8 @@ export function encodePayBatchConversionRequest(enrichedRequests: EnrichedReques // Get values and add cryptpoDetails to metaDetails if (pn2requests.length > 0) { - const { - tokenAddresses, - paymentAddresses, - amountsToPay, - paymentReferences, - feesToPay, - } = getBatchArgs(pn2requests, 'ERC20'); + const { tokenAddresses, paymentAddresses, amountsToPay, paymentReferences, feesToPay } = + getBatchArgs(pn2requests, 'ERC20'); metaDetails.push({ paymentNetworkId: 2, diff --git a/packages/payment-processor/src/payment/batch-proxy.ts b/packages/payment-processor/src/payment/batch-proxy.ts index 1b2862145..49884b55a 100644 --- a/packages/payment-processor/src/payment/batch-proxy.ts +++ b/packages/payment-processor/src/payment/batch-proxy.ts @@ -216,7 +216,7 @@ export function getBatchArgs( */ export function getBatchProxyAddress(request: ClientTypes.IRequestData, version: string): string { const pn = getPaymentNetworkExtension(request); - const pnId = (pn?.id as unknown) as PaymentTypes.PAYMENT_NETWORK_ID; + const pnId = pn?.id as unknown as PaymentTypes.PAYMENT_NETWORK_ID; if (!pnId) { throw new Error('No payment network Id'); } diff --git a/packages/payment-processor/src/payment/erc20.ts b/packages/payment-processor/src/payment/erc20.ts index bf96523c4..73cc58c49 100644 --- a/packages/payment-processor/src/payment/erc20.ts +++ b/packages/payment-processor/src/payment/erc20.ts @@ -164,8 +164,8 @@ export function encodeApproveErc20( request: ClientTypes.IRequestData, signerOrProvider: providers.Provider | Signer = getProvider(), ): string { - const paymentNetworkId = (getPaymentNetworkExtension(request) - ?.id as unknown) as PaymentTypes.PAYMENT_NETWORK_ID; + const paymentNetworkId = getPaymentNetworkExtension(request) + ?.id as unknown as PaymentTypes.PAYMENT_NETWORK_ID; if (!paymentNetworkId) { throw new Error('No payment network Id'); } diff --git a/packages/payment-processor/src/payment/utils.ts b/packages/payment-processor/src/payment/utils.ts index 0605470cb..5ef52a46e 100644 --- a/packages/payment-processor/src/payment/utils.ts +++ b/packages/payment-processor/src/payment/utils.ts @@ -82,9 +82,7 @@ export function getPaymentNetworkExtension( * of a Request. * @param request */ -export function getRequestPaymentValues( - request: ClientTypes.IRequestData, -): { +export function getRequestPaymentValues(request: ClientTypes.IRequestData): { paymentAddress: string; paymentReference: string; feeAmount?: string; @@ -194,9 +192,8 @@ export function validateRequest( request: ClientTypes.IRequestData, paymentNetworkId: PaymentTypes.PAYMENT_NETWORK_ID, ): void { - const { feeAmount, feeAddress, expectedFlowRate, expectedStartDate } = getRequestPaymentValues( - request, - ); + const { feeAmount, feeAddress, expectedFlowRate, expectedStartDate } = + getRequestPaymentValues(request); let extension = request.extensions[paymentNetworkId]; // FIXME: updating the extension: not needed anymore when "invoicing" will use only ethFeeProxy diff --git a/packages/payment-processor/test/payment/any-to-erc20-batch-proxy.test.ts b/packages/payment-processor/test/payment/any-to-erc20-batch-proxy.test.ts index c50398b31..f1177dd97 100644 --- a/packages/payment-processor/test/payment/any-to-erc20-batch-proxy.test.ts +++ b/packages/payment-processor/test/payment/any-to-erc20-batch-proxy.test.ts @@ -314,8 +314,7 @@ describe(`[Conversion]: erc20-batch-conversion-proxy`, () => { { gasPrice: '20000000000' }, ); expect(spy).toHaveBeenCalledWith({ - data: - '0xf0fa379f0000000000000000000000000000000000000000000000000000000000000040000000000000000000000000c5fdf4076b8f3a5357c5e395ab970b5b54098fef0000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000024000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000020000000000000000000000000f17f52151ebef6c7334fad080c5704d77216b7320000000000000000000000000000000000000000000000000000000005f5e10000000000000000000000000000000000000000000000000000000000000000e0000000000000000000000000000000000000000000000000000000000000016000000000000000000000000000000000000000000000000000000000001e84800000000000000000000000000000000000000000204fce5e3e250261100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000300000000000000000000000017b4158805772ced11225e77339f90beb5aae968000000000000000000000000775eb53d00dd0acd3ec1696472105d579b9b386b00000000000000000000000038cf23c52bb4b13f051aec09580a2de845a7fa35000000000000000000000000000000000000000000000000000000000000000886dfbccad783599a00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000c000000000000000000000000000000000000000000000000000000000000000e00000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000012000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000', + data: '0xf0fa379f0000000000000000000000000000000000000000000000000000000000000040000000000000000000000000c5fdf4076b8f3a5357c5e395ab970b5b54098fef0000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000024000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000020000000000000000000000000f17f52151ebef6c7334fad080c5704d77216b7320000000000000000000000000000000000000000000000000000000005f5e10000000000000000000000000000000000000000000000000000000000000000e0000000000000000000000000000000000000000000000000000000000000016000000000000000000000000000000000000000000000000000000000001e84800000000000000000000000000000000000000000204fce5e3e250261100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000300000000000000000000000017b4158805772ced11225e77339f90beb5aae968000000000000000000000000775eb53d00dd0acd3ec1696472105d579b9b386b00000000000000000000000038cf23c52bb4b13f051aec09580a2de845a7fa35000000000000000000000000000000000000000000000000000000000000000886dfbccad783599a00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000c000000000000000000000000000000000000000000000000000000000000000e00000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000012000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000', gasPrice: '20000000000', to: getBatchConversionProxyAddress(request1, '0.1.0', alphaPaymentSettings), value: 0, diff --git a/packages/payment-processor/test/payment/any-to-erc20-proxy.test.ts b/packages/payment-processor/test/payment/any-to-erc20-proxy.test.ts index 820b57fac..d4a81bb9f 100644 --- a/packages/payment-processor/test/payment/any-to-erc20-proxy.test.ts +++ b/packages/payment-processor/test/payment/any-to-erc20-proxy.test.ts @@ -142,8 +142,7 @@ describe('conversion-erc20-fee-proxy', () => { }, ); expect(spy).toHaveBeenCalledWith({ - data: - '0x3af2c012000000000000000000000000f17f52151ebef6c7334fad080c5704d77216b7320000000000000000000000000000000000000000000000000000000005f5e1000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000018000000000000000000000000000000000000000000000000000000000001e8480000000000000000000000000c5fdf4076b8f3a5357c5e395ab970b5b54098fefffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000300000000000000000000000017b4158805772ced11225e77339f90beb5aae968000000000000000000000000775eb53d00dd0acd3ec1696472105d579b9b386b00000000000000000000000038cf23c52bb4b13f051aec09580a2de845a7fa35000000000000000000000000000000000000000000000000000000000000000886dfbccad783599a000000000000000000000000000000000000000000000000', + data: '0x3af2c012000000000000000000000000f17f52151ebef6c7334fad080c5704d77216b7320000000000000000000000000000000000000000000000000000000005f5e1000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000018000000000000000000000000000000000000000000000000000000000001e8480000000000000000000000000c5fdf4076b8f3a5357c5e395ab970b5b54098fefffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000300000000000000000000000017b4158805772ced11225e77339f90beb5aae968000000000000000000000000775eb53d00dd0acd3ec1696472105d579b9b386b00000000000000000000000038cf23c52bb4b13f051aec09580a2de845a7fa35000000000000000000000000000000000000000000000000000000000000000886dfbccad783599a000000000000000000000000000000000000000000000000', gasPrice: '20000000000', to: '0xdE5491f774F0Cb009ABcEA7326342E105dbb1B2E', value: 0, From ed525a4d672776d765b0f8f17bb8c5ad27c954e1 Mon Sep 17 00:00:00 2001 From: olivier7delf <55892112+olivier7delf@users.noreply.github.com> Date: Thu, 25 Aug 2022 16:06:32 +0200 Subject: [PATCH 066/105] clean tests and restore previous batchFee values --- .../BatchConversionErc20Payments.test.ts | 30 +++++++++++-------- 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/packages/smart-contracts/test/contracts/BatchConversionErc20Payments.test.ts b/packages/smart-contracts/test/contracts/BatchConversionErc20Payments.test.ts index 2278e6959..3599c317d 100644 --- a/packages/smart-contracts/test/contracts/BatchConversionErc20Payments.test.ts +++ b/packages/smart-contracts/test/contracts/BatchConversionErc20Payments.test.ts @@ -68,8 +68,8 @@ describe('contract: BatchConversionPayments', () => { // variables needed for chainlink and conversion payments let path: string[]; - let conversionToPayBis: BigNumber; - let conversionFeesBis: BigNumber; + let conversionToPay: BigNumber; + let conversionFees: BigNumber; // type required by Erc20 conversion batch function inputs type ConversionDetail = { @@ -108,16 +108,16 @@ describe('contract: BatchConversionPayments', () => { _chainlinkPath: ChainlinkConversionPath, ) => { const conversionToPayFull = await _chainlinkPath.getConversion(_requestAmount, _path); - conversionToPayBis = conversionToPayFull.result; + conversionToPay = conversionToPayFull.result; const conversionFeeFull = await _chainlinkPath.getConversion(_feeAmount, _path); - conversionFeesBis = conversionFeeFull.result; + conversionFees = conversionFeeFull.result; convDetail = { recipient: _recipient, requestAmount: _requestAmount, path: _path, paymentReference: referenceExample, feeAmount: _feeAmount, - maxToSpend: conversionToPayBis.add(conversionFeesBis).toString(), + maxToSpend: conversionToPay.add(conversionFees).toString(), maxRateTimespan: _maxRateTimespan, }; }; @@ -222,8 +222,8 @@ describe('contract: BatchConversionPayments', () => { const emitOneTx = ( result: Chai.Assertion, _convDetail: ConversionDetail, - _conversionToPay = conversionToPayBis, - _conversionFees = conversionFeesBis, + _conversionToPay = conversionToPay, + _conversionFees = conversionFees, _testErc20ConversionProxy = testErc20ConversionProxy, ) => { return result.to @@ -260,11 +260,11 @@ describe('contract: BatchConversionPayments', () => { const receipt = await tx.wait(); console.log(`gas consumption: `, receipt.gasUsed.toString()); } else { - await emitOneTx(expect(result), convDetail, conversionToPayBis, conversionFeesBis); + await emitOneTx(expect(result), convDetail, conversionToPay, conversionFees); } [fromDiffBalanceExpected, toDiffBalanceExpected, feeDiffBalanceExpected] = - expectedERC20Balances([conversionToPayBis], [conversionFeesBis], batchConvFee); + expectedERC20Balances([conversionToPay], [conversionFees], batchConvFee); }; /** @@ -295,11 +295,11 @@ describe('contract: BatchConversionPayments', () => { for (let i = 0; i < nTimes; i++) { convDetails = convDetails.concat([convDetail, convDetail2]); conversionsToPay_results = conversionsToPay_results.concat([ - conversionToPayBis, + conversionToPay, conversionToPayFull2.result, ]); conversionsFees_results = conversionsFees_results.concat([ - conversionFeesBis, + conversionFees, conversionFeesFull2.result, ]); } @@ -484,6 +484,12 @@ describe('contract: BatchConversionPayments', () => { ); }); + after(async () => { + // restore previous values for consistency + await testBatchConversionProxy.setBatchFee(30); + await testBatchConversionProxy.setBatchConversionFee(30); + }); + describe(useBatchRouter ? 'Through batchRouter' : 'Without batchRouter', () => { describe('batchERC20ConversionPaymentsMultiTokens with DAI', async () => { it('allows to transfer DAI tokens for USD payment', async () => { @@ -521,7 +527,7 @@ describe('contract: BatchConversionPayments', () => { }); it('cannot transfer if max to spend too low', async function () { - convDetail.maxToSpend = conversionToPayBis.add(conversionFeesBis).sub(1).toString(); + convDetail.maxToSpend = conversionToPay.add(conversionFees).sub(1).toString(); await expect(batchConvFunction(argTemplate([convDetail]), feeAddress)).to.be.revertedWith( 'Amount to pay is over the user limit', ); From 69cb5ab5b4b259e28926855221c5a94e05ee7c19 Mon Sep 17 00:00:00 2001 From: olivier7delf <55892112+olivier7delf@users.noreply.github.com> Date: Thu, 25 Aug 2022 16:36:41 +0200 Subject: [PATCH 067/105] test refacto to simplify batchConvFunction --- .../BatchConversionErc20Payments.test.ts | 69 +++++++++---------- 1 file changed, 33 insertions(+), 36 deletions(-) diff --git a/packages/smart-contracts/test/contracts/BatchConversionErc20Payments.test.ts b/packages/smart-contracts/test/contracts/BatchConversionErc20Payments.test.ts index 3599c317d..b9280962b 100644 --- a/packages/smart-contracts/test/contracts/BatchConversionErc20Payments.test.ts +++ b/packages/smart-contracts/test/contracts/BatchConversionErc20Payments.test.ts @@ -93,9 +93,6 @@ describe('contract: BatchConversionPayments', () => { optional?: any, ) => Promise; - /** Format arguments so they can be used by batchConvFunction */ - let argTemplate: Function; - /** * @notice it gets the conversions including fees to be paid, and it set the convDetail input */ @@ -189,30 +186,31 @@ describe('contract: BatchConversionPayments', () => { * @param _signer */ const setBatchConvFunction = async (useBatchRouter: boolean, _signer: Signer) => { - if (useBatchRouter) { - batchConvFunction = testBatchConversionProxy.connect(_signer).batchRouter; - argTemplate = (convDetails: ConversionDetail[]) => { - return [ - { - paymentNetworkId: '0', - conversionDetails: convDetails, - cryptoDetails: { - tokenAddresses: [], - recipients: [], - amounts: [], - paymentReferences: [], - feeAmounts: [], - }, - }, - ]; - }; - } else { - batchConvFunction = - testBatchConversionProxy.connect(_signer).batchERC20ConversionPaymentsMultiTokens; - argTemplate = (convDetails: ConversionDetail[]) => { - return convDetails; - }; - } + batchConvFunction = ( + convDetails: ConversionDetail[], + feeAddress: string, + ): Promise => { + return useBatchRouter + ? testBatchConversionProxy.connect(_signer).batchRouter( + [ + { + paymentNetworkId: '0', + conversionDetails: convDetails, + cryptoDetails: { + tokenAddresses: [], + recipients: [], + amounts: [], + paymentReferences: [], + feeAmounts: [], + }, + }, + ], + feeAddress, + ) + : testBatchConversionProxy + .connect(_signer) + .batchERC20ConversionPaymentsMultiTokens(convDetails, feeAddress); + }; }; /** @@ -253,14 +251,13 @@ describe('contract: BatchConversionPayments', () => { const onePaymentBatchConv = async (path: string[]) => { await getConvToPayAndConvDetail(to, path, amountInFiat, feesAmountInFiat, 0, chainlinkPath); - const result = batchConvFunction(argTemplate([convDetail]), feeAddress); + const result = batchConvFunction([convDetail], feeAddress); + await emitOneTx(expect(result), convDetail, conversionToPay, conversionFees); if (logGas) { const tx = await result; await tx.wait(1); const receipt = await tx.wait(); console.log(`gas consumption: `, receipt.gasUsed.toString()); - } else { - await emitOneTx(expect(result), convDetail, conversionToPay, conversionFees); } [fromDiffBalanceExpected, toDiffBalanceExpected, feeDiffBalanceExpected] = @@ -309,7 +306,7 @@ describe('contract: BatchConversionPayments', () => { const toOldBalance2 = await testERC20b.balanceOf(to); const feeOldBalance2 = await testERC20b.balanceOf(feeAddress); - const tx = await batchConvFunction(argTemplate(convDetails), feeAddress); + const tx = await batchConvFunction(convDetails, feeAddress); if (logGas) { const receipt = await tx.wait(); console.log(`${2 * nTimes} req, gas consumption: `, receipt.gasUsed.toString()); @@ -521,14 +518,14 @@ describe('contract: BatchConversionPayments', () => { it('cannot transfer with invalid path', async function () { const wrongPath = [EUR_hash, ETH_hash, DAI_address]; convDetail.path = wrongPath; - await expect(batchConvFunction(argTemplate([convDetail]), feeAddress)).to.be.revertedWith( + await expect(batchConvFunction([convDetail], feeAddress)).to.be.revertedWith( 'revert No aggregator found', ); }); it('cannot transfer if max to spend too low', async function () { convDetail.maxToSpend = conversionToPay.add(conversionFees).sub(1).toString(); - await expect(batchConvFunction(argTemplate([convDetail]), feeAddress)).to.be.revertedWith( + await expect(batchConvFunction([convDetail], feeAddress)).to.be.revertedWith( 'Amount to pay is over the user limit', ); }); @@ -536,7 +533,7 @@ describe('contract: BatchConversionPayments', () => { it('cannot transfer if rate is too old', async function () { convDetail.maxRateTimespan = 10; - await expect(batchConvFunction(argTemplate([convDetail]), feeAddress)).to.be.revertedWith( + await expect(batchConvFunction([convDetail], feeAddress)).to.be.revertedWith( 'aggregator rate is outdated', ); }); @@ -544,7 +541,7 @@ describe('contract: BatchConversionPayments', () => { it('Not enough allowance', async function () { // signer4 connect to the batch function setBatchConvFunction(useBatchRouter, signer4); - await expect(batchConvFunction(argTemplate([convDetail]), feeAddress)).to.be.revertedWith( + await expect(batchConvFunction([convDetail], feeAddress)).to.be.revertedWith( 'Insufficient allowance for batch to pay', ); // reset: signer1 connect to the batch function @@ -559,7 +556,7 @@ describe('contract: BatchConversionPayments', () => { // signer4 connect to the batch function setBatchConvFunction(useBatchRouter, signer4); - await expect(batchConvFunction(argTemplate([convDetail]), feeAddress)).to.be.revertedWith( + await expect(batchConvFunction([convDetail], feeAddress)).to.be.revertedWith( 'not enough funds, including fees', ); From 6bbccca98e131076b6b512b7c303dd145789caec Mon Sep 17 00:00:00 2001 From: olivier7delf <55892112+olivier7delf@users.noreply.github.com> Date: Fri, 26 Aug 2022 10:05:06 +0200 Subject: [PATCH 068/105] update batch conversion contract --- .../smart-contracts/src/contracts/BatchConversionPayments.sol | 2 +- packages/smart-contracts/src/contracts/BatchPaymentsPublic.sol | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/smart-contracts/src/contracts/BatchConversionPayments.sol b/packages/smart-contracts/src/contracts/BatchConversionPayments.sol index 9b2e56539..5c846b8a6 100644 --- a/packages/smart-contracts/src/contracts/BatchConversionPayments.sol +++ b/packages/smart-contracts/src/contracts/BatchConversionPayments.sol @@ -103,7 +103,7 @@ contract BatchConversionPayments is BatchPaymentsPublic { * For single payment network payments, it is more efficient to use the suited batch function. */ function batchRouter(MetaDetail[] calldata metaDetails, address _feeAddress) external payable { - require(metaDetails.length < 4, 'more than 4 conversionDetails'); + require(metaDetails.length < 6, 'more than 5 conversionDetails'); for (uint256 i = 0; i < metaDetails.length; i++) { MetaDetail calldata metaConversionDetail = metaDetails[i]; if (metaConversionDetail.paymentNetworkId == 0) { diff --git a/packages/smart-contracts/src/contracts/BatchPaymentsPublic.sol b/packages/smart-contracts/src/contracts/BatchPaymentsPublic.sol index 95181065d..a3b78906a 100644 --- a/packages/smart-contracts/src/contracts/BatchPaymentsPublic.sol +++ b/packages/smart-contracts/src/contracts/BatchPaymentsPublic.sol @@ -57,7 +57,7 @@ contract BatchPaymentsPublic is Ownable { /** * This contract is non-payable. Making an ETH payment with conversion requires the contract to accept incoming ETH. - * See the end of `paymentEthConversionProxy.transferWithReferenceAndFee` where the leftover is given back. + * @dev See the end of `paymentEthConversionProxy.transferWithReferenceAndFee` where the leftover is given back. */ receive() external payable { require(payerAuthorized || msg.value == 0, 'Non-payable'); From b961daf86c29d6554f8ac888c77606c70c8f1618 Mon Sep 17 00:00:00 2001 From: olivier7delf <55892112+olivier7delf@users.noreply.github.com> Date: Fri, 26 Aug 2022 10:53:32 +0200 Subject: [PATCH 069/105] clean escrow --- .../test/payment/erc20-escrow-payment.test.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/packages/payment-processor/test/payment/erc20-escrow-payment.test.ts b/packages/payment-processor/test/payment/erc20-escrow-payment.test.ts index b0ece6216..58b7bb9b0 100644 --- a/packages/payment-processor/test/payment/erc20-escrow-payment.test.ts +++ b/packages/payment-processor/test/payment/erc20-escrow-payment.test.ts @@ -193,10 +193,6 @@ describe('erc20-escrow-payment tests:', () => { const escrowAfterBalance = await getErc20Balance(request, escrowAddress); const feeAfterBalance = await getErc20Balance(request, feeAddress); - const lala = (aa?: string): { aa?: string } => { - return { aa }; - }; - console.log('lala', lala); // Expect payer ERC20 balance should be lower. expect(BigNumber.from(payerAfterBalance)).toEqual( BigNumber.from(payerBeforeBalance).sub(102), From 714590f2c16d1056a7614e464a627a9c5400152c Mon Sep 17 00:00:00 2001 From: olivier7delf <55892112+olivier7delf@users.noreply.github.com> Date: Fri, 26 Aug 2022 13:35:13 +0200 Subject: [PATCH 070/105] version with errors on 2 proxies --- .../scripts-create2/compute-one-address.ts | 1 + .../scripts-create2/constructor-args.ts | 14 ++++ .../contract-setup/adminTasks.ts | 80 ++++++++++++++++++- .../setupBatchConversionPayments.ts | 79 ++++++++++++++++++ .../contract-setup/setupBatchPayments.ts | 2 +- .../scripts-create2/contract-setup/setups.ts | 5 ++ .../smart-contracts/scripts-create2/deploy.ts | 8 ++ .../smart-contracts/scripts-create2/utils.ts | 19 +++-- .../smart-contracts/scripts-create2/verify.ts | 7 ++ .../BatchConversionPayments/index.ts | 4 + 10 files changed, 206 insertions(+), 13 deletions(-) create mode 100644 packages/smart-contracts/scripts-create2/contract-setup/setupBatchConversionPayments.ts diff --git a/packages/smart-contracts/scripts-create2/compute-one-address.ts b/packages/smart-contracts/scripts-create2/compute-one-address.ts index f2803616e..0e558437b 100644 --- a/packages/smart-contracts/scripts-create2/compute-one-address.ts +++ b/packages/smart-contracts/scripts-create2/compute-one-address.ts @@ -57,6 +57,7 @@ export const computeCreate2DeploymentAddressesFromList = async ( case 'Erc20ConversionProxy': case 'ERC20EscrowToPay': case 'BatchPayments': + case 'BatchConversionPayments': case 'ERC20SwapToConversion': { try { const constructorArgs = getConstructorArgs(contract, hre.network.name); diff --git a/packages/smart-contracts/scripts-create2/constructor-args.ts b/packages/smart-contracts/scripts-create2/constructor-args.ts index db9d4952e..cac996383 100644 --- a/packages/smart-contracts/scripts-create2/constructor-args.ts +++ b/packages/smart-contracts/scripts-create2/constructor-args.ts @@ -49,6 +49,20 @@ export const getConstructorArgs = (contract: string, network?: string): string[] getAdminWalletAddress(contract), ]; } + case 'BatchConversionPayments': { + if (!network) { + throw new Error( + 'Batch conversion contract requires network parameter to get correct address of erc20FeeProxy, erc20ConversionFeeProxy, ethereumFeeProxy, and ethereumConversionFeeProxy', + ); + } + return [ + '0x0000000000000000000000000000000000000000', + '0x0000000000000000000000000000000000000000', + '0x0000000000000000000000000000000000000000', + '0x0000000000000000000000000000000000000000', + getAdminWalletAddress(contract), + ]; + } default: return []; } diff --git a/packages/smart-contracts/scripts-create2/contract-setup/adminTasks.ts b/packages/smart-contracts/scripts-create2/contract-setup/adminTasks.ts index a2c8d6ffc..f689dbd63 100644 --- a/packages/smart-contracts/scripts-create2/contract-setup/adminTasks.ts +++ b/packages/smart-contracts/scripts-create2/contract-setup/adminTasks.ts @@ -6,7 +6,14 @@ import { BigNumber } from 'ethers'; // Fees: 0.5% export const REQUEST_SWAP_FEES = 5; // Batch Fees: .3% -export const BATCH_FEE = 3; +/** + * BATCH_FEE_DEPRECATED is only used with batchProxy (NOT with batchConversionProxy) + */ +export const BATCH_FEE_DEPRECATED = 3; +export const BATCH_FEE = 30; + +// Batch Fees: .3% +export const BATCH_CONVERSION_FEE = 30; export const updateChainlinkConversionPath = async ( contract: any, @@ -50,16 +57,41 @@ export const updateRequestSwapFees = async ( } }; +/** + * Updates batch and batchConversion batchFee dependant of the proxy selected + * @param batchConversionProxy batchConversionProxy must be specified because + * it impact the calcul of the batch fees + */ export const updateBatchPaymentFees = async ( contract: any, nonce: number, gasPrice: BigNumber, + batchConversionProxy = true, ): Promise => { + const batchFee = batchConversionProxy ? BATCH_FEE : BATCH_FEE_DEPRECATED; const currentFees = await contract.batchFee(); - if (currentFees !== BATCH_FEE) { + if (currentFees !== batchFee) { // Log is useful to have a direct view on was is being updated - console.log(`currentFees: ${currentFees.toString()}, new fees: ${BATCH_FEE}`); - await contract.setBatchFee(BATCH_FEE, { nonce: nonce, gasPrice: gasPrice }); + console.log(`BatchFees, currentFees: ${currentFees.toString()}, new fees: ${batchFee}`); + await contract.setBatchFee(batchFee, { nonce: nonce, gasPrice: gasPrice }); + } +}; + +export const updateBatchConversionPaymentFees = async ( + contract: any, + nonce: number, + gasPrice: BigNumber, +): Promise => { + const currentFees = await contract.batchConversionFee(); + if (currentFees !== BATCH_CONVERSION_FEE) { + // Log is useful to have a direct view on was is being updated + console.log( + `BatchConversionFees, currentFees: ${currentFees.toString()}, new fees: ${BATCH_CONVERSION_FEE}`, + ); + await contract.setBatchConversionFee(BATCH_CONVERSION_FEE, { + nonce: nonce, + gasPrice: gasPrice, + }); } }; @@ -96,3 +128,43 @@ export const updatePaymentEthFeeProxy = async ( }); } }; + +/** + * Update the address of a payment proxy used by batch conversion contract + */ +export const updateBatchConversionPaymentProxy = async ( + contract: any, + network: string, + nonce: number, + gasPrice: BigNumber, + proxyName: 'eth' | 'ethConversion' | 'erc20' | 'erc20Conversion', +): Promise => { + let proxyAddress: string; + let batchSetProxy: any; + let currentAddress: string; + if (proxyName === 'eth') { + proxyAddress = artifacts.ethereumFeeProxyArtifact.getAddress(network); + batchSetProxy = contract.setPaymentEthProxy; + currentAddress = await contract.paymentEthProxy(); + } else if (proxyName === 'ethConversion') { + proxyAddress = artifacts.ethConversionArtifact.getAddress(network); + batchSetProxy = contract.setPaymentEthConversionProxy; + currentAddress = await contract.paymentEthConversionProxy(); + } else if (proxyName === 'erc20') { + proxyAddress = artifacts.erc20FeeProxyArtifact.getAddress(network); + batchSetProxy = contract.setPaymentErc20Proxy; + currentAddress = await contract.paymentErc20Proxy(); + } else { + // "erc20Conversion" + proxyAddress = artifacts.erc20ConversionProxy.getAddress(network); + batchSetProxy = contract.setPaymentErc20ConversionProxy; + currentAddress = await contract.paymentErc20ConversionProxy(); + } + + if (currentAddress !== proxyAddress) { + await batchSetProxy(proxyAddress, { + nonce: nonce, + gasPrice: gasPrice, + }); + } +}; diff --git a/packages/smart-contracts/scripts-create2/contract-setup/setupBatchConversionPayments.ts b/packages/smart-contracts/scripts-create2/contract-setup/setupBatchConversionPayments.ts new file mode 100644 index 000000000..aff1ab77c --- /dev/null +++ b/packages/smart-contracts/scripts-create2/contract-setup/setupBatchConversionPayments.ts @@ -0,0 +1,79 @@ +import { batchConversionPaymentsArtifact } from '../../src/lib'; +import { HardhatRuntimeEnvironmentExtended } from '../types'; +import utils from '@requestnetwork/utils'; +import { + updateBatchPaymentFees, + updateBatchConversionPaymentFees, + updateBatchConversionPaymentProxy, +} from './adminTasks'; + +/** + * Updates the values of the batch fees of the BatchConversionPayments contract, if needed + * @param contractAddress address of the BatchConversionPayments Proxy + * @param hre Hardhat runtime environment + */ +export const setupBatchConversionPayments = async ( + contractAddress: string, + hre: HardhatRuntimeEnvironmentExtended, +): Promise => { + // Setup contract parameters + const batchConversionPaymentContract = new hre.ethers.Contract( + contractAddress, + batchConversionPaymentsArtifact.getContractAbi(), + ); + await Promise.all( + hre.config.xdeploy.networks.map(async (network) => { + let provider; + if (network === 'celo') { + provider = utils.getCeloProvider(); + } else { + provider = utils.getDefaultProvider(network); + } + const wallet = new hre.ethers.Wallet(hre.config.xdeploy.signer, provider); + const signer = wallet.connect(provider); + const batchConversionPaymentConnected = await batchConversionPaymentContract.connect(signer); + const adminNonce = await signer.getTransactionCount(); + const gasPrice = await provider.getGasPrice(); + + // start from the adminNonce, increase gasPrice if needed + const gasCoef = 3; + await Promise.all([ + updateBatchPaymentFees(batchConversionPaymentConnected, adminNonce, gasPrice.mul(gasCoef)), + updateBatchConversionPaymentFees( + batchConversionPaymentConnected, + adminNonce + 1, + gasPrice.mul(gasCoef), + ), + updateBatchConversionPaymentProxy( + batchConversionPaymentConnected, + network, + adminNonce + 2, + gasPrice.mul(gasCoef), + 'erc20', + ), + updateBatchConversionPaymentProxy( + batchConversionPaymentConnected, + network, + adminNonce + 3, + gasPrice.mul(gasCoef), + 'eth', + ), + updateBatchConversionPaymentProxy( + batchConversionPaymentConnected, + network, + adminNonce + 4, + gasPrice.mul(gasCoef), + 'erc20Conversion', + ), + updateBatchConversionPaymentProxy( + batchConversionPaymentConnected, + network, + adminNonce + 5, + gasPrice.mul(gasCoef), + 'ethConversion', + ), + ]); + }), + ); + console.log('Setup for setupBatchConversionPayment successfull'); +}; diff --git a/packages/smart-contracts/scripts-create2/contract-setup/setupBatchPayments.ts b/packages/smart-contracts/scripts-create2/contract-setup/setupBatchPayments.ts index 78fa718e0..e48818107 100644 --- a/packages/smart-contracts/scripts-create2/contract-setup/setupBatchPayments.ts +++ b/packages/smart-contracts/scripts-create2/contract-setup/setupBatchPayments.ts @@ -37,7 +37,7 @@ export const setupBatchPayments = async ( // start from the adminNonce, increase gasPrice if needed await Promise.all([ - updateBatchPaymentFees(batchPaymentConnected, adminNonce, gasPrice.mul(2)), + updateBatchPaymentFees(batchPaymentConnected, adminNonce, gasPrice.mul(2), false), updatePaymentErc20FeeProxy(batchPaymentConnected, network, adminNonce + 1, gasPrice.mul(2)), updatePaymentEthFeeProxy(batchPaymentConnected, network, adminNonce + 2, gasPrice.mul(2)), ]); diff --git a/packages/smart-contracts/scripts-create2/contract-setup/setups.ts b/packages/smart-contracts/scripts-create2/contract-setup/setups.ts index cda45affa..79cc90b1b 100644 --- a/packages/smart-contracts/scripts-create2/contract-setup/setups.ts +++ b/packages/smart-contracts/scripts-create2/contract-setup/setups.ts @@ -1,6 +1,7 @@ import { HardhatRuntimeEnvironmentExtended } from '../types'; import { setupETHConversionProxy } from './setupETHConversionProxy'; import { setupBatchPayments } from './setupBatchPayments'; +import { setupBatchConversionPayments } from './setupBatchConversionPayments'; import { setupERC20SwapToConversion } from './setupERC20SwapToConversion'; /** @@ -27,6 +28,10 @@ export const setupContract = async ( await setupBatchPayments(contractAddress, hre); break; } + case 'BatchConversionPayments': { + await setupBatchConversionPayments(contractAddress, hre); + break; + } default: { console.log('Contract name not found'); break; diff --git a/packages/smart-contracts/scripts-create2/deploy.ts b/packages/smart-contracts/scripts-create2/deploy.ts index 23dbfc931..737314bf3 100644 --- a/packages/smart-contracts/scripts-create2/deploy.ts +++ b/packages/smart-contracts/scripts-create2/deploy.ts @@ -5,6 +5,7 @@ import { xdeploy } from './xdeployer'; import { getConstructorArgs } from './constructor-args'; import { setupERC20SwapToConversion } from './contract-setup'; import { setupBatchPayments } from './contract-setup/setupBatchPayments'; +import { setupBatchConversionPayments } from './contract-setup/setupBatchConversionPayments'; // Deploys, set up the contracts and returns the address export const deployOneWithCreate2 = async ( @@ -79,6 +80,13 @@ export const deployWithCreate2FromList = async ( await setupBatchPayments(address, hre); break; } + case 'BatchConversionPayments': { + const network = hre.config.xdeploy.networks[0]; + const constructorArgs = getConstructorArgs(contract, network); + const address = await deployOneWithCreate2({ contract, constructorArgs }, hre); + await setupBatchConversionPayments(address, hre); + break; + } // Other cases to add when necessary default: throw new Error(`The contract ${contract} is not to be deployed using the CREATE2 scheme`); diff --git a/packages/smart-contracts/scripts-create2/utils.ts b/packages/smart-contracts/scripts-create2/utils.ts index 6956b562a..f4fe002fa 100644 --- a/packages/smart-contracts/scripts-create2/utils.ts +++ b/packages/smart-contracts/scripts-create2/utils.ts @@ -7,14 +7,15 @@ import * as artifacts from '../src/lib'; * If you want to skip deploying one or more, then comment them out in the list bellow. */ export const create2ContractDeploymentList = [ - 'EthereumProxy', - 'EthereumFeeProxy', - 'EthConversionProxy', - 'ERC20FeeProxy', - 'Erc20ConversionProxy', - 'ERC20SwapToConversion', - 'ERC20EscrowToPay', - 'BatchPayments', + // 'EthereumProxy', + // 'EthereumFeeProxy', + // 'EthConversionProxy', + // 'ERC20FeeProxy', + // 'Erc20ConversionProxy', + // 'ERC20SwapToConversion', + // 'ERC20EscrowToPay', + // 'BatchPayments', + 'BatchConversionPayments', ]; /** @@ -49,6 +50,8 @@ export const getArtifact = (contract: string): artifacts.ContractArtifact Date: Fri, 26 Aug 2022 13:38:15 +0200 Subject: [PATCH 071/105] make proxies public --- .../src/contracts/BatchConversionPayments.sol | 4 +-- .../BatchConversionPayments/0.1.0.json | 26 +++++++++++++++++++ 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/packages/smart-contracts/src/contracts/BatchConversionPayments.sol b/packages/smart-contracts/src/contracts/BatchConversionPayments.sol index 5c846b8a6..0c97c48d0 100644 --- a/packages/smart-contracts/src/contracts/BatchConversionPayments.sol +++ b/packages/smart-contracts/src/contracts/BatchConversionPayments.sol @@ -22,8 +22,8 @@ import './BatchPaymentsPublic.sol'; contract BatchConversionPayments is BatchPaymentsPublic { using SafeERC20 for IERC20; - IERC20ConversionProxy paymentErc20ConversionProxy; - IEthConversionProxy paymentEthConversionProxy; + IERC20ConversionProxy public paymentErc20ConversionProxy; + IEthConversionProxy public paymentEthConversionProxy; uint256 public batchConversionFee; diff --git a/packages/smart-contracts/src/lib/artifacts/BatchConversionPayments/0.1.0.json b/packages/smart-contracts/src/lib/artifacts/BatchConversionPayments/0.1.0.json index 377ce324c..33a9211dd 100644 --- a/packages/smart-contracts/src/lib/artifacts/BatchConversionPayments/0.1.0.json +++ b/packages/smart-contracts/src/lib/artifacts/BatchConversionPayments/0.1.0.json @@ -407,6 +407,19 @@ "stateMutability": "view", "type": "function" }, + { + "inputs": [], + "name": "paymentErc20ConversionProxy", + "outputs": [ + { + "internalType": "contract IERC20ConversionProxy", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, { "inputs": [], "name": "paymentErc20Proxy", @@ -420,6 +433,19 @@ "stateMutability": "view", "type": "function" }, + { + "inputs": [], + "name": "paymentEthConversionProxy", + "outputs": [ + { + "internalType": "contract IEthConversionProxy", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, { "inputs": [], "name": "paymentEthProxy", From f0437d5531290b592290fdae2b738ec4d0086355 Mon Sep 17 00:00:00 2001 From: olivier7delf <55892112+olivier7delf@users.noreply.github.com> Date: Fri, 26 Aug 2022 13:42:12 +0200 Subject: [PATCH 072/105] batch conversion deploy on rinkeby --- .../contract-setup/setupBatchConversionPayments.ts | 2 +- .../src/lib/artifacts/BatchConversionPayments/index.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/smart-contracts/scripts-create2/contract-setup/setupBatchConversionPayments.ts b/packages/smart-contracts/scripts-create2/contract-setup/setupBatchConversionPayments.ts index aff1ab77c..e1530772d 100644 --- a/packages/smart-contracts/scripts-create2/contract-setup/setupBatchConversionPayments.ts +++ b/packages/smart-contracts/scripts-create2/contract-setup/setupBatchConversionPayments.ts @@ -36,7 +36,7 @@ export const setupBatchConversionPayments = async ( const gasPrice = await provider.getGasPrice(); // start from the adminNonce, increase gasPrice if needed - const gasCoef = 3; + const gasCoef = 2; await Promise.all([ updateBatchPaymentFees(batchConversionPaymentConnected, adminNonce, gasPrice.mul(gasCoef)), updateBatchConversionPaymentFees( diff --git a/packages/smart-contracts/src/lib/artifacts/BatchConversionPayments/index.ts b/packages/smart-contracts/src/lib/artifacts/BatchConversionPayments/index.ts index 426e39107..1bb98c03f 100644 --- a/packages/smart-contracts/src/lib/artifacts/BatchConversionPayments/index.ts +++ b/packages/smart-contracts/src/lib/artifacts/BatchConversionPayments/index.ts @@ -14,8 +14,8 @@ export const batchConversionPaymentsArtifact = new ContractArtifact Date: Fri, 26 Aug 2022 13:43:44 +0200 Subject: [PATCH 073/105] batch conversion deployed on goerli --- .../src/lib/artifacts/BatchConversionPayments/index.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/smart-contracts/src/lib/artifacts/BatchConversionPayments/index.ts b/packages/smart-contracts/src/lib/artifacts/BatchConversionPayments/index.ts index 1bb98c03f..9b2356b17 100644 --- a/packages/smart-contracts/src/lib/artifacts/BatchConversionPayments/index.ts +++ b/packages/smart-contracts/src/lib/artifacts/BatchConversionPayments/index.ts @@ -17,6 +17,10 @@ export const batchConversionPaymentsArtifact = new ContractArtifact Date: Fri, 26 Aug 2022 13:48:22 +0200 Subject: [PATCH 074/105] clean test --- .../test/payment/any-to-erc20-batch-proxy.test.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/payment-processor/test/payment/any-to-erc20-batch-proxy.test.ts b/packages/payment-processor/test/payment/any-to-erc20-batch-proxy.test.ts index f1177dd97..4129f5163 100644 --- a/packages/payment-processor/test/payment/any-to-erc20-batch-proxy.test.ts +++ b/packages/payment-processor/test/payment/any-to-erc20-batch-proxy.test.ts @@ -264,8 +264,7 @@ describe(`[Conversion]: erc20-batch-conversion-proxy`, () => { ), ).rejects.toThrowError('no payment network found'); }); - // Tests specific to batch conversion ERC20 - it('should throw an error if the request is not erc20', async () => { + it('should throw an error if the request is ETH', async () => { request2.currencyInfo.type = RequestLogicTypes.CURRENCY.ETH; await expect( payBatchConversionProxyRequest(enrichedRequests, batchConvVersion, wallet), @@ -601,7 +600,6 @@ const testERC20Batch = ( // first approve the contract const tmpRequest1 = Utils.deepCopy(request1); const isMultiToken = !sameCurrencyValue(request1, request2); - console.log('isMultiToken', isMultiToken); let amount = BigNumber.from(request1.expectedAmount); if (!isMultiToken) { amount = amount.add(BigNumber.from(request2.expectedAmount)); From d09aa033b735256f951fb7cc58f2148f7f869cbe Mon Sep 17 00:00:00 2001 From: olivier7delf <55892112+olivier7delf@users.noreply.github.com> Date: Fri, 26 Aug 2022 13:50:53 +0200 Subject: [PATCH 075/105] update contract to make proxies public --- .../src/contracts/BatchConversionPayments.sol | 4 +-- .../BatchConversionPayments/0.1.0.json | 26 +++++++++++++++++++ 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/packages/smart-contracts/src/contracts/BatchConversionPayments.sol b/packages/smart-contracts/src/contracts/BatchConversionPayments.sol index 5c846b8a6..0c97c48d0 100644 --- a/packages/smart-contracts/src/contracts/BatchConversionPayments.sol +++ b/packages/smart-contracts/src/contracts/BatchConversionPayments.sol @@ -22,8 +22,8 @@ import './BatchPaymentsPublic.sol'; contract BatchConversionPayments is BatchPaymentsPublic { using SafeERC20 for IERC20; - IERC20ConversionProxy paymentErc20ConversionProxy; - IEthConversionProxy paymentEthConversionProxy; + IERC20ConversionProxy public paymentErc20ConversionProxy; + IEthConversionProxy public paymentEthConversionProxy; uint256 public batchConversionFee; diff --git a/packages/smart-contracts/src/lib/artifacts/BatchConversionPayments/0.1.0.json b/packages/smart-contracts/src/lib/artifacts/BatchConversionPayments/0.1.0.json index 377ce324c..33a9211dd 100644 --- a/packages/smart-contracts/src/lib/artifacts/BatchConversionPayments/0.1.0.json +++ b/packages/smart-contracts/src/lib/artifacts/BatchConversionPayments/0.1.0.json @@ -407,6 +407,19 @@ "stateMutability": "view", "type": "function" }, + { + "inputs": [], + "name": "paymentErc20ConversionProxy", + "outputs": [ + { + "internalType": "contract IERC20ConversionProxy", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, { "inputs": [], "name": "paymentErc20Proxy", @@ -420,6 +433,19 @@ "stateMutability": "view", "type": "function" }, + { + "inputs": [], + "name": "paymentEthConversionProxy", + "outputs": [ + { + "internalType": "contract IEthConversionProxy", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, { "inputs": [], "name": "paymentEthProxy", From ab9f4b7a186b8bb7fa9f05acfe75a47ba786f808 Mon Sep 17 00:00:00 2001 From: olivier7delf <55892112+olivier7delf@users.noreply.github.com> Date: Fri, 26 Aug 2022 15:40:31 +0200 Subject: [PATCH 076/105] fix import getPaymentNetworkExtension and its paymentReference undefined possibility --- .../payment-processor/src/payment/any-to-erc20-proxy.ts | 4 +++- .../payment-processor/src/payment/batch-conv-proxy.ts | 9 +++------ 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/packages/payment-processor/src/payment/any-to-erc20-proxy.ts b/packages/payment-processor/src/payment/any-to-erc20-proxy.ts index 03f7197a3..cdc9de379 100644 --- a/packages/payment-processor/src/payment/any-to-erc20-proxy.ts +++ b/packages/payment-processor/src/payment/any-to-erc20-proxy.ts @@ -155,7 +155,9 @@ export function prepAnyToErc20ProxyRequest( const { paymentReference, paymentAddress, feeAddress, feeAmount, maxRateTimespan } = getRequestPaymentValues(request); - + if (!paymentReference) { + throw new Error('paymentReference is missing'); + } const amountToPay = padAmountForChainlink(getAmountToPay(request, amount), requestCurrency); const feeToPay = padAmountForChainlink(feeAmountOverride || feeAmount || 0, requestCurrency); return { diff --git a/packages/payment-processor/src/payment/batch-conv-proxy.ts b/packages/payment-processor/src/payment/batch-conv-proxy.ts index d892336ff..83f45c960 100644 --- a/packages/payment-processor/src/payment/batch-conv-proxy.ts +++ b/packages/payment-processor/src/payment/batch-conv-proxy.ts @@ -3,14 +3,11 @@ import { batchConversionPaymentsArtifact } from '@requestnetwork/smart-contracts import { BatchConversionPayments__factory } from '@requestnetwork/smart-contracts/types'; import { ClientTypes, ExtensionTypes, RequestLogicTypes } from '@requestnetwork/types'; import { ITransactionOverrides } from './transaction-overrides'; +import { comparePnTypeAndVersion, getProvider, getRequestPaymentValues, getSigner } from './utils'; import { - comparePnTypeAndVersion, + padAmountForChainlink, getPaymentNetworkExtension, - getProvider, - getRequestPaymentValues, - getSigner, -} from './utils'; -import { padAmountForChainlink } from '@requestnetwork/payment-detection'; +} from '@requestnetwork/payment-detection'; import { IPreparedTransaction } from './prepared-transaction'; import { EnrichedRequest, IConversionPaymentSettings } from './index'; import { checkRequestAndGetPathAndCurrency } from './any-to-erc20-proxy'; From b35d5d1070482d83e67da18106bf171af8c24a43 Mon Sep 17 00:00:00 2001 From: olivier7delf <55892112+olivier7delf@users.noreply.github.com> Date: Fri, 26 Aug 2022 15:42:58 +0200 Subject: [PATCH 077/105] fix error message --- .../test/payment/any-to-erc20-batch-proxy.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/payment-processor/test/payment/any-to-erc20-batch-proxy.test.ts b/packages/payment-processor/test/payment/any-to-erc20-batch-proxy.test.ts index 4129f5163..a7247c3d2 100644 --- a/packages/payment-processor/test/payment/any-to-erc20-batch-proxy.test.ts +++ b/packages/payment-processor/test/payment/any-to-erc20-batch-proxy.test.ts @@ -274,7 +274,7 @@ describe(`[Conversion]: erc20-batch-conversion-proxy`, () => { request2.currencyInfo.value = ''; await expect( payBatchConversionProxyRequest(enrichedRequests, batchConvVersion, wallet), - ).rejects.toThrowError("The currency symbol '' is unknown or not supported"); + ).rejects.toThrowError("The currency '' is unknown or not supported"); }); it('should throw an error if request has no extension', async () => { request2.extensions = [] as any; From 16b2b94e957e46d1332d352199fd094507c1bd66 Mon Sep 17 00:00:00 2001 From: olivier7delf <55892112+olivier7delf@users.noreply.github.com> Date: Tue, 30 Aug 2022 12:48:31 +0200 Subject: [PATCH 078/105] rename batch conversion file --- packages/payment-processor/src/index.ts | 1 + .../payment/{batch-conv-proxy.ts => batch-conversion-proxy.ts} | 0 .../test/payment/any-to-erc20-batch-proxy.test.ts | 2 +- 3 files changed, 2 insertions(+), 1 deletion(-) rename packages/payment-processor/src/payment/{batch-conv-proxy.ts => batch-conversion-proxy.ts} (100%) diff --git a/packages/payment-processor/src/index.ts b/packages/payment-processor/src/index.ts index fc48075b2..0bbf676fb 100644 --- a/packages/payment-processor/src/index.ts +++ b/packages/payment-processor/src/index.ts @@ -9,6 +9,7 @@ export * from './payment/near-input-data'; export * from './payment/eth-proxy'; export * from './payment/eth-fee-proxy'; export * from './payment/batch-proxy'; +export * from './payment/batch-conversion-proxy'; export * from './payment/swap-conversion-erc20'; export * from './payment/swap-any-to-erc20'; export * from './payment/swap-erc20'; diff --git a/packages/payment-processor/src/payment/batch-conv-proxy.ts b/packages/payment-processor/src/payment/batch-conversion-proxy.ts similarity index 100% rename from packages/payment-processor/src/payment/batch-conv-proxy.ts rename to packages/payment-processor/src/payment/batch-conversion-proxy.ts diff --git a/packages/payment-processor/test/payment/any-to-erc20-batch-proxy.test.ts b/packages/payment-processor/test/payment/any-to-erc20-batch-proxy.test.ts index a7247c3d2..506a28a56 100644 --- a/packages/payment-processor/test/payment/any-to-erc20-batch-proxy.test.ts +++ b/packages/payment-processor/test/payment/any-to-erc20-batch-proxy.test.ts @@ -17,7 +17,7 @@ import { getBatchConversionProxyAddress, payBatchConversionProxyRequest, prepareBatchConversionPaymentTransaction, -} from '../../src/payment/batch-conv-proxy'; +} from '../../src/payment/batch-conversion-proxy'; import { sameCurrencyValue } from './shared'; import { batchConversionPaymentsArtifact } from '@requestnetwork/smart-contracts'; import { UnsupportedCurrencyError } from '@requestnetwork/currency'; From 9f746bc3a3464b059647ce52df36937fcbad3b6b Mon Sep 17 00:00:00 2001 From: olivier7delf <55892112+olivier7delf@users.noreply.github.com> Date: Wed, 31 Aug 2022 10:10:46 +0200 Subject: [PATCH 079/105] test refacto: delete emitOneTx - path and add before - adminSigner --- .../BatchConversionErc20Payments.test.ts | 128 +++++++++--------- .../BatchConversionEthPayments.test.ts | 2 +- 2 files changed, 64 insertions(+), 66 deletions(-) diff --git a/packages/smart-contracts/test/contracts/BatchConversionErc20Payments.test.ts b/packages/smart-contracts/test/contracts/BatchConversionErc20Payments.test.ts index b9280962b..70f87829d 100644 --- a/packages/smart-contracts/test/contracts/BatchConversionErc20Payments.test.ts +++ b/packages/smart-contracts/test/contracts/BatchConversionErc20Payments.test.ts @@ -23,13 +23,14 @@ import Utils from '@requestnetwork/utils'; // set to true to log batch payments's gas consumption const logGas = false; -describe('contract: BatchConversionPayments', () => { +describe('contract: BatchConversionPayments', async () => { let from: string; let to: string; let feeAddress: string; let batchAddress: string; let signer1: Signer; let signer4: Signer; + let adminSigner: Signer; // constants used to set up batch conversion proxy, and also requests payment const batchFee = 50; @@ -67,7 +68,6 @@ describe('contract: BatchConversionPayments', () => { let feeDiffBalanceExpected: BigNumber; // variables needed for chainlink and conversion payments - let path: string[]; let conversionToPay: BigNumber; let conversionFees: BigNumber; @@ -214,45 +214,31 @@ describe('contract: BatchConversionPayments', () => { }; /** - * Function used to check the events emitted from the batch conversion proxy. - * @dev referenceExample and feeAddress are not args because there values never change + * @notice update convDetail, do an ERC20 conversion batch payment with a single payment inside and calculate the balances + * @param path to update the convDetail */ - const emitOneTx = ( - result: Chai.Assertion, - _convDetail: ConversionDetail, - _conversionToPay = conversionToPay, - _conversionFees = conversionFees, - _testErc20ConversionProxy = testErc20ConversionProxy, - ) => { - return result.to - .emit(_testErc20ConversionProxy, 'TransferWithConversionAndReference') + const onePaymentBatchConv = async (path: string[]) => { + await getConvToPayAndConvDetail(to, path, amountInFiat, feesAmountInFiat, 0, chainlinkPath); + + const result = batchConvFunction([convDetail], feeAddress); + await expect(result) + .to.emit(testErc20ConversionProxy, 'TransferWithConversionAndReference') .withArgs( - _convDetail.requestAmount, - ethers.utils.getAddress(_convDetail.path[0]), + convDetail.requestAmount, + ethers.utils.getAddress(convDetail.path[0]), ethers.utils.keccak256(referenceExample), - _convDetail.feeAmount, + convDetail.feeAmount, '0', ) - .to.emit(_testErc20ConversionProxy, 'TransferWithReferenceAndFee') + .to.emit(testErc20ConversionProxy, 'TransferWithReferenceAndFee') .withArgs( ethers.utils.getAddress(DAI_address), - ethers.utils.getAddress(_convDetail.recipient), - _conversionToPay, + ethers.utils.getAddress(convDetail.recipient), + conversionToPay, ethers.utils.keccak256(referenceExample), - _conversionFees, + conversionFees, feeAddress, ); - }; - - /** - * @notice update convDetail, do an ERC20 conversion batch payment with a single payment inside and calculate the balances - * @param path to update the convDetail - */ - const onePaymentBatchConv = async (path: string[]) => { - await getConvToPayAndConvDetail(to, path, amountInFiat, feesAmountInFiat, 0, chainlinkPath); - - const result = batchConvFunction([convDetail], feeAddress); - await emitOneTx(expect(result), convDetail, conversionToPay, conversionFees); if (logGas) { const tx = await result; await tx.wait(1); @@ -401,7 +387,25 @@ describe('contract: BatchConversionPayments', () => { [fromDiffBalanceExpected, toDiffBalanceExpected, feeDiffBalanceExpected] = expectedERC20Balances([BigNumber.from(amount)], [BigNumber.from(feeAmount)], batchFee, false); }; - + before(async () => { + [from, from, to, feeAddress] = (await ethers.getSigners()).map((s) => s.address); + [adminSigner, signer1, signer4, signer4, signer4] = await ethers.getSigners(); + + chainlinkPath = chainlinkConversionPath.connect(network.name, signer1); + + erc20FeeProxy = await new ERC20FeeProxy__factory(adminSigner).deploy(); + ethereumFeeProxy = await new EthereumFeeProxy__factory(adminSigner).deploy(); + testErc20ConversionProxy = await new Erc20ConversionProxy__factory(adminSigner).deploy( + erc20FeeProxy.address, + chainlinkPath.address, + await adminSigner.getAddress(), + ); + testEthConversionProxy = await new EthConversionProxy__factory(adminSigner).deploy( + ethereumFeeProxy.address, + chainlinkPath.address, + ETH_hash, + ); + }); /** * @notice it contains all the tests related to the ERC20 batch payment, and its context required * @param useBatchRouter allows to use the function "batchERC20ConversionPaymentsMultiTokens" @@ -409,23 +413,8 @@ describe('contract: BatchConversionPayments', () => { */ for (const useBatchRouter of [true, false]) { before(async () => { - [from, to, feeAddress] = (await ethers.getSigners()).map((s) => s.address); - [signer1, signer4, signer4, signer4] = await ethers.getSigners(); - chainlinkPath = chainlinkConversionPath.connect(network.name, signer1); - erc20FeeProxy = await new ERC20FeeProxy__factory(signer1).deploy(); - ethereumFeeProxy = await new EthereumFeeProxy__factory(signer1).deploy(); - testErc20ConversionProxy = await new Erc20ConversionProxy__factory(signer1).deploy( - erc20FeeProxy.address, - chainlinkPath.address, - await signer1.getAddress(), - ); - testEthConversionProxy = await new EthConversionProxy__factory(signer1).deploy( - ethereumFeeProxy.address, - chainlinkPath.address, - ETH_hash, - ); - testBatchConversionProxy = batchConversionPaymentsArtifact.connect(network.name, signer1); - + // TODO deploy batch proxy -> then no need to revoke approvals + testBatchConversionProxy = batchConversionPaymentsArtifact.connect(network.name, adminSigner); // update batch payment proxies, and batch fees await testBatchConversionProxy.setPaymentErc20Proxy(erc20FeeProxy.address); await testBatchConversionProxy.setPaymentEthProxy(ethereumFeeProxy.address); @@ -433,18 +422,22 @@ describe('contract: BatchConversionPayments', () => { testErc20ConversionProxy.address, ); await testBatchConversionProxy.setPaymentEthConversionProxy(testEthConversionProxy.address); - - await testBatchConversionProxy.setBatchFee(batchFee); - await testBatchConversionProxy.setBatchConversionFee(batchConvFee); - // set ERC20 tokens DAI_address = localERC20AlphaArtifact.getAddress(network.name); - testERC20 = new TestERC20__factory(signer1).attach(DAI_address); + testERC20 = new TestERC20__factory(adminSigner).attach(DAI_address); FAU_address = secondLocalERC20AlphaArtifact.getAddress(network.name); - testERC20b = new TestERC20__factory(signer1).attach(FAU_address); + testERC20b = new TestERC20__factory(adminSigner).attach(FAU_address); batchAddress = testBatchConversionProxy.address; + await testBatchConversionProxy.setBatchFee(batchFee); + await testBatchConversionProxy.setBatchConversionFee(batchConvFee); + + await testERC20.transfer(from, BigNumber.from(thousandWith18Decimal)); + testERC20 = TestERC20__factory.connect(testERC20.address, signer1); + await testERC20b.transfer(from, BigNumber.from(thousandWith18Decimal)); + testERC20b = TestERC20__factory.connect(testERC20b.address, signer1); + testBatchConversionProxy = batchConversionPaymentsArtifact.connect(network.name, signer1); setBatchConvFunction(useBatchRouter, signer1); }); @@ -464,8 +457,14 @@ describe('contract: BatchConversionPayments', () => { feeOldBalance = await testERC20.balanceOf(feeAddress); // create a default convDetail - path = [USD_hash, DAI_address]; - getConvToPayAndConvDetail(to, path, amountInFiat, feesAmountInFiat, 0, chainlinkPath); + getConvToPayAndConvDetail( + to, + [USD_hash, DAI_address], + amountInFiat, + feesAmountInFiat, + 0, + chainlinkPath, + ); }); afterEach(async () => { @@ -483,25 +482,24 @@ describe('contract: BatchConversionPayments', () => { after(async () => { // restore previous values for consistency - await testBatchConversionProxy.setBatchFee(30); - await testBatchConversionProxy.setBatchConversionFee(30); + await testBatchConversionProxy.connect(adminSigner).setBatchFee(30); + await testBatchConversionProxy.connect(adminSigner).setBatchConversionFee(30); }); describe(useBatchRouter ? 'Through batchRouter' : 'Without batchRouter', () => { + // TODO reunite both describe describe('batchERC20ConversionPaymentsMultiTokens with DAI', async () => { it('allows to transfer DAI tokens for USD payment', async () => { - await onePaymentBatchConv(path); + await onePaymentBatchConv([USD_hash, DAI_address]); }); it('allows to transfer DAI tokens for EUR payment', async () => { - path = [EUR_hash, USD_hash, DAI_address]; - await onePaymentBatchConv(path); + await onePaymentBatchConv([EUR_hash, USD_hash, DAI_address]); }); it('allows to transfer 2 transactions DAI tokens for USD payment', async function () { - await manyPaymentsBatchConv(path, 1); + await manyPaymentsBatchConv([USD_hash, DAI_address], 1); }); it('allows to transfer DAI tokens for EUR payment', async () => { - path = [EUR_hash, USD_hash, DAI_address]; - await onePaymentBatchConv(path); + await onePaymentBatchConv([EUR_hash, USD_hash, DAI_address]); }); it('allows to transfer 2 transactions DAI tokens for USD and EUR payments', async function () { const path2 = [EUR_hash, USD_hash, DAI_address]; diff --git a/packages/smart-contracts/test/contracts/BatchConversionEthPayments.test.ts b/packages/smart-contracts/test/contracts/BatchConversionEthPayments.test.ts index 658ed1621..c654f87ad 100644 --- a/packages/smart-contracts/test/contracts/BatchConversionEthPayments.test.ts +++ b/packages/smart-contracts/test/contracts/BatchConversionEthPayments.test.ts @@ -53,6 +53,7 @@ describe('contract: BatchConversionPayments', () => { maxRateTimespan: BigNumberish; }; let convDetail: ConversionDetail; + let inputs: Array; let tx: ContractTransaction; @@ -65,7 +66,6 @@ describe('contract: BatchConversionPayments', () => { // amount and feeAmount are usually in fiat for conversion inputs, else in ETH const amount = BigNumber.from(100000); const feeAmount = amount.mul(10).div(10000); - let inputs: Array; const pathUsdEth = [USD_hash, ETH_hash]; /** From 2a683ee36a9564d6daca5f8d8992aded6839df0d Mon Sep 17 00:00:00 2001 From: olivier7delf <55892112+olivier7delf@users.noreply.github.com> Date: Wed, 31 Aug 2022 11:06:11 +0200 Subject: [PATCH 080/105] deploy instead of connect to batchProxy --- packages/smart-contracts/hardhat.config.ts | 1 + .../BatchConversionErc20Payments.test.ts | 50 +- .../BatchConversionErc20PaymentsOLD.test.ts | 584 ++++++++++++++++++ 3 files changed, 610 insertions(+), 25 deletions(-) create mode 100644 packages/smart-contracts/test/contracts/BatchConversionErc20PaymentsOLD.test.ts diff --git a/packages/smart-contracts/hardhat.config.ts b/packages/smart-contracts/hardhat.config.ts index fc0dcc591..dc8b444cb 100644 --- a/packages/smart-contracts/hardhat.config.ts +++ b/packages/smart-contracts/hardhat.config.ts @@ -59,6 +59,7 @@ export default { private: { url: 'http://127.0.0.1:8545', accounts: undefined, + setTimeout: 60000, }, mainnet: { url: process.env.WEB3_PROVIDER_URL || 'https://mainnet.infura.io/v3/YOUR_API_KEY', diff --git a/packages/smart-contracts/test/contracts/BatchConversionErc20Payments.test.ts b/packages/smart-contracts/test/contracts/BatchConversionErc20Payments.test.ts index 70f87829d..a32815063 100644 --- a/packages/smart-contracts/test/contracts/BatchConversionErc20Payments.test.ts +++ b/packages/smart-contracts/test/contracts/BatchConversionErc20Payments.test.ts @@ -11,12 +11,13 @@ import { Erc20ConversionProxy, EthConversionProxy, TestERC20__factory, + BatchConversionPayments__factory, BatchConversionPayments, } from '../../src/types'; import { BigNumber, BigNumberish, BytesLike, ContractTransaction, Signer } from 'ethers'; import { expect } from 'chai'; import { CurrencyManager } from '@requestnetwork/currency'; -import { chainlinkConversionPath, batchConversionPaymentsArtifact } from '../../src/lib'; +import { chainlinkConversionPath } from '../../src/lib'; import { localERC20AlphaArtifact, secondLocalERC20AlphaArtifact } from './localArtifacts'; import Utils from '@requestnetwork/utils'; @@ -49,13 +50,13 @@ describe('contract: BatchConversionPayments', async () => { let DAI_address: string; let FAU_address: string; - let testErc20ConversionProxy: Erc20ConversionProxy; - let testEthConversionProxy: EthConversionProxy; + let erc20ConversionProxy: Erc20ConversionProxy; + let ethConversionProxy: EthConversionProxy; let testBatchConversionProxy: BatchConversionPayments; let testERC20: TestERC20; let testERC20b: TestERC20; let erc20FeeProxy: ERC20FeeProxy; - let ethereumFeeProxy: EthereumFeeProxy; + let ethFeeProxy: EthereumFeeProxy; let chainlinkPath: ChainlinkConversionPath; // variables used to check testERC20 balances @@ -222,7 +223,7 @@ describe('contract: BatchConversionPayments', async () => { const result = batchConvFunction([convDetail], feeAddress); await expect(result) - .to.emit(testErc20ConversionProxy, 'TransferWithConversionAndReference') + .to.emit(erc20ConversionProxy, 'TransferWithConversionAndReference') .withArgs( convDetail.requestAmount, ethers.utils.getAddress(convDetail.path[0]), @@ -230,7 +231,7 @@ describe('contract: BatchConversionPayments', async () => { convDetail.feeAmount, '0', ) - .to.emit(testErc20ConversionProxy, 'TransferWithReferenceAndFee') + .to.emit(erc20ConversionProxy, 'TransferWithReferenceAndFee') .withArgs( ethers.utils.getAddress(DAI_address), ethers.utils.getAddress(convDetail.recipient), @@ -394,34 +395,39 @@ describe('contract: BatchConversionPayments', async () => { chainlinkPath = chainlinkConversionPath.connect(network.name, signer1); erc20FeeProxy = await new ERC20FeeProxy__factory(adminSigner).deploy(); - ethereumFeeProxy = await new EthereumFeeProxy__factory(adminSigner).deploy(); - testErc20ConversionProxy = await new Erc20ConversionProxy__factory(adminSigner).deploy( + ethFeeProxy = await new EthereumFeeProxy__factory(adminSigner).deploy(); + erc20ConversionProxy = await new Erc20ConversionProxy__factory(adminSigner).deploy( erc20FeeProxy.address, chainlinkPath.address, await adminSigner.getAddress(), ); - testEthConversionProxy = await new EthConversionProxy__factory(adminSigner).deploy( - ethereumFeeProxy.address, + ethConversionProxy = await new EthConversionProxy__factory(adminSigner).deploy( + ethFeeProxy.address, chainlinkPath.address, ETH_hash, ); + + // TODO deploy batch proxy -> then no need to revoke approvals + testBatchConversionProxy = await new BatchConversionPayments__factory(adminSigner).deploy( + erc20FeeProxy.address, + ethFeeProxy.address, + erc20ConversionProxy.address, + ethConversionProxy.address, + await adminSigner.getAddress(), + ); }); /** * @notice it contains all the tests related to the ERC20 batch payment, and its context required * @param useBatchRouter allows to use the function "batchERC20ConversionPaymentsMultiTokens" * through the batchRouter or directly */ - for (const useBatchRouter of [true, false]) { + for (const useBatchRouter of [true]) { before(async () => { - // TODO deploy batch proxy -> then no need to revoke approvals - testBatchConversionProxy = batchConversionPaymentsArtifact.connect(network.name, adminSigner); // update batch payment proxies, and batch fees await testBatchConversionProxy.setPaymentErc20Proxy(erc20FeeProxy.address); - await testBatchConversionProxy.setPaymentEthProxy(ethereumFeeProxy.address); - await testBatchConversionProxy.setPaymentErc20ConversionProxy( - testErc20ConversionProxy.address, - ); - await testBatchConversionProxy.setPaymentEthConversionProxy(testEthConversionProxy.address); + await testBatchConversionProxy.setPaymentEthProxy(ethFeeProxy.address); + await testBatchConversionProxy.setPaymentErc20ConversionProxy(erc20ConversionProxy.address); + await testBatchConversionProxy.setPaymentEthConversionProxy(ethConversionProxy.address); // set ERC20 tokens DAI_address = localERC20AlphaArtifact.getAddress(network.name); testERC20 = new TestERC20__factory(adminSigner).attach(DAI_address); @@ -437,7 +443,7 @@ describe('contract: BatchConversionPayments', async () => { await testERC20b.transfer(from, BigNumber.from(thousandWith18Decimal)); testERC20b = TestERC20__factory.connect(testERC20b.address, signer1); - testBatchConversionProxy = batchConversionPaymentsArtifact.connect(network.name, signer1); + testBatchConversionProxy = testBatchConversionProxy.connect(signer1); setBatchConvFunction(useBatchRouter, signer1); }); @@ -480,12 +486,6 @@ describe('contract: BatchConversionPayments', async () => { ); }); - after(async () => { - // restore previous values for consistency - await testBatchConversionProxy.connect(adminSigner).setBatchFee(30); - await testBatchConversionProxy.connect(adminSigner).setBatchConversionFee(30); - }); - describe(useBatchRouter ? 'Through batchRouter' : 'Without batchRouter', () => { // TODO reunite both describe describe('batchERC20ConversionPaymentsMultiTokens with DAI', async () => { diff --git a/packages/smart-contracts/test/contracts/BatchConversionErc20PaymentsOLD.test.ts b/packages/smart-contracts/test/contracts/BatchConversionErc20PaymentsOLD.test.ts new file mode 100644 index 000000000..70f87829d --- /dev/null +++ b/packages/smart-contracts/test/contracts/BatchConversionErc20PaymentsOLD.test.ts @@ -0,0 +1,584 @@ +import { ethers, network } from 'hardhat'; +import { + ERC20FeeProxy__factory, + Erc20ConversionProxy__factory, + EthConversionProxy__factory, + EthereumFeeProxy__factory, + ERC20FeeProxy, + EthereumFeeProxy, + ChainlinkConversionPath, + TestERC20, + Erc20ConversionProxy, + EthConversionProxy, + TestERC20__factory, + BatchConversionPayments, +} from '../../src/types'; +import { BigNumber, BigNumberish, BytesLike, ContractTransaction, Signer } from 'ethers'; +import { expect } from 'chai'; +import { CurrencyManager } from '@requestnetwork/currency'; +import { chainlinkConversionPath, batchConversionPaymentsArtifact } from '../../src/lib'; +import { localERC20AlphaArtifact, secondLocalERC20AlphaArtifact } from './localArtifacts'; +import Utils from '@requestnetwork/utils'; + +// set to true to log batch payments's gas consumption +const logGas = false; + +describe('contract: BatchConversionPayments', async () => { + let from: string; + let to: string; + let feeAddress: string; + let batchAddress: string; + let signer1: Signer; + let signer4: Signer; + let adminSigner: Signer; + + // constants used to set up batch conversion proxy, and also requests payment + const batchFee = 50; + const batchConvFee = 100; + const amountInFiat = '100000000'; // 1 with 8 decimal + const feesAmountInFiat = '100000'; // 0.001 with 8 decimal + const thousandWith18Decimal = '1000000000000000000000'; + const referenceExample = '0xaaaa'; + + // constants and variables to set up proxies and paths + 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 DAI_address: string; + let FAU_address: string; + + let testErc20ConversionProxy: Erc20ConversionProxy; + let testEthConversionProxy: EthConversionProxy; + let testBatchConversionProxy: BatchConversionPayments; + let testERC20: TestERC20; + let testERC20b: TestERC20; + let erc20FeeProxy: ERC20FeeProxy; + let ethereumFeeProxy: EthereumFeeProxy; + let chainlinkPath: ChainlinkConversionPath; + + // variables used to check testERC20 balances + let fromOldBalance: BigNumber; + let toOldBalance: BigNumber; + let feeOldBalance: BigNumber; + + let fromDiffBalanceExpected: BigNumber; + let toDiffBalanceExpected: BigNumber; + let feeDiffBalanceExpected: BigNumber; + + // variables needed for chainlink and conversion payments + let conversionToPay: BigNumber; + let conversionFees: BigNumber; + + // type required by Erc20 conversion batch function inputs + type ConversionDetail = { + recipient: string; + requestAmount: BigNumberish; + path: string[]; + paymentReference: BytesLike; + feeAmount: BigNumberish; + maxToSpend: BigNumberish; + maxRateTimespan: BigNumberish; + }; + let convDetail: ConversionDetail; + + /** + * @notice Function batch conversion, it can be the batchRouter function, + * used with conversion args, or directly batchERC20ConversionPaymentsMultiTokens + */ + let batchConvFunction: ( + args: any, + feeAddress: string, + optional?: any, + ) => Promise; + + /** + * @notice it gets the conversions including fees to be paid, and it set the convDetail input + */ + const getConvToPayAndConvDetail = async ( + _recipient: string, + _path: string[], + _requestAmount: string, + _feeAmount: string, + _maxRateTimespan: number, + _chainlinkPath: ChainlinkConversionPath, + ) => { + const conversionToPayFull = await _chainlinkPath.getConversion(_requestAmount, _path); + conversionToPay = conversionToPayFull.result; + const conversionFeeFull = await _chainlinkPath.getConversion(_feeAmount, _path); + conversionFees = conversionFeeFull.result; + convDetail = { + recipient: _recipient, + requestAmount: _requestAmount, + path: _path, + paymentReference: referenceExample, + feeAmount: _feeAmount, + maxToSpend: conversionToPay.add(conversionFees).toString(), + maxRateTimespan: _maxRateTimespan, + }; + }; + + /** + * check testERC20 balances of: the payer (from), the recipient (to), the feeAddress, and the batch contract + */ + const checkBalancesForOneToken = async ( + _testERC20: TestERC20, + _fromOldBalance: BigNumber, + _toOldBalance: BigNumber, + _feeOldBalance: BigNumber, + _fromDiffBalanceExpected: BigNumber, + _toDiffBalanceExpected: BigNumber, + _feeDiffBalanceExpected: BigNumber, + ) => { + // Get balances + const fromBalance = await _testERC20.balanceOf(from); + const toBalance = await _testERC20.balanceOf(to); + const feeBalance = await _testERC20.balanceOf(feeAddress); + const batchBalance = await _testERC20.balanceOf(batchAddress); + + // Calculate the difference of the balance : now - before + const fromDiffBalance = BigNumber.from(fromBalance).sub(_fromOldBalance); + const toDiffBalance = BigNumber.from(toBalance).sub(_toOldBalance); + const feeDiffBalance = BigNumber.from(feeBalance).sub(_feeOldBalance); + // Check balance changes + expect(fromDiffBalance).to.equals(_fromDiffBalanceExpected, 'fromDiffBalance'); + expect(toDiffBalance).to.equals(_toDiffBalanceExpected, 'toDiffBalance'); + expect(feeDiffBalance).to.equals(_feeDiffBalanceExpected, 'feeDiffBalance'); + expect(batchBalance).to.equals('0', 'batchBalance'); + }; + + /** + * @notice Used to calculate the expected new ERC20 balance of a single token for batch conversion. + * @dev fees are not exactly calculated with the same formula, depending if it is with conversion or not + */ + const expectedERC20Balances = ( + _conversionToPay_results: BigNumber[], + _conversionFees_results: BigNumber[], + appliedFees: number, + withConversion = true, + ) => { + let _fromDiffBalanceExpected = _conversionToPay_results.reduce( + (prev, x) => prev.sub(x), + BigNumber.from(0), + ); + let _toDiffBalanceExpected = _fromDiffBalanceExpected.mul(-1); + let _feeDiffBalanceExpected = _conversionFees_results.reduce( + (prev, x) => prev.add(x), + BigNumber.from(0), + ); + + _feeDiffBalanceExpected = withConversion + ? _toDiffBalanceExpected + .add(_feeDiffBalanceExpected) + .mul(appliedFees) + .div(10000) + .add(_feeDiffBalanceExpected) + : _toDiffBalanceExpected.mul(appliedFees).div(10000).add(_feeDiffBalanceExpected); + + _fromDiffBalanceExpected = _fromDiffBalanceExpected.sub(_feeDiffBalanceExpected); + return [_fromDiffBalanceExpected, _toDiffBalanceExpected, _feeDiffBalanceExpected]; + }; + + /** + * It sets the right batch conversion function, with the associated arguments format + * @param useBatchRouter allows to use batchERC20ConversionPaymentsMultiTokens with batchRouter + * @param _signer + */ + const setBatchConvFunction = async (useBatchRouter: boolean, _signer: Signer) => { + batchConvFunction = ( + convDetails: ConversionDetail[], + feeAddress: string, + ): Promise => { + return useBatchRouter + ? testBatchConversionProxy.connect(_signer).batchRouter( + [ + { + paymentNetworkId: '0', + conversionDetails: convDetails, + cryptoDetails: { + tokenAddresses: [], + recipients: [], + amounts: [], + paymentReferences: [], + feeAmounts: [], + }, + }, + ], + feeAddress, + ) + : testBatchConversionProxy + .connect(_signer) + .batchERC20ConversionPaymentsMultiTokens(convDetails, feeAddress); + }; + }; + + /** + * @notice update convDetail, do an ERC20 conversion batch payment with a single payment inside and calculate the balances + * @param path to update the convDetail + */ + const onePaymentBatchConv = async (path: string[]) => { + await getConvToPayAndConvDetail(to, path, amountInFiat, feesAmountInFiat, 0, chainlinkPath); + + const result = batchConvFunction([convDetail], feeAddress); + await expect(result) + .to.emit(testErc20ConversionProxy, 'TransferWithConversionAndReference') + .withArgs( + convDetail.requestAmount, + ethers.utils.getAddress(convDetail.path[0]), + ethers.utils.keccak256(referenceExample), + convDetail.feeAmount, + '0', + ) + .to.emit(testErc20ConversionProxy, 'TransferWithReferenceAndFee') + .withArgs( + ethers.utils.getAddress(DAI_address), + ethers.utils.getAddress(convDetail.recipient), + conversionToPay, + ethers.utils.keccak256(referenceExample), + conversionFees, + feeAddress, + ); + if (logGas) { + const tx = await result; + await tx.wait(1); + const receipt = await tx.wait(); + console.log(`gas consumption: `, receipt.gasUsed.toString()); + } + + [fromDiffBalanceExpected, toDiffBalanceExpected, feeDiffBalanceExpected] = + expectedERC20Balances([conversionToPay], [conversionFees], batchConvFee); + }; + + /** + * @notice generate nTimes 2 convDetails, do an ERC20 conv batch payment with theses 2*nTimes payments + * and calculate the balances + * @param path2 to update the second convDetail + */ + const manyPaymentsBatchConv = async (path2: string[], nTimes: number) => { + // define a second payment request + const amountInFiat2 = BigNumber.from(amountInFiat).mul(2).toString(); + const feesAmountInFiat2 = BigNumber.from(feesAmountInFiat).mul(2).toString(); + + const conversionToPayFull2 = await chainlinkPath.getConversion(amountInFiat2, path2); + const conversionFeesFull2 = await chainlinkPath.getConversion(feesAmountInFiat2, path2); + + let convDetail2 = Utils.deepCopy(convDetail); + + convDetail2.path = path2; + convDetail2.requestAmount = amountInFiat2; + convDetail2.feeAmount = feesAmountInFiat2; + convDetail2.maxToSpend = conversionToPayFull2.result.add(conversionFeesFull2.result).toString(); + + // define the new arg convDetails for the function, + // and conversionsToPay & conversionsFees results to calculate the expected balances + let convDetails: ConversionDetail[] = []; + let conversionsToPay_results: BigNumber[] = []; + let conversionsFees_results: BigNumber[] = []; + for (let i = 0; i < nTimes; i++) { + convDetails = convDetails.concat([convDetail, convDetail2]); + conversionsToPay_results = conversionsToPay_results.concat([ + conversionToPay, + conversionToPayFull2.result, + ]); + conversionsFees_results = conversionsFees_results.concat([ + conversionFees, + conversionFeesFull2.result, + ]); + } + + // get balances of the 2nd token, useful when there are 2 different tokens used + const fromOldBalance2 = await testERC20b.balanceOf(from); + const toOldBalance2 = await testERC20b.balanceOf(to); + const feeOldBalance2 = await testERC20b.balanceOf(feeAddress); + + const tx = await batchConvFunction(convDetails, feeAddress); + if (logGas) { + const receipt = await tx.wait(); + console.log(`${2 * nTimes} req, gas consumption: `, receipt.gasUsed.toString()); + } + + // 1st condition: every tokens (end of the paths) are identicals + if ( + convDetail.path[convDetail.path.length - 1] === convDetail2.path[convDetail2.path.length - 1] + ) { + [fromDiffBalanceExpected, toDiffBalanceExpected, feeDiffBalanceExpected] = + expectedERC20Balances(conversionsToPay_results, conversionsFees_results, batchConvFee); + } + // else: there are 2 different tokens used (end of the paths): testERC20 and testERC20b + else { + // calculate the expected balances of the 1st token: testERC20 + [fromDiffBalanceExpected, toDiffBalanceExpected, feeDiffBalanceExpected] = + expectedERC20Balances( + conversionsToPay_results.filter((_, i) => i % 2 === 0), + conversionsFees_results.filter((_, i) => i % 2 === 0), + batchConvFee, + ); + + // calculate the expected balances of the 2nd token: testERC20b + const [fromDiffBalanceExpected2, toDiffBalanceExpected2, feeDiffBalanceExpected2] = + expectedERC20Balances( + conversionsToPay_results.filter((_, i) => i % 2 === 1), + conversionsFees_results.filter((_, i) => i % 2 === 1), + batchConvFee, + ); + + // check the balance of testERC20b token, which is not checked in "afterEach" as testERC20 token. + checkBalancesForOneToken( + testERC20b, + fromOldBalance2, + toOldBalance2, + feeOldBalance2, + fromDiffBalanceExpected2, + toDiffBalanceExpected2, + feeDiffBalanceExpected2, + ); + } + }; + + /** + * @notice Use to test one batch payment execution for a given ERC20 batch function (no conversion). + * It tests the ERC20 transfer and fee proxy `TransferWithReferenceAndFee` events + * @param useBatchRouter allows to use a function through the batchRouter or not + * @param erc20Function selects the batch function name tested: "batchERC20PaymentsWithReference" + * or "batchERC20PaymentsMultiTokensWithReference" + */ + const batchERC20Payments = async (useBatchRouter: boolean, erc20Function: string) => { + // set up main variables + const amount = 200000; + const feeAmount = 3000; + const tokenAddress = testERC20.address; + + // Select the batch function and pay + let batchFunction: Function; + if (useBatchRouter) { + batchFunction = testBatchConversionProxy.batchRouter; + await batchFunction( + [ + { + paymentNetworkId: erc20Function === 'batchERC20PaymentsWithReference' ? 1 : 2, + conversionDetails: [], + cryptoDetails: { + tokenAddresses: [tokenAddress], + recipients: [to], + amounts: [amount], + paymentReferences: [referenceExample], + feeAmounts: [feeAmount], + }, + }, + ], + feeAddress, + ); + } else { + batchFunction = + erc20Function === 'batchERC20PaymentsWithReference' + ? testBatchConversionProxy.batchERC20PaymentsWithReference + : testBatchConversionProxy.batchERC20PaymentsMultiTokensWithReference; + await batchFunction( + erc20Function === 'batchERC20PaymentsWithReference' ? tokenAddress : [tokenAddress], + [to], + [amount], + [referenceExample], + [feeAmount], + feeAddress, + ); + } + + [fromDiffBalanceExpected, toDiffBalanceExpected, feeDiffBalanceExpected] = + expectedERC20Balances([BigNumber.from(amount)], [BigNumber.from(feeAmount)], batchFee, false); + }; + before(async () => { + [from, from, to, feeAddress] = (await ethers.getSigners()).map((s) => s.address); + [adminSigner, signer1, signer4, signer4, signer4] = await ethers.getSigners(); + + chainlinkPath = chainlinkConversionPath.connect(network.name, signer1); + + erc20FeeProxy = await new ERC20FeeProxy__factory(adminSigner).deploy(); + ethereumFeeProxy = await new EthereumFeeProxy__factory(adminSigner).deploy(); + testErc20ConversionProxy = await new Erc20ConversionProxy__factory(adminSigner).deploy( + erc20FeeProxy.address, + chainlinkPath.address, + await adminSigner.getAddress(), + ); + testEthConversionProxy = await new EthConversionProxy__factory(adminSigner).deploy( + ethereumFeeProxy.address, + chainlinkPath.address, + ETH_hash, + ); + }); + /** + * @notice it contains all the tests related to the ERC20 batch payment, and its context required + * @param useBatchRouter allows to use the function "batchERC20ConversionPaymentsMultiTokens" + * through the batchRouter or directly + */ + for (const useBatchRouter of [true, false]) { + before(async () => { + // TODO deploy batch proxy -> then no need to revoke approvals + testBatchConversionProxy = batchConversionPaymentsArtifact.connect(network.name, adminSigner); + // update batch payment proxies, and batch fees + await testBatchConversionProxy.setPaymentErc20Proxy(erc20FeeProxy.address); + await testBatchConversionProxy.setPaymentEthProxy(ethereumFeeProxy.address); + await testBatchConversionProxy.setPaymentErc20ConversionProxy( + testErc20ConversionProxy.address, + ); + await testBatchConversionProxy.setPaymentEthConversionProxy(testEthConversionProxy.address); + // set ERC20 tokens + DAI_address = localERC20AlphaArtifact.getAddress(network.name); + testERC20 = new TestERC20__factory(adminSigner).attach(DAI_address); + + FAU_address = secondLocalERC20AlphaArtifact.getAddress(network.name); + testERC20b = new TestERC20__factory(adminSigner).attach(FAU_address); + batchAddress = testBatchConversionProxy.address; + await testBatchConversionProxy.setBatchFee(batchFee); + await testBatchConversionProxy.setBatchConversionFee(batchConvFee); + + await testERC20.transfer(from, BigNumber.from(thousandWith18Decimal)); + testERC20 = TestERC20__factory.connect(testERC20.address, signer1); + await testERC20b.transfer(from, BigNumber.from(thousandWith18Decimal)); + testERC20b = TestERC20__factory.connect(testERC20b.address, signer1); + + testBatchConversionProxy = batchConversionPaymentsArtifact.connect(network.name, signer1); + setBatchConvFunction(useBatchRouter, signer1); + }); + + beforeEach(async () => { + fromDiffBalanceExpected = BigNumber.from(0); + toDiffBalanceExpected = BigNumber.from(0); + feeDiffBalanceExpected = BigNumber.from(0); + await testERC20.approve(testBatchConversionProxy.address, thousandWith18Decimal, { + from, + }); + await testERC20b.approve(testBatchConversionProxy.address, thousandWith18Decimal, { + from, + }); + // get balances of testERC20 token + fromOldBalance = await testERC20.balanceOf(from); + toOldBalance = await testERC20.balanceOf(to); + feeOldBalance = await testERC20.balanceOf(feeAddress); + + // create a default convDetail + getConvToPayAndConvDetail( + to, + [USD_hash, DAI_address], + amountInFiat, + feesAmountInFiat, + 0, + chainlinkPath, + ); + }); + + afterEach(async () => { + // check balances of testERC20 token + checkBalancesForOneToken( + testERC20, + fromOldBalance, + toOldBalance, + feeOldBalance, + fromDiffBalanceExpected, + toDiffBalanceExpected, + feeDiffBalanceExpected, + ); + }); + + after(async () => { + // restore previous values for consistency + await testBatchConversionProxy.connect(adminSigner).setBatchFee(30); + await testBatchConversionProxy.connect(adminSigner).setBatchConversionFee(30); + }); + + describe(useBatchRouter ? 'Through batchRouter' : 'Without batchRouter', () => { + // TODO reunite both describe + describe('batchERC20ConversionPaymentsMultiTokens with DAI', async () => { + it('allows to transfer DAI tokens for USD payment', async () => { + await onePaymentBatchConv([USD_hash, DAI_address]); + }); + it('allows to transfer DAI tokens for EUR payment', async () => { + await onePaymentBatchConv([EUR_hash, USD_hash, DAI_address]); + }); + it('allows to transfer 2 transactions DAI tokens for USD payment', async function () { + await manyPaymentsBatchConv([USD_hash, DAI_address], 1); + }); + it('allows to transfer DAI tokens for EUR payment', async () => { + await onePaymentBatchConv([EUR_hash, USD_hash, DAI_address]); + }); + it('allows to transfer 2 transactions DAI tokens for USD and EUR payments', async function () { + const path2 = [EUR_hash, USD_hash, DAI_address]; + await manyPaymentsBatchConv(path2, 1); + }); + it('allows to transfer two kinds of tokens for USD', async function () { + const path2 = [USD_hash, FAU_address]; + await manyPaymentsBatchConv(path2, 1); + }); + }); + }); + + describe('batchERC20ConversionPaymentsMultiTokens with errors', () => { + it('cannot transfer with invalid path', async function () { + const wrongPath = [EUR_hash, ETH_hash, DAI_address]; + convDetail.path = wrongPath; + await expect(batchConvFunction([convDetail], feeAddress)).to.be.revertedWith( + 'revert No aggregator found', + ); + }); + + it('cannot transfer if max to spend too low', async function () { + convDetail.maxToSpend = conversionToPay.add(conversionFees).sub(1).toString(); + await expect(batchConvFunction([convDetail], feeAddress)).to.be.revertedWith( + 'Amount to pay is over the user limit', + ); + }); + + it('cannot transfer if rate is too old', async function () { + convDetail.maxRateTimespan = 10; + + await expect(batchConvFunction([convDetail], feeAddress)).to.be.revertedWith( + 'aggregator rate is outdated', + ); + }); + + it('Not enough allowance', async function () { + // signer4 connect to the batch function + setBatchConvFunction(useBatchRouter, signer4); + await expect(batchConvFunction([convDetail], feeAddress)).to.be.revertedWith( + 'Insufficient allowance for batch to pay', + ); + // reset: signer1 connect to the batch function + setBatchConvFunction(useBatchRouter, signer1); + }); + + it('Not enough funds', async function () { + // increase signer4 allowance + await testERC20 + .connect(signer4) + .approve(testBatchConversionProxy.address, thousandWith18Decimal); + // signer4 connect to the batch function + setBatchConvFunction(useBatchRouter, signer4); + + await expect(batchConvFunction([convDetail], feeAddress)).to.be.revertedWith( + 'not enough funds, including fees', + ); + + // reset: decrease signer4 allowance and reconnect with signer1 + await testERC20.connect(signer4).approve(testBatchConversionProxy.address, '0'); + testERC20.connect(signer1); + // reset: signer1 connect to the batch function + setBatchConvFunction(useBatchRouter, signer1); + }); + }); + + /** Make sure the existing ERC20 functions from the parent contract BatchPaymentPublic.sol are still working */ + describe('Test BatchErc20Payments functions', () => { + it(`${ + useBatchRouter ? 'with batchRouter, ' : '' + }batchERC20PaymentsWithReference transfers token`, async function () { + await batchERC20Payments(useBatchRouter, 'batchERC20PaymentsWithReference'); + }); + + it(`${ + useBatchRouter ? 'with batchRouter, ' : '' + }batchERC20PaymentsMultiTokensWithReference transfers token`, async function () { + await batchERC20Payments(useBatchRouter, 'batchERC20PaymentsMultiTokensWithReference'); + }); + }); + } +}); From 65c9f94e1a087bac82b35934d2adb4c2de24127e Mon Sep 17 00:00:00 2001 From: olivier7delf <55892112+olivier7delf@users.noreply.github.com> Date: Wed, 31 Aug 2022 16:17:35 +0200 Subject: [PATCH 081/105] end cleaning old test version --- .../BatchConversionErc20Payments.test.ts | 245 +++++++--------- .../BatchConversionErc20PaymentsOLD.test.ts | 277 ++++++++---------- 2 files changed, 216 insertions(+), 306 deletions(-) diff --git a/packages/smart-contracts/test/contracts/BatchConversionErc20Payments.test.ts b/packages/smart-contracts/test/contracts/BatchConversionErc20Payments.test.ts index a32815063..2f0fcd456 100644 --- a/packages/smart-contracts/test/contracts/BatchConversionErc20Payments.test.ts +++ b/packages/smart-contracts/test/contracts/BatchConversionErc20Payments.test.ts @@ -4,8 +4,6 @@ import { Erc20ConversionProxy__factory, EthConversionProxy__factory, EthereumFeeProxy__factory, - ERC20FeeProxy, - EthereumFeeProxy, ChainlinkConversionPath, TestERC20, Erc20ConversionProxy, @@ -21,17 +19,12 @@ import { chainlinkConversionPath } from '../../src/lib'; import { localERC20AlphaArtifact, secondLocalERC20AlphaArtifact } from './localArtifacts'; import Utils from '@requestnetwork/utils'; -// set to true to log batch payments's gas consumption -const logGas = false; - describe('contract: BatchConversionPayments', async () => { let from: string; let to: string; let feeAddress: string; - let batchAddress: string; let signer1: Signer; let signer4: Signer; - let adminSigner: Signer; // constants used to set up batch conversion proxy, and also requests payment const batchFee = 50; @@ -55,8 +48,6 @@ describe('contract: BatchConversionPayments', async () => { let testBatchConversionProxy: BatchConversionPayments; let testERC20: TestERC20; let testERC20b: TestERC20; - let erc20FeeProxy: ERC20FeeProxy; - let ethFeeProxy: EthereumFeeProxy; let chainlinkPath: ChainlinkConversionPath; // variables used to check testERC20 balances @@ -105,10 +96,8 @@ describe('contract: BatchConversionPayments', async () => { _maxRateTimespan: number, _chainlinkPath: ChainlinkConversionPath, ) => { - const conversionToPayFull = await _chainlinkPath.getConversion(_requestAmount, _path); - conversionToPay = conversionToPayFull.result; - const conversionFeeFull = await _chainlinkPath.getConversion(_feeAmount, _path); - conversionFees = conversionFeeFull.result; + conversionToPay = (await _chainlinkPath.getConversion(_requestAmount, _path)).result; + conversionFees = (await _chainlinkPath.getConversion(_feeAmount, _path)).result; convDetail = { recipient: _recipient, requestAmount: _requestAmount, @@ -136,7 +125,7 @@ describe('contract: BatchConversionPayments', async () => { const fromBalance = await _testERC20.balanceOf(from); const toBalance = await _testERC20.balanceOf(to); const feeBalance = await _testERC20.balanceOf(feeAddress); - const batchBalance = await _testERC20.balanceOf(batchAddress); + const batchBalance = await _testERC20.balanceOf(testBatchConversionProxy.address); // Calculate the difference of the balance : now - before const fromDiffBalance = BigNumber.from(fromBalance).sub(_fromOldBalance); @@ -240,12 +229,6 @@ describe('contract: BatchConversionPayments', async () => { conversionFees, feeAddress, ); - if (logGas) { - const tx = await result; - await tx.wait(1); - const receipt = await tx.wait(); - console.log(`gas consumption: `, receipt.gasUsed.toString()); - } [fromDiffBalanceExpected, toDiffBalanceExpected, feeDiffBalanceExpected] = expectedERC20Balances([conversionToPay], [conversionFees], batchConvFee); @@ -261,31 +244,25 @@ describe('contract: BatchConversionPayments', async () => { const amountInFiat2 = BigNumber.from(amountInFiat).mul(2).toString(); const feesAmountInFiat2 = BigNumber.from(feesAmountInFiat).mul(2).toString(); - const conversionToPayFull2 = await chainlinkPath.getConversion(amountInFiat2, path2); - const conversionFeesFull2 = await chainlinkPath.getConversion(feesAmountInFiat2, path2); + const conversionToPay2 = (await chainlinkPath.getConversion(amountInFiat2, path2)).result; + const conversionFees2 = (await chainlinkPath.getConversion(feesAmountInFiat2, path2)).result; let convDetail2 = Utils.deepCopy(convDetail); convDetail2.path = path2; convDetail2.requestAmount = amountInFiat2; convDetail2.feeAmount = feesAmountInFiat2; - convDetail2.maxToSpend = conversionToPayFull2.result.add(conversionFeesFull2.result).toString(); + convDetail2.maxToSpend = conversionToPay2.add(conversionFees2).toString(); // define the new arg convDetails for the function, - // and conversionsToPay & conversionsFees results to calculate the expected balances + // and conversionsToPays & conversionsFees to calculate the expected balances let convDetails: ConversionDetail[] = []; - let conversionsToPay_results: BigNumber[] = []; - let conversionsFees_results: BigNumber[] = []; + let conversionsToPays: BigNumber[] = []; + let conversionsFees: BigNumber[] = []; for (let i = 0; i < nTimes; i++) { convDetails = convDetails.concat([convDetail, convDetail2]); - conversionsToPay_results = conversionsToPay_results.concat([ - conversionToPay, - conversionToPayFull2.result, - ]); - conversionsFees_results = conversionsFees_results.concat([ - conversionFees, - conversionFeesFull2.result, - ]); + conversionsToPays = conversionsToPays.concat([conversionToPay, conversionToPay2]); + conversionsFees = conversionsFees.concat([conversionFees, conversionFees2]); } // get balances of the 2nd token, useful when there are 2 different tokens used @@ -293,36 +270,28 @@ describe('contract: BatchConversionPayments', async () => { const toOldBalance2 = await testERC20b.balanceOf(to); const feeOldBalance2 = await testERC20b.balanceOf(feeAddress); - const tx = await batchConvFunction(convDetails, feeAddress); - if (logGas) { - const receipt = await tx.wait(); - console.log(`${2 * nTimes} req, gas consumption: `, receipt.gasUsed.toString()); - } + await batchConvFunction(convDetails, feeAddress); // 1st condition: every tokens (end of the paths) are identicals if ( convDetail.path[convDetail.path.length - 1] === convDetail2.path[convDetail2.path.length - 1] ) { [fromDiffBalanceExpected, toDiffBalanceExpected, feeDiffBalanceExpected] = - expectedERC20Balances(conversionsToPay_results, conversionsFees_results, batchConvFee); + expectedERC20Balances(conversionsToPays, conversionsFees, batchConvFee); } // else: there are 2 different tokens used (end of the paths): testERC20 and testERC20b else { // calculate the expected balances of the 1st token: testERC20 + const conversionsToPayToken1 = conversionsToPays.filter((_, i) => i % 2 === 0); + const conversionsFeesToken1 = conversionsFees.filter((_, i) => i % 2 === 0); [fromDiffBalanceExpected, toDiffBalanceExpected, feeDiffBalanceExpected] = - expectedERC20Balances( - conversionsToPay_results.filter((_, i) => i % 2 === 0), - conversionsFees_results.filter((_, i) => i % 2 === 0), - batchConvFee, - ); + expectedERC20Balances(conversionsToPayToken1, conversionsFeesToken1, batchConvFee); // calculate the expected balances of the 2nd token: testERC20b + const conversionsToPayToken2 = conversionsToPays.filter((_, i) => i % 2 === 1); + const conversionsFeesToken2 = conversionsFees.filter((_, i) => i % 2 === 1); const [fromDiffBalanceExpected2, toDiffBalanceExpected2, feeDiffBalanceExpected2] = - expectedERC20Balances( - conversionsToPay_results.filter((_, i) => i % 2 === 1), - conversionsFees_results.filter((_, i) => i % 2 === 1), - batchConvFee, - ); + expectedERC20Balances(conversionsToPayToken2, conversionsFeesToken2, batchConvFee); // check the balance of testERC20b token, which is not checked in "afterEach" as testERC20 token. checkBalancesForOneToken( @@ -389,13 +358,14 @@ describe('contract: BatchConversionPayments', async () => { expectedERC20Balances([BigNumber.from(amount)], [BigNumber.from(feeAmount)], batchFee, false); }; before(async () => { - [from, from, to, feeAddress] = (await ethers.getSigners()).map((s) => s.address); - [adminSigner, signer1, signer4, signer4, signer4] = await ethers.getSigners(); + [, from, to, feeAddress] = (await ethers.getSigners()).map((s) => s.address); + let adminSigner: Signer; + [adminSigner, signer1, , , signer4] = await ethers.getSigners(); chainlinkPath = chainlinkConversionPath.connect(network.name, signer1); - erc20FeeProxy = await new ERC20FeeProxy__factory(adminSigner).deploy(); - ethFeeProxy = await new EthereumFeeProxy__factory(adminSigner).deploy(); + const erc20FeeProxy = await new ERC20FeeProxy__factory(adminSigner).deploy(); + const ethFeeProxy = await new EthereumFeeProxy__factory(adminSigner).deploy(); erc20ConversionProxy = await new Erc20ConversionProxy__factory(adminSigner).deploy( erc20FeeProxy.address, chainlinkPath.address, @@ -407,7 +377,6 @@ describe('contract: BatchConversionPayments', async () => { ETH_hash, ); - // TODO deploy batch proxy -> then no need to revoke approvals testBatchConversionProxy = await new BatchConversionPayments__factory(adminSigner).deploy( erc20FeeProxy.address, ethFeeProxy.address, @@ -415,39 +384,33 @@ describe('contract: BatchConversionPayments', async () => { ethConversionProxy.address, await adminSigner.getAddress(), ); + + // set batch proxy fees + await testBatchConversionProxy.setBatchFee(batchFee); + await testBatchConversionProxy.setBatchConversionFee(batchConvFee); + testBatchConversionProxy = testBatchConversionProxy.connect(signer1); + + // set ERC20 tokens + DAI_address = localERC20AlphaArtifact.getAddress(network.name); + testERC20 = new TestERC20__factory(adminSigner).attach(DAI_address); + await testERC20.transfer(from, BigNumber.from(thousandWith18Decimal)); + testERC20 = TestERC20__factory.connect(testERC20.address, signer1); + + FAU_address = secondLocalERC20AlphaArtifact.getAddress(network.name); + testERC20b = new TestERC20__factory(adminSigner).attach(FAU_address); + await testERC20b.transfer(from, BigNumber.from(thousandWith18Decimal)); + testERC20b = TestERC20__factory.connect(testERC20b.address, signer1); }); + /** * @notice it contains all the tests related to the ERC20 batch payment, and its context required * @param useBatchRouter allows to use the function "batchERC20ConversionPaymentsMultiTokens" * through the batchRouter or directly */ - for (const useBatchRouter of [true]) { - before(async () => { - // update batch payment proxies, and batch fees - await testBatchConversionProxy.setPaymentErc20Proxy(erc20FeeProxy.address); - await testBatchConversionProxy.setPaymentEthProxy(ethFeeProxy.address); - await testBatchConversionProxy.setPaymentErc20ConversionProxy(erc20ConversionProxy.address); - await testBatchConversionProxy.setPaymentEthConversionProxy(ethConversionProxy.address); - // set ERC20 tokens - DAI_address = localERC20AlphaArtifact.getAddress(network.name); - testERC20 = new TestERC20__factory(adminSigner).attach(DAI_address); - - FAU_address = secondLocalERC20AlphaArtifact.getAddress(network.name); - testERC20b = new TestERC20__factory(adminSigner).attach(FAU_address); - batchAddress = testBatchConversionProxy.address; - await testBatchConversionProxy.setBatchFee(batchFee); - await testBatchConversionProxy.setBatchConversionFee(batchConvFee); - - await testERC20.transfer(from, BigNumber.from(thousandWith18Decimal)); - testERC20 = TestERC20__factory.connect(testERC20.address, signer1); - await testERC20b.transfer(from, BigNumber.from(thousandWith18Decimal)); - testERC20b = TestERC20__factory.connect(testERC20b.address, signer1); - - testBatchConversionProxy = testBatchConversionProxy.connect(signer1); + for (const useBatchRouter of [true, false]) { + beforeEach(async () => { setBatchConvFunction(useBatchRouter, signer1); - }); - beforeEach(async () => { fromDiffBalanceExpected = BigNumber.from(0); toDiffBalanceExpected = BigNumber.from(0); feeDiffBalanceExpected = BigNumber.from(0); @@ -486,8 +449,7 @@ describe('contract: BatchConversionPayments', async () => { ); }); - describe(useBatchRouter ? 'Through batchRouter' : 'Without batchRouter', () => { - // TODO reunite both describe + describe(useBatchRouter ? 'Through batchRouter' : 'Without batchRouter ', async () => { describe('batchERC20ConversionPaymentsMultiTokens with DAI', async () => { it('allows to transfer DAI tokens for USD payment', async () => { await onePaymentBatchConv([USD_hash, DAI_address]); @@ -502,82 +464,75 @@ describe('contract: BatchConversionPayments', async () => { await onePaymentBatchConv([EUR_hash, USD_hash, DAI_address]); }); it('allows to transfer 2 transactions DAI tokens for USD and EUR payments', async function () { - const path2 = [EUR_hash, USD_hash, DAI_address]; - await manyPaymentsBatchConv(path2, 1); + await manyPaymentsBatchConv([EUR_hash, USD_hash, DAI_address], 1); }); it('allows to transfer two kinds of tokens for USD', async function () { - const path2 = [USD_hash, FAU_address]; - await manyPaymentsBatchConv(path2, 1); + await manyPaymentsBatchConv([USD_hash, FAU_address], 1); }); }); - }); - describe('batchERC20ConversionPaymentsMultiTokens with errors', () => { - it('cannot transfer with invalid path', async function () { - const wrongPath = [EUR_hash, ETH_hash, DAI_address]; - convDetail.path = wrongPath; - await expect(batchConvFunction([convDetail], feeAddress)).to.be.revertedWith( - 'revert No aggregator found', - ); - }); + describe('batchERC20ConversionPaymentsMultiTokens with errors', () => { + it('cannot transfer with invalid path', async function () { + convDetail.path = [EUR_hash, ETH_hash, DAI_address]; + await expect(batchConvFunction([convDetail], feeAddress)).to.be.revertedWith( + 'revert No aggregator found', + ); + }); - it('cannot transfer if max to spend too low', async function () { - convDetail.maxToSpend = conversionToPay.add(conversionFees).sub(1).toString(); - await expect(batchConvFunction([convDetail], feeAddress)).to.be.revertedWith( - 'Amount to pay is over the user limit', - ); - }); + it('cannot transfer if max to spend too low', async function () { + convDetail.maxToSpend = conversionToPay.add(conversionFees).sub(1).toString(); + await expect(batchConvFunction([convDetail], feeAddress)).to.be.revertedWith( + 'Amount to pay is over the user limit', + ); + }); - it('cannot transfer if rate is too old', async function () { - convDetail.maxRateTimespan = 10; + it('cannot transfer if rate is too old', async function () { + convDetail.maxRateTimespan = 10; - await expect(batchConvFunction([convDetail], feeAddress)).to.be.revertedWith( - 'aggregator rate is outdated', - ); - }); + await expect(batchConvFunction([convDetail], feeAddress)).to.be.revertedWith( + 'aggregator rate is outdated', + ); + }); - it('Not enough allowance', async function () { - // signer4 connect to the batch function - setBatchConvFunction(useBatchRouter, signer4); - await expect(batchConvFunction([convDetail], feeAddress)).to.be.revertedWith( - 'Insufficient allowance for batch to pay', - ); - // reset: signer1 connect to the batch function - setBatchConvFunction(useBatchRouter, signer1); - }); + it('Not enough allowance', async function () { + // signer4 connect to the batch function + setBatchConvFunction(useBatchRouter, signer4); + await expect(batchConvFunction([convDetail], feeAddress)).to.be.revertedWith( + 'Insufficient allowance for batch to pay', + ); + // reset: signer1 connect to the batch function + setBatchConvFunction(useBatchRouter, signer1); + }); - it('Not enough funds', async function () { - // increase signer4 allowance - await testERC20 - .connect(signer4) - .approve(testBatchConversionProxy.address, thousandWith18Decimal); - // signer4 connect to the batch function - setBatchConvFunction(useBatchRouter, signer4); - - await expect(batchConvFunction([convDetail], feeAddress)).to.be.revertedWith( - 'not enough funds, including fees', - ); - - // reset: decrease signer4 allowance and reconnect with signer1 - await testERC20.connect(signer4).approve(testBatchConversionProxy.address, '0'); - testERC20.connect(signer1); - // reset: signer1 connect to the batch function - setBatchConvFunction(useBatchRouter, signer1); + it('Not enough funds', async function () { + // increase signer4 allowance + await testERC20 + .connect(signer4) + .approve(testBatchConversionProxy.address, thousandWith18Decimal); + // signer4 connect to the batch function + setBatchConvFunction(useBatchRouter, signer4); + + await expect(batchConvFunction([convDetail], feeAddress)).to.be.revertedWith( + 'not enough funds, including fees', + ); + + // reset: decrease signer4 allowance and reconnect with signer1 + await testERC20.connect(signer4).approve(testBatchConversionProxy.address, '0'); + testERC20.connect(signer1); + // reset: signer1 connect to the batch function + setBatchConvFunction(useBatchRouter, signer1); + }); }); - }); - /** Make sure the existing ERC20 functions from the parent contract BatchPaymentPublic.sol are still working */ - describe('Test BatchErc20Payments functions', () => { - it(`${ - useBatchRouter ? 'with batchRouter, ' : '' - }batchERC20PaymentsWithReference transfers token`, async function () { - await batchERC20Payments(useBatchRouter, 'batchERC20PaymentsWithReference'); - }); + /** Make sure the existing ERC20 functions from the parent contract BatchPaymentPublic.sol are still working */ + describe('Herited from contract BatchErc20Payments functions', () => { + it(`batchERC20PaymentsWithReference transfers token`, async function () { + await batchERC20Payments(useBatchRouter, 'batchERC20PaymentsWithReference'); + }); - it(`${ - useBatchRouter ? 'with batchRouter, ' : '' - }batchERC20PaymentsMultiTokensWithReference transfers token`, async function () { - await batchERC20Payments(useBatchRouter, 'batchERC20PaymentsMultiTokensWithReference'); + it(`batchERC20PaymentsMultiTokensWithReference transfers token`, async function () { + await batchERC20Payments(useBatchRouter, 'batchERC20PaymentsMultiTokensWithReference'); + }); }); }); } diff --git a/packages/smart-contracts/test/contracts/BatchConversionErc20PaymentsOLD.test.ts b/packages/smart-contracts/test/contracts/BatchConversionErc20PaymentsOLD.test.ts index 70f87829d..2f0fcd456 100644 --- a/packages/smart-contracts/test/contracts/BatchConversionErc20PaymentsOLD.test.ts +++ b/packages/smart-contracts/test/contracts/BatchConversionErc20PaymentsOLD.test.ts @@ -4,33 +4,27 @@ import { Erc20ConversionProxy__factory, EthConversionProxy__factory, EthereumFeeProxy__factory, - ERC20FeeProxy, - EthereumFeeProxy, ChainlinkConversionPath, TestERC20, Erc20ConversionProxy, EthConversionProxy, TestERC20__factory, + BatchConversionPayments__factory, BatchConversionPayments, } from '../../src/types'; import { BigNumber, BigNumberish, BytesLike, ContractTransaction, Signer } from 'ethers'; import { expect } from 'chai'; import { CurrencyManager } from '@requestnetwork/currency'; -import { chainlinkConversionPath, batchConversionPaymentsArtifact } from '../../src/lib'; +import { chainlinkConversionPath } from '../../src/lib'; import { localERC20AlphaArtifact, secondLocalERC20AlphaArtifact } from './localArtifacts'; import Utils from '@requestnetwork/utils'; -// set to true to log batch payments's gas consumption -const logGas = false; - describe('contract: BatchConversionPayments', async () => { let from: string; let to: string; let feeAddress: string; - let batchAddress: string; let signer1: Signer; let signer4: Signer; - let adminSigner: Signer; // constants used to set up batch conversion proxy, and also requests payment const batchFee = 50; @@ -49,13 +43,11 @@ describe('contract: BatchConversionPayments', async () => { let DAI_address: string; let FAU_address: string; - let testErc20ConversionProxy: Erc20ConversionProxy; - let testEthConversionProxy: EthConversionProxy; + let erc20ConversionProxy: Erc20ConversionProxy; + let ethConversionProxy: EthConversionProxy; let testBatchConversionProxy: BatchConversionPayments; let testERC20: TestERC20; let testERC20b: TestERC20; - let erc20FeeProxy: ERC20FeeProxy; - let ethereumFeeProxy: EthereumFeeProxy; let chainlinkPath: ChainlinkConversionPath; // variables used to check testERC20 balances @@ -104,10 +96,8 @@ describe('contract: BatchConversionPayments', async () => { _maxRateTimespan: number, _chainlinkPath: ChainlinkConversionPath, ) => { - const conversionToPayFull = await _chainlinkPath.getConversion(_requestAmount, _path); - conversionToPay = conversionToPayFull.result; - const conversionFeeFull = await _chainlinkPath.getConversion(_feeAmount, _path); - conversionFees = conversionFeeFull.result; + conversionToPay = (await _chainlinkPath.getConversion(_requestAmount, _path)).result; + conversionFees = (await _chainlinkPath.getConversion(_feeAmount, _path)).result; convDetail = { recipient: _recipient, requestAmount: _requestAmount, @@ -135,7 +125,7 @@ describe('contract: BatchConversionPayments', async () => { const fromBalance = await _testERC20.balanceOf(from); const toBalance = await _testERC20.balanceOf(to); const feeBalance = await _testERC20.balanceOf(feeAddress); - const batchBalance = await _testERC20.balanceOf(batchAddress); + const batchBalance = await _testERC20.balanceOf(testBatchConversionProxy.address); // Calculate the difference of the balance : now - before const fromDiffBalance = BigNumber.from(fromBalance).sub(_fromOldBalance); @@ -222,7 +212,7 @@ describe('contract: BatchConversionPayments', async () => { const result = batchConvFunction([convDetail], feeAddress); await expect(result) - .to.emit(testErc20ConversionProxy, 'TransferWithConversionAndReference') + .to.emit(erc20ConversionProxy, 'TransferWithConversionAndReference') .withArgs( convDetail.requestAmount, ethers.utils.getAddress(convDetail.path[0]), @@ -230,7 +220,7 @@ describe('contract: BatchConversionPayments', async () => { convDetail.feeAmount, '0', ) - .to.emit(testErc20ConversionProxy, 'TransferWithReferenceAndFee') + .to.emit(erc20ConversionProxy, 'TransferWithReferenceAndFee') .withArgs( ethers.utils.getAddress(DAI_address), ethers.utils.getAddress(convDetail.recipient), @@ -239,12 +229,6 @@ describe('contract: BatchConversionPayments', async () => { conversionFees, feeAddress, ); - if (logGas) { - const tx = await result; - await tx.wait(1); - const receipt = await tx.wait(); - console.log(`gas consumption: `, receipt.gasUsed.toString()); - } [fromDiffBalanceExpected, toDiffBalanceExpected, feeDiffBalanceExpected] = expectedERC20Balances([conversionToPay], [conversionFees], batchConvFee); @@ -260,31 +244,25 @@ describe('contract: BatchConversionPayments', async () => { const amountInFiat2 = BigNumber.from(amountInFiat).mul(2).toString(); const feesAmountInFiat2 = BigNumber.from(feesAmountInFiat).mul(2).toString(); - const conversionToPayFull2 = await chainlinkPath.getConversion(amountInFiat2, path2); - const conversionFeesFull2 = await chainlinkPath.getConversion(feesAmountInFiat2, path2); + const conversionToPay2 = (await chainlinkPath.getConversion(amountInFiat2, path2)).result; + const conversionFees2 = (await chainlinkPath.getConversion(feesAmountInFiat2, path2)).result; let convDetail2 = Utils.deepCopy(convDetail); convDetail2.path = path2; convDetail2.requestAmount = amountInFiat2; convDetail2.feeAmount = feesAmountInFiat2; - convDetail2.maxToSpend = conversionToPayFull2.result.add(conversionFeesFull2.result).toString(); + convDetail2.maxToSpend = conversionToPay2.add(conversionFees2).toString(); // define the new arg convDetails for the function, - // and conversionsToPay & conversionsFees results to calculate the expected balances + // and conversionsToPays & conversionsFees to calculate the expected balances let convDetails: ConversionDetail[] = []; - let conversionsToPay_results: BigNumber[] = []; - let conversionsFees_results: BigNumber[] = []; + let conversionsToPays: BigNumber[] = []; + let conversionsFees: BigNumber[] = []; for (let i = 0; i < nTimes; i++) { convDetails = convDetails.concat([convDetail, convDetail2]); - conversionsToPay_results = conversionsToPay_results.concat([ - conversionToPay, - conversionToPayFull2.result, - ]); - conversionsFees_results = conversionsFees_results.concat([ - conversionFees, - conversionFeesFull2.result, - ]); + conversionsToPays = conversionsToPays.concat([conversionToPay, conversionToPay2]); + conversionsFees = conversionsFees.concat([conversionFees, conversionFees2]); } // get balances of the 2nd token, useful when there are 2 different tokens used @@ -292,36 +270,28 @@ describe('contract: BatchConversionPayments', async () => { const toOldBalance2 = await testERC20b.balanceOf(to); const feeOldBalance2 = await testERC20b.balanceOf(feeAddress); - const tx = await batchConvFunction(convDetails, feeAddress); - if (logGas) { - const receipt = await tx.wait(); - console.log(`${2 * nTimes} req, gas consumption: `, receipt.gasUsed.toString()); - } + await batchConvFunction(convDetails, feeAddress); // 1st condition: every tokens (end of the paths) are identicals if ( convDetail.path[convDetail.path.length - 1] === convDetail2.path[convDetail2.path.length - 1] ) { [fromDiffBalanceExpected, toDiffBalanceExpected, feeDiffBalanceExpected] = - expectedERC20Balances(conversionsToPay_results, conversionsFees_results, batchConvFee); + expectedERC20Balances(conversionsToPays, conversionsFees, batchConvFee); } // else: there are 2 different tokens used (end of the paths): testERC20 and testERC20b else { // calculate the expected balances of the 1st token: testERC20 + const conversionsToPayToken1 = conversionsToPays.filter((_, i) => i % 2 === 0); + const conversionsFeesToken1 = conversionsFees.filter((_, i) => i % 2 === 0); [fromDiffBalanceExpected, toDiffBalanceExpected, feeDiffBalanceExpected] = - expectedERC20Balances( - conversionsToPay_results.filter((_, i) => i % 2 === 0), - conversionsFees_results.filter((_, i) => i % 2 === 0), - batchConvFee, - ); + expectedERC20Balances(conversionsToPayToken1, conversionsFeesToken1, batchConvFee); // calculate the expected balances of the 2nd token: testERC20b + const conversionsToPayToken2 = conversionsToPays.filter((_, i) => i % 2 === 1); + const conversionsFeesToken2 = conversionsFees.filter((_, i) => i % 2 === 1); const [fromDiffBalanceExpected2, toDiffBalanceExpected2, feeDiffBalanceExpected2] = - expectedERC20Balances( - conversionsToPay_results.filter((_, i) => i % 2 === 1), - conversionsFees_results.filter((_, i) => i % 2 === 1), - batchConvFee, - ); + expectedERC20Balances(conversionsToPayToken2, conversionsFeesToken2, batchConvFee); // check the balance of testERC20b token, which is not checked in "afterEach" as testERC20 token. checkBalancesForOneToken( @@ -388,60 +358,59 @@ describe('contract: BatchConversionPayments', async () => { expectedERC20Balances([BigNumber.from(amount)], [BigNumber.from(feeAmount)], batchFee, false); }; before(async () => { - [from, from, to, feeAddress] = (await ethers.getSigners()).map((s) => s.address); - [adminSigner, signer1, signer4, signer4, signer4] = await ethers.getSigners(); + [, from, to, feeAddress] = (await ethers.getSigners()).map((s) => s.address); + let adminSigner: Signer; + [adminSigner, signer1, , , signer4] = await ethers.getSigners(); chainlinkPath = chainlinkConversionPath.connect(network.name, signer1); - erc20FeeProxy = await new ERC20FeeProxy__factory(adminSigner).deploy(); - ethereumFeeProxy = await new EthereumFeeProxy__factory(adminSigner).deploy(); - testErc20ConversionProxy = await new Erc20ConversionProxy__factory(adminSigner).deploy( + const erc20FeeProxy = await new ERC20FeeProxy__factory(adminSigner).deploy(); + const ethFeeProxy = await new EthereumFeeProxy__factory(adminSigner).deploy(); + erc20ConversionProxy = await new Erc20ConversionProxy__factory(adminSigner).deploy( erc20FeeProxy.address, chainlinkPath.address, await adminSigner.getAddress(), ); - testEthConversionProxy = await new EthConversionProxy__factory(adminSigner).deploy( - ethereumFeeProxy.address, + ethConversionProxy = await new EthConversionProxy__factory(adminSigner).deploy( + ethFeeProxy.address, chainlinkPath.address, ETH_hash, ); + + testBatchConversionProxy = await new BatchConversionPayments__factory(adminSigner).deploy( + erc20FeeProxy.address, + ethFeeProxy.address, + erc20ConversionProxy.address, + ethConversionProxy.address, + await adminSigner.getAddress(), + ); + + // set batch proxy fees + await testBatchConversionProxy.setBatchFee(batchFee); + await testBatchConversionProxy.setBatchConversionFee(batchConvFee); + testBatchConversionProxy = testBatchConversionProxy.connect(signer1); + + // set ERC20 tokens + DAI_address = localERC20AlphaArtifact.getAddress(network.name); + testERC20 = new TestERC20__factory(adminSigner).attach(DAI_address); + await testERC20.transfer(from, BigNumber.from(thousandWith18Decimal)); + testERC20 = TestERC20__factory.connect(testERC20.address, signer1); + + FAU_address = secondLocalERC20AlphaArtifact.getAddress(network.name); + testERC20b = new TestERC20__factory(adminSigner).attach(FAU_address); + await testERC20b.transfer(from, BigNumber.from(thousandWith18Decimal)); + testERC20b = TestERC20__factory.connect(testERC20b.address, signer1); }); + /** * @notice it contains all the tests related to the ERC20 batch payment, and its context required * @param useBatchRouter allows to use the function "batchERC20ConversionPaymentsMultiTokens" * through the batchRouter or directly */ for (const useBatchRouter of [true, false]) { - before(async () => { - // TODO deploy batch proxy -> then no need to revoke approvals - testBatchConversionProxy = batchConversionPaymentsArtifact.connect(network.name, adminSigner); - // update batch payment proxies, and batch fees - await testBatchConversionProxy.setPaymentErc20Proxy(erc20FeeProxy.address); - await testBatchConversionProxy.setPaymentEthProxy(ethereumFeeProxy.address); - await testBatchConversionProxy.setPaymentErc20ConversionProxy( - testErc20ConversionProxy.address, - ); - await testBatchConversionProxy.setPaymentEthConversionProxy(testEthConversionProxy.address); - // set ERC20 tokens - DAI_address = localERC20AlphaArtifact.getAddress(network.name); - testERC20 = new TestERC20__factory(adminSigner).attach(DAI_address); - - FAU_address = secondLocalERC20AlphaArtifact.getAddress(network.name); - testERC20b = new TestERC20__factory(adminSigner).attach(FAU_address); - batchAddress = testBatchConversionProxy.address; - await testBatchConversionProxy.setBatchFee(batchFee); - await testBatchConversionProxy.setBatchConversionFee(batchConvFee); - - await testERC20.transfer(from, BigNumber.from(thousandWith18Decimal)); - testERC20 = TestERC20__factory.connect(testERC20.address, signer1); - await testERC20b.transfer(from, BigNumber.from(thousandWith18Decimal)); - testERC20b = TestERC20__factory.connect(testERC20b.address, signer1); - - testBatchConversionProxy = batchConversionPaymentsArtifact.connect(network.name, signer1); + beforeEach(async () => { setBatchConvFunction(useBatchRouter, signer1); - }); - beforeEach(async () => { fromDiffBalanceExpected = BigNumber.from(0); toDiffBalanceExpected = BigNumber.from(0); feeDiffBalanceExpected = BigNumber.from(0); @@ -480,14 +449,7 @@ describe('contract: BatchConversionPayments', async () => { ); }); - after(async () => { - // restore previous values for consistency - await testBatchConversionProxy.connect(adminSigner).setBatchFee(30); - await testBatchConversionProxy.connect(adminSigner).setBatchConversionFee(30); - }); - - describe(useBatchRouter ? 'Through batchRouter' : 'Without batchRouter', () => { - // TODO reunite both describe + describe(useBatchRouter ? 'Through batchRouter' : 'Without batchRouter ', async () => { describe('batchERC20ConversionPaymentsMultiTokens with DAI', async () => { it('allows to transfer DAI tokens for USD payment', async () => { await onePaymentBatchConv([USD_hash, DAI_address]); @@ -502,82 +464,75 @@ describe('contract: BatchConversionPayments', async () => { await onePaymentBatchConv([EUR_hash, USD_hash, DAI_address]); }); it('allows to transfer 2 transactions DAI tokens for USD and EUR payments', async function () { - const path2 = [EUR_hash, USD_hash, DAI_address]; - await manyPaymentsBatchConv(path2, 1); + await manyPaymentsBatchConv([EUR_hash, USD_hash, DAI_address], 1); }); it('allows to transfer two kinds of tokens for USD', async function () { - const path2 = [USD_hash, FAU_address]; - await manyPaymentsBatchConv(path2, 1); + await manyPaymentsBatchConv([USD_hash, FAU_address], 1); }); }); - }); - describe('batchERC20ConversionPaymentsMultiTokens with errors', () => { - it('cannot transfer with invalid path', async function () { - const wrongPath = [EUR_hash, ETH_hash, DAI_address]; - convDetail.path = wrongPath; - await expect(batchConvFunction([convDetail], feeAddress)).to.be.revertedWith( - 'revert No aggregator found', - ); - }); + describe('batchERC20ConversionPaymentsMultiTokens with errors', () => { + it('cannot transfer with invalid path', async function () { + convDetail.path = [EUR_hash, ETH_hash, DAI_address]; + await expect(batchConvFunction([convDetail], feeAddress)).to.be.revertedWith( + 'revert No aggregator found', + ); + }); - it('cannot transfer if max to spend too low', async function () { - convDetail.maxToSpend = conversionToPay.add(conversionFees).sub(1).toString(); - await expect(batchConvFunction([convDetail], feeAddress)).to.be.revertedWith( - 'Amount to pay is over the user limit', - ); - }); + it('cannot transfer if max to spend too low', async function () { + convDetail.maxToSpend = conversionToPay.add(conversionFees).sub(1).toString(); + await expect(batchConvFunction([convDetail], feeAddress)).to.be.revertedWith( + 'Amount to pay is over the user limit', + ); + }); - it('cannot transfer if rate is too old', async function () { - convDetail.maxRateTimespan = 10; + it('cannot transfer if rate is too old', async function () { + convDetail.maxRateTimespan = 10; - await expect(batchConvFunction([convDetail], feeAddress)).to.be.revertedWith( - 'aggregator rate is outdated', - ); - }); + await expect(batchConvFunction([convDetail], feeAddress)).to.be.revertedWith( + 'aggregator rate is outdated', + ); + }); - it('Not enough allowance', async function () { - // signer4 connect to the batch function - setBatchConvFunction(useBatchRouter, signer4); - await expect(batchConvFunction([convDetail], feeAddress)).to.be.revertedWith( - 'Insufficient allowance for batch to pay', - ); - // reset: signer1 connect to the batch function - setBatchConvFunction(useBatchRouter, signer1); - }); + it('Not enough allowance', async function () { + // signer4 connect to the batch function + setBatchConvFunction(useBatchRouter, signer4); + await expect(batchConvFunction([convDetail], feeAddress)).to.be.revertedWith( + 'Insufficient allowance for batch to pay', + ); + // reset: signer1 connect to the batch function + setBatchConvFunction(useBatchRouter, signer1); + }); - it('Not enough funds', async function () { - // increase signer4 allowance - await testERC20 - .connect(signer4) - .approve(testBatchConversionProxy.address, thousandWith18Decimal); - // signer4 connect to the batch function - setBatchConvFunction(useBatchRouter, signer4); - - await expect(batchConvFunction([convDetail], feeAddress)).to.be.revertedWith( - 'not enough funds, including fees', - ); - - // reset: decrease signer4 allowance and reconnect with signer1 - await testERC20.connect(signer4).approve(testBatchConversionProxy.address, '0'); - testERC20.connect(signer1); - // reset: signer1 connect to the batch function - setBatchConvFunction(useBatchRouter, signer1); + it('Not enough funds', async function () { + // increase signer4 allowance + await testERC20 + .connect(signer4) + .approve(testBatchConversionProxy.address, thousandWith18Decimal); + // signer4 connect to the batch function + setBatchConvFunction(useBatchRouter, signer4); + + await expect(batchConvFunction([convDetail], feeAddress)).to.be.revertedWith( + 'not enough funds, including fees', + ); + + // reset: decrease signer4 allowance and reconnect with signer1 + await testERC20.connect(signer4).approve(testBatchConversionProxy.address, '0'); + testERC20.connect(signer1); + // reset: signer1 connect to the batch function + setBatchConvFunction(useBatchRouter, signer1); + }); }); - }); - /** Make sure the existing ERC20 functions from the parent contract BatchPaymentPublic.sol are still working */ - describe('Test BatchErc20Payments functions', () => { - it(`${ - useBatchRouter ? 'with batchRouter, ' : '' - }batchERC20PaymentsWithReference transfers token`, async function () { - await batchERC20Payments(useBatchRouter, 'batchERC20PaymentsWithReference'); - }); + /** Make sure the existing ERC20 functions from the parent contract BatchPaymentPublic.sol are still working */ + describe('Herited from contract BatchErc20Payments functions', () => { + it(`batchERC20PaymentsWithReference transfers token`, async function () { + await batchERC20Payments(useBatchRouter, 'batchERC20PaymentsWithReference'); + }); - it(`${ - useBatchRouter ? 'with batchRouter, ' : '' - }batchERC20PaymentsMultiTokensWithReference transfers token`, async function () { - await batchERC20Payments(useBatchRouter, 'batchERC20PaymentsMultiTokensWithReference'); + it(`batchERC20PaymentsMultiTokensWithReference transfers token`, async function () { + await batchERC20Payments(useBatchRouter, 'batchERC20PaymentsMultiTokensWithReference'); + }); }); }); } From c6dc943f72c54c3de784e804cd35e77f97430cb8 Mon Sep 17 00:00:00 2001 From: olivier7delf <55892112+olivier7delf@users.noreply.github.com> Date: Wed, 31 Aug 2022 19:03:31 +0200 Subject: [PATCH 082/105] refacto new test structure --- .../BatchConversionErc20Payments.test.ts | 495 +++++++++--------- 1 file changed, 257 insertions(+), 238 deletions(-) diff --git a/packages/smart-contracts/test/contracts/BatchConversionErc20Payments.test.ts b/packages/smart-contracts/test/contracts/BatchConversionErc20Payments.test.ts index 2f0fcd456..a87727971 100644 --- a/packages/smart-contracts/test/contracts/BatchConversionErc20Payments.test.ts +++ b/packages/smart-contracts/test/contracts/BatchConversionErc20Payments.test.ts @@ -23,6 +23,7 @@ describe('contract: BatchConversionPayments', async () => { let from: string; let to: string; let feeAddress: string; + let adminSigner: Signer; let signer1: Signer; let signer4: Signer; @@ -75,6 +76,14 @@ describe('contract: BatchConversionPayments', async () => { }; let convDetail: ConversionDetail; + const emptyCryptoDetails = { + tokenAddresses: [], + recipients: [], + amounts: [], + paymentReferences: [], + feeAmounts: [], + }; + /** * @notice Function batch conversion, it can be the batchRouter function, * used with conversion args, or directly batchERC20ConversionPaymentsMultiTokens @@ -86,9 +95,9 @@ describe('contract: BatchConversionPayments', async () => { ) => Promise; /** - * @notice it gets the conversions including fees to be paid, and it set the convDetail input + * @notice it sets the conversions including fees to be paid, and it set the convDetail input */ - const getConvToPayAndConvDetail = async ( + const setConvToPayAndConvDetail = async ( _recipient: string, _path: string[], _requestAmount: string, @@ -170,66 +179,13 @@ describe('contract: BatchConversionPayments', async () => { return [_fromDiffBalanceExpected, _toDiffBalanceExpected, _feeDiffBalanceExpected]; }; - /** - * It sets the right batch conversion function, with the associated arguments format - * @param useBatchRouter allows to use batchERC20ConversionPaymentsMultiTokens with batchRouter - * @param _signer - */ - const setBatchConvFunction = async (useBatchRouter: boolean, _signer: Signer) => { - batchConvFunction = ( - convDetails: ConversionDetail[], - feeAddress: string, - ): Promise => { - return useBatchRouter - ? testBatchConversionProxy.connect(_signer).batchRouter( - [ - { - paymentNetworkId: '0', - conversionDetails: convDetails, - cryptoDetails: { - tokenAddresses: [], - recipients: [], - amounts: [], - paymentReferences: [], - feeAmounts: [], - }, - }, - ], - feeAddress, - ) - : testBatchConversionProxy - .connect(_signer) - .batchERC20ConversionPaymentsMultiTokens(convDetails, feeAddress); - }; - }; - /** * @notice update convDetail, do an ERC20 conversion batch payment with a single payment inside and calculate the balances * @param path to update the convDetail */ const onePaymentBatchConv = async (path: string[]) => { - await getConvToPayAndConvDetail(to, path, amountInFiat, feesAmountInFiat, 0, chainlinkPath); - - const result = batchConvFunction([convDetail], feeAddress); - await expect(result) - .to.emit(erc20ConversionProxy, 'TransferWithConversionAndReference') - .withArgs( - convDetail.requestAmount, - ethers.utils.getAddress(convDetail.path[0]), - ethers.utils.keccak256(referenceExample), - convDetail.feeAmount, - '0', - ) - .to.emit(erc20ConversionProxy, 'TransferWithReferenceAndFee') - .withArgs( - ethers.utils.getAddress(DAI_address), - ethers.utils.getAddress(convDetail.recipient), - conversionToPay, - ethers.utils.keccak256(referenceExample), - conversionFees, - feeAddress, - ); - + await setConvToPayAndConvDetail(to, path, amountInFiat, feesAmountInFiat, 0, chainlinkPath); + await batchConvFunction([convDetail], feeAddress); [fromDiffBalanceExpected, toDiffBalanceExpected, feeDiffBalanceExpected] = expectedERC20Balances([conversionToPay], [conversionFees], batchConvFee); }; @@ -239,71 +195,69 @@ describe('contract: BatchConversionPayments', async () => { * and calculate the balances * @param path2 to update the second convDetail */ - const manyPaymentsBatchConv = async (path2: string[], nTimes: number) => { + const manyPaymentsBatchConv = async ( + path1: string[], + path2: string[], + withBatchRouter = false, + ) => { + await setConvToPayAndConvDetail(to, path1, amountInFiat, feesAmountInFiat, 0, chainlinkPath); // define a second payment request - const amountInFiat2 = BigNumber.from(amountInFiat).mul(2).toString(); - const feesAmountInFiat2 = BigNumber.from(feesAmountInFiat).mul(2).toString(); - - const conversionToPay2 = (await chainlinkPath.getConversion(amountInFiat2, path2)).result; - const conversionFees2 = (await chainlinkPath.getConversion(feesAmountInFiat2, path2)).result; - - let convDetail2 = Utils.deepCopy(convDetail); - + const conversionToPay2 = (await chainlinkPath.getConversion(amountInFiat, path2)).result; + const conversionFees2 = (await chainlinkPath.getConversion(feesAmountInFiat, path2)).result; + const convDetail2 = Utils.deepCopy(convDetail); convDetail2.path = path2; - convDetail2.requestAmount = amountInFiat2; - convDetail2.feeAmount = feesAmountInFiat2; convDetail2.maxToSpend = conversionToPay2.add(conversionFees2).toString(); - // define the new arg convDetails for the function, - // and conversionsToPays & conversionsFees to calculate the expected balances - let convDetails: ConversionDetail[] = []; - let conversionsToPays: BigNumber[] = []; - let conversionsFees: BigNumber[] = []; - for (let i = 0; i < nTimes; i++) { - convDetails = convDetails.concat([convDetail, convDetail2]); - conversionsToPays = conversionsToPays.concat([conversionToPay, conversionToPay2]); - conversionsFees = conversionsFees.concat([conversionFees, conversionFees2]); - } + // define conversionsToPays & conversionsFees to calculate the expected balances + const conversionsToPays = [conversionToPay, conversionToPay, conversionToPay2]; + const conversionsFees = [conversionFees, conversionFees, conversionFees2]; // get balances of the 2nd token, useful when there are 2 different tokens used const fromOldBalance2 = await testERC20b.balanceOf(from); const toOldBalance2 = await testERC20b.balanceOf(to); const feeOldBalance2 = await testERC20b.balanceOf(feeAddress); - await batchConvFunction(convDetails, feeAddress); - - // 1st condition: every tokens (end of the paths) are identicals - if ( - convDetail.path[convDetail.path.length - 1] === convDetail2.path[convDetail2.path.length - 1] - ) { - [fromDiffBalanceExpected, toDiffBalanceExpected, feeDiffBalanceExpected] = - expectedERC20Balances(conversionsToPays, conversionsFees, batchConvFee); - } - // else: there are 2 different tokens used (end of the paths): testERC20 and testERC20b - else { - // calculate the expected balances of the 1st token: testERC20 - const conversionsToPayToken1 = conversionsToPays.filter((_, i) => i % 2 === 0); - const conversionsFeesToken1 = conversionsFees.filter((_, i) => i % 2 === 0); - [fromDiffBalanceExpected, toDiffBalanceExpected, feeDiffBalanceExpected] = - expectedERC20Balances(conversionsToPayToken1, conversionsFeesToken1, batchConvFee); - - // calculate the expected balances of the 2nd token: testERC20b - const conversionsToPayToken2 = conversionsToPays.filter((_, i) => i % 2 === 1); - const conversionsFeesToken2 = conversionsFees.filter((_, i) => i % 2 === 1); - const [fromDiffBalanceExpected2, toDiffBalanceExpected2, feeDiffBalanceExpected2] = - expectedERC20Balances(conversionsToPayToken2, conversionsFeesToken2, batchConvFee); - - // check the balance of testERC20b token, which is not checked in "afterEach" as testERC20 token. - checkBalancesForOneToken( - testERC20b, - fromOldBalance2, - toOldBalance2, - feeOldBalance2, - fromDiffBalanceExpected2, - toDiffBalanceExpected2, - feeDiffBalanceExpected2, + if (withBatchRouter) { + await batchConvFunction( + [ + { + paymentNetworkId: '0', + conversionDetails: [convDetail, convDetail, convDetail2], + cryptoDetails: emptyCryptoDetails, + }, + ], + feeAddress, ); + } else { + await batchConvFunction([convDetail, convDetail, convDetail2], feeAddress); } + + // 1st token: testERC20 - calculate the expected balances + [fromDiffBalanceExpected, toDiffBalanceExpected, feeDiffBalanceExpected] = + expectedERC20Balances( + conversionsToPays.slice(0, 2), + conversionsFees.slice(0, 2), + batchConvFee, + ); + + // 2nd token: testERC20b - calculate the expected balances + const [fromDiffBalanceExpected2, toDiffBalanceExpected2, feeDiffBalanceExpected2] = + expectedERC20Balances( + conversionsToPays.slice(2, 3), + conversionsFees.slice(2, 3), + batchConvFee, + ); + + // check the balance of 2nd token, which is not checked in "afterEach" as 1st token. + checkBalancesForOneToken( + testERC20b, + fromOldBalance2, + toOldBalance2, + feeOldBalance2, + fromDiffBalanceExpected2, + toDiffBalanceExpected2, + feeDiffBalanceExpected2, + ); }; /** @@ -357,9 +311,9 @@ describe('contract: BatchConversionPayments', async () => { [fromDiffBalanceExpected, toDiffBalanceExpected, feeDiffBalanceExpected] = expectedERC20Balances([BigNumber.from(amount)], [BigNumber.from(feeAmount)], batchFee, false); }; + before(async () => { [, from, to, feeAddress] = (await ethers.getSigners()).map((s) => s.address); - let adminSigner: Signer; [adminSigner, signer1, , , signer4] = await ethers.getSigners(); chainlinkPath = chainlinkConversionPath.connect(network.name, signer1); @@ -402,138 +356,203 @@ describe('contract: BatchConversionPayments', async () => { testERC20b = TestERC20__factory.connect(testERC20b.address, signer1); }); - /** - * @notice it contains all the tests related to the ERC20 batch payment, and its context required - * @param useBatchRouter allows to use the function "batchERC20ConversionPaymentsMultiTokens" - * through the batchRouter or directly - */ - for (const useBatchRouter of [true, false]) { - beforeEach(async () => { - setBatchConvFunction(useBatchRouter, signer1); - - fromDiffBalanceExpected = BigNumber.from(0); - toDiffBalanceExpected = BigNumber.from(0); - feeDiffBalanceExpected = BigNumber.from(0); - await testERC20.approve(testBatchConversionProxy.address, thousandWith18Decimal, { - from, - }); - await testERC20b.approve(testBatchConversionProxy.address, thousandWith18Decimal, { - from, - }); - // get balances of testERC20 token - fromOldBalance = await testERC20.balanceOf(from); - toOldBalance = await testERC20.balanceOf(to); - feeOldBalance = await testERC20.balanceOf(feeAddress); - - // create a default convDetail - getConvToPayAndConvDetail( - to, - [USD_hash, DAI_address], - amountInFiat, - feesAmountInFiat, - 0, - chainlinkPath, + beforeEach(async () => { + fromDiffBalanceExpected = BigNumber.from(0); + toDiffBalanceExpected = BigNumber.from(0); + feeDiffBalanceExpected = BigNumber.from(0); + await testERC20.approve(testBatchConversionProxy.address, thousandWith18Decimal, { + from, + }); + await testERC20b.approve(testBatchConversionProxy.address, thousandWith18Decimal, { + from, + }); + // get balances of testERC20 token + fromOldBalance = await testERC20.balanceOf(from); + toOldBalance = await testERC20.balanceOf(to); + feeOldBalance = await testERC20.balanceOf(feeAddress); + + // create a default convDetail + setConvToPayAndConvDetail( + to, + [EUR_hash, USD_hash, DAI_address], + amountInFiat, + feesAmountInFiat, + 0, + chainlinkPath, + ); + }); + + afterEach(async () => { + // check balances of testERC20 token + checkBalancesForOneToken( + testERC20, + fromOldBalance, + toOldBalance, + feeOldBalance, + fromDiffBalanceExpected, + toDiffBalanceExpected, + feeDiffBalanceExpected, + ); + }); + + describe('batchERC20ConversionPaymentsMultiTokens', async () => { + it('make 1 payment with 1-step conversion', async () => { + batchConvFunction = testBatchConversionProxy.batchERC20ConversionPaymentsMultiTokens; + await onePaymentBatchConv([USD_hash, DAI_address]); + }); + it('make 1 payment with 2-steps conversion', async () => { + batchConvFunction = testBatchConversionProxy.batchERC20ConversionPaymentsMultiTokens; + await onePaymentBatchConv([EUR_hash, USD_hash, DAI_address]); + }); + it('make 3 payment with different tokens and conversion length', async () => { + batchConvFunction = testBatchConversionProxy.batchERC20ConversionPaymentsMultiTokens; + await manyPaymentsBatchConv([EUR_hash, USD_hash, DAI_address], [USD_hash, FAU_address]); + }); + }); + + describe('batchERC20ConversionPaymentsMultiTokens errors', async () => { + before(async () => { + batchConvFunction = testBatchConversionProxy.batchERC20ConversionPaymentsMultiTokens; + }); + it('cannot transfer with invalid path', async function () { + convDetail.path = [EUR_hash, ETH_hash, DAI_address]; + await expect(batchConvFunction([convDetail], feeAddress)).to.be.revertedWith( + 'revert No aggregator found', + ); + }); + + it('cannot transfer if max to spend too low', async function () { + convDetail.maxToSpend = conversionToPay.add(conversionFees).sub(1).toString(); + await expect(batchConvFunction([convDetail], feeAddress)).to.be.revertedWith( + 'Amount to pay is over the user limit', + ); + }); + + it('cannot transfer if rate is too old', async function () { + convDetail.maxRateTimespan = 10; + await expect(batchConvFunction([convDetail], feeAddress)).to.be.revertedWith( + 'aggregator rate is outdated', + ); + }); + + it('Not enough allowance', async function () { + // reduce signer1 allowance + await testERC20.approve( + testBatchConversionProxy.address, + BigNumber.from(convDetail.maxToSpend).sub(2), + { + from, + }, ); + await expect(batchConvFunction([convDetail], feeAddress)).to.be.revertedWith( + 'Insufficient allowance for batch to pay', + ); + }); + + it('Not enough funds even if partially enough funds', async function () { + // signer1 transfer enough token to pay just 1 invoice to signer4 + await testERC20 + .connect(signer1) + .transfer(await signer4.getAddress(), BigNumber.from(convDetail.maxToSpend)); + // increase signer4 allowance + await testERC20 + .connect(signer4) + .approve(testBatchConversionProxy.address, thousandWith18Decimal); + + batchConvFunction = + testBatchConversionProxy.connect(signer4).batchERC20ConversionPaymentsMultiTokens; + + // 3 invoices to pay + await expect( + batchConvFunction([convDetail, convDetail, convDetail], feeAddress), + ).to.be.revertedWith('not enough funds, including fees'); + + // signer4 transfer token to signer1 + await testERC20 + .connect(signer4) + .transfer(from, await testERC20.balanceOf(await signer4.getAddress())); + testERC20.connect(adminSigner); + testBatchConversionProxy = testBatchConversionProxy.connect(signer1); + }); + }); + + describe('batchRouter', async () => { + it(`1 payment with no conversion`, async function () { + await batchERC20Payments(true, 'batchERC20PaymentsMultiTokensWithReference'); + }); + it('make 3 payment with different tokens and conversion length', async () => { + batchConvFunction = testBatchConversionProxy.batchRouter; + await manyPaymentsBatchConv([EUR_hash, USD_hash, DAI_address], [USD_hash, FAU_address], true); }); - afterEach(async () => { - // check balances of testERC20 token - checkBalancesForOneToken( - testERC20, - fromOldBalance, - toOldBalance, - feeOldBalance, - fromDiffBalanceExpected, - toDiffBalanceExpected, - feeDiffBalanceExpected, + it('make n heterogeneous payments', async () => { + // set convDetail: done "beforeEach" + + // set cryptoDetails + const amount = 200000; + const feeAmount = 3000; + const tokenAddress = testERC20.address; + const cryptoDetails = { + tokenAddresses: [tokenAddress], + recipients: [to], + amounts: [amount], + paymentReferences: [referenceExample], + feeAmounts: [feeAmount], + }; + + testBatchConversionProxy.batchRouter( + [ + { + paymentNetworkId: 0, + conversionDetails: [convDetail], + cryptoDetails: emptyCryptoDetails, + }, + { + paymentNetworkId: 2, + conversionDetails: [], + cryptoDetails: cryptoDetails, + }, + ], + feeAddress, + ); + + const [ + conversionFromDiffBalanceExpected, + conversionToDiffBalanceExpected, + conversionFeeDiffBalanceExpected, + ] = expectedERC20Balances([conversionToPay], [conversionFees], batchConvFee); + + const [ + noConversionFromDiffBalanceExpected, + noConversionToDiffBalanceExpected, + noConversionFeeDiffBalanceExpected, + ] = expectedERC20Balances( + [BigNumber.from(amount)], + [BigNumber.from(feeAmount)], + batchFee, + false, + ); + + fromDiffBalanceExpected = conversionFromDiffBalanceExpected.add( + noConversionFromDiffBalanceExpected, + ); + toDiffBalanceExpected = conversionToDiffBalanceExpected.add( + noConversionToDiffBalanceExpected, + ); + feeDiffBalanceExpected = conversionFeeDiffBalanceExpected.add( + noConversionFeeDiffBalanceExpected, ); }); + }); - describe(useBatchRouter ? 'Through batchRouter' : 'Without batchRouter ', async () => { - describe('batchERC20ConversionPaymentsMultiTokens with DAI', async () => { - it('allows to transfer DAI tokens for USD payment', async () => { - await onePaymentBatchConv([USD_hash, DAI_address]); - }); - it('allows to transfer DAI tokens for EUR payment', async () => { - await onePaymentBatchConv([EUR_hash, USD_hash, DAI_address]); - }); - it('allows to transfer 2 transactions DAI tokens for USD payment', async function () { - await manyPaymentsBatchConv([USD_hash, DAI_address], 1); - }); - it('allows to transfer DAI tokens for EUR payment', async () => { - await onePaymentBatchConv([EUR_hash, USD_hash, DAI_address]); - }); - it('allows to transfer 2 transactions DAI tokens for USD and EUR payments', async function () { - await manyPaymentsBatchConv([EUR_hash, USD_hash, DAI_address], 1); - }); - it('allows to transfer two kinds of tokens for USD', async function () { - await manyPaymentsBatchConv([USD_hash, FAU_address], 1); - }); - }); - - describe('batchERC20ConversionPaymentsMultiTokens with errors', () => { - it('cannot transfer with invalid path', async function () { - convDetail.path = [EUR_hash, ETH_hash, DAI_address]; - await expect(batchConvFunction([convDetail], feeAddress)).to.be.revertedWith( - 'revert No aggregator found', - ); - }); - - it('cannot transfer if max to spend too low', async function () { - convDetail.maxToSpend = conversionToPay.add(conversionFees).sub(1).toString(); - await expect(batchConvFunction([convDetail], feeAddress)).to.be.revertedWith( - 'Amount to pay is over the user limit', - ); - }); - - it('cannot transfer if rate is too old', async function () { - convDetail.maxRateTimespan = 10; - - await expect(batchConvFunction([convDetail], feeAddress)).to.be.revertedWith( - 'aggregator rate is outdated', - ); - }); - - it('Not enough allowance', async function () { - // signer4 connect to the batch function - setBatchConvFunction(useBatchRouter, signer4); - await expect(batchConvFunction([convDetail], feeAddress)).to.be.revertedWith( - 'Insufficient allowance for batch to pay', - ); - // reset: signer1 connect to the batch function - setBatchConvFunction(useBatchRouter, signer1); - }); - - it('Not enough funds', async function () { - // increase signer4 allowance - await testERC20 - .connect(signer4) - .approve(testBatchConversionProxy.address, thousandWith18Decimal); - // signer4 connect to the batch function - setBatchConvFunction(useBatchRouter, signer4); - - await expect(batchConvFunction([convDetail], feeAddress)).to.be.revertedWith( - 'not enough funds, including fees', - ); - - // reset: decrease signer4 allowance and reconnect with signer1 - await testERC20.connect(signer4).approve(testBatchConversionProxy.address, '0'); - testERC20.connect(signer1); - // reset: signer1 connect to the batch function - setBatchConvFunction(useBatchRouter, signer1); - }); - }); - - /** Make sure the existing ERC20 functions from the parent contract BatchPaymentPublic.sol are still working */ - describe('Herited from contract BatchErc20Payments functions', () => { - it(`batchERC20PaymentsWithReference transfers token`, async function () { - await batchERC20Payments(useBatchRouter, 'batchERC20PaymentsWithReference'); - }); - - it(`batchERC20PaymentsMultiTokensWithReference transfers token`, async function () { - await batchERC20Payments(useBatchRouter, 'batchERC20PaymentsMultiTokensWithReference'); - }); - }); + /** Make sure the existing ERC20 functions from the parent contract BatchPaymentPublic.sol are still working */ + describe('Functions herited from contract BatchErc20Payments ', () => { + it(`batchERC20PaymentsWithReference 1 payment`, async function () { + await batchERC20Payments(false, 'batchERC20PaymentsWithReference'); }); - } + + it(`batchERC20PaymentsMultiTokensWithReference 1 payment`, async function () { + await batchERC20Payments(false, 'batchERC20PaymentsMultiTokensWithReference'); + }); + }); }); From 44d6372bb4fe9516f264e74de42cb221af4de268 Mon Sep 17 00:00:00 2001 From: olivier7delf <55892112+olivier7delf@users.noreply.github.com> Date: Wed, 31 Aug 2022 20:40:08 +0200 Subject: [PATCH 083/105] refacto test add eth beginning --- .../BatchConversionErc20Payments.test.ts | 629 ++++++++++++------ .../BatchConversionErc20PaymentsOLD.test.ts | 1 + 2 files changed, 445 insertions(+), 185 deletions(-) diff --git a/packages/smart-contracts/test/contracts/BatchConversionErc20Payments.test.ts b/packages/smart-contracts/test/contracts/BatchConversionErc20Payments.test.ts index a87727971..eaa928141 100644 --- a/packages/smart-contracts/test/contracts/BatchConversionErc20Payments.test.ts +++ b/packages/smart-contracts/test/contracts/BatchConversionErc20Payments.test.ts @@ -18,8 +18,12 @@ import { CurrencyManager } from '@requestnetwork/currency'; import { chainlinkConversionPath } from '../../src/lib'; import { localERC20AlphaArtifact, secondLocalERC20AlphaArtifact } from './localArtifacts'; import Utils from '@requestnetwork/utils'; +import { HttpNetworkConfig } from 'hardhat/types'; describe('contract: BatchConversionPayments', async () => { + const networkConfig = network.config as HttpNetworkConfig; + const provider = new ethers.providers.JsonRpcProvider(networkConfig.url); + let from: string; let to: string; let feeAddress: string; @@ -27,6 +31,8 @@ describe('contract: BatchConversionPayments', async () => { let signer1: Signer; let signer4: Signer; + let tx: ContractTransaction; + // constants used to set up batch conversion proxy, and also requests payment const batchFee = 50; const batchConvFee = 100; @@ -60,9 +66,23 @@ describe('contract: BatchConversionPayments', async () => { let toDiffBalanceExpected: BigNumber; let feeDiffBalanceExpected: BigNumber; + let beforeEthBalanceTo: BigNumber; + let beforeEthBalanceFee: BigNumber; + let beforeEthBalance: BigNumber; + + let amountToPayExpected: BigNumber; + let feeToPayExpected: BigNumber; + + // amount and feeAmount are usually in fiat for conversion inputs, else in ETH + const amount = BigNumber.from(100000); + const feeAmount = amount.mul(10).div(10000); + // variables needed for chainlink and conversion payments let conversionToPay: BigNumber; let conversionFees: BigNumber; + // TODO check coherence + let usdConversionToPay: BigNumber; + let usdConversionFee: BigNumber; // type required by Erc20 conversion batch function inputs type ConversionDetail = { @@ -75,6 +95,7 @@ describe('contract: BatchConversionPayments', async () => { maxRateTimespan: BigNumberish; }; let convDetail: ConversionDetail; + let inputs: Array; const emptyCryptoDetails = { tokenAddresses: [], @@ -269,8 +290,6 @@ describe('contract: BatchConversionPayments', async () => { */ const batchERC20Payments = async (useBatchRouter: boolean, erc20Function: string) => { // set up main variables - const amount = 200000; - const feeAmount = 3000; const tokenAddress = testERC20.address; // Select the batch function and pay @@ -312,6 +331,60 @@ describe('contract: BatchConversionPayments', async () => { expectedERC20Balances([BigNumber.from(amount)], [BigNumber.from(feeAmount)], batchFee, false); }; + /** + * @notice it modify the Eth batch conversion inputs if needed, depending it is + * directly or through batchRouter + * @param useBatchRouter + * @param inputs a list of convDetail + */ + const getEthConvInputs = (useBatchRouter: boolean, inputs: Array) => { + if (useBatchRouter) { + return [ + { + paymentNetworkId: '3', + conversionDetails: inputs, + cryptoDetails: { + tokenAddresses: [], + recipients: [], + amounts: [], + paymentReferences: [], + feeAmounts: [], + }, // cryptoDetails is not used + }, + ]; + } + return inputs; + }; + + const checkEthBalances = async (amountToPayExpected: BigNumber, feeToPayExpected: BigNumber) => { + const receipt = await tx.wait(); + const gasUsed = receipt.gasUsed.mul(2 * 10 ** 10); + + const afterEthBalance = await provider.getBalance(await signer1.getAddress()); + const afterEthBalanceTo = await provider.getBalance(to); + const afterEthBalanceFee = await provider.getBalance(feeAddress); + const proxyBalance = await provider.getBalance(testBatchConversionProxy.address); + + // Calculate the difference of the balance : now - before + const _diffBalance = beforeEthBalance.sub(afterEthBalance); + const _diffBalanceTo = afterEthBalanceTo.sub(beforeEthBalanceTo); + const _diffBalanceFee = afterEthBalanceFee.sub(beforeEthBalanceFee); + + // feeToPayExpected includes batch conversion fees now + feeToPayExpected = amountToPayExpected + .add(feeToPayExpected) + .mul(batchConvFee) + .div(10000) + .add(feeToPayExpected); + const _diffBalanceExpect = gasUsed.add(amountToPayExpected).add(feeToPayExpected); + + // Check balance changes + expect(_diffBalance).to.equals(_diffBalanceExpect, 'DiffBalance'); + expect(_diffBalanceTo).to.equals(amountToPayExpected, 'diffBalanceTo'); + expect(_diffBalanceFee).to.equals(feeToPayExpected, 'diffBalanceFee'); + expect(proxyBalance).to.equals('0', 'proxyBalance'); + }; + before(async () => { [, from, to, feeAddress] = (await ethers.getSigners()).map((s) => s.address); [adminSigner, signer1, , , signer4] = await ethers.getSigners(); @@ -355,204 +428,390 @@ describe('contract: BatchConversionPayments', async () => { await testERC20b.transfer(from, BigNumber.from(thousandWith18Decimal)); testERC20b = TestERC20__factory.connect(testERC20b.address, signer1); }); - - beforeEach(async () => { - fromDiffBalanceExpected = BigNumber.from(0); - toDiffBalanceExpected = BigNumber.from(0); - feeDiffBalanceExpected = BigNumber.from(0); - await testERC20.approve(testBatchConversionProxy.address, thousandWith18Decimal, { - from, - }); - await testERC20b.approve(testBatchConversionProxy.address, thousandWith18Decimal, { - from, - }); - // get balances of testERC20 token - fromOldBalance = await testERC20.balanceOf(from); - toOldBalance = await testERC20.balanceOf(to); - feeOldBalance = await testERC20.balanceOf(feeAddress); - - // create a default convDetail - setConvToPayAndConvDetail( - to, - [EUR_hash, USD_hash, DAI_address], - amountInFiat, - feesAmountInFiat, - 0, - chainlinkPath, - ); - }); - - afterEach(async () => { - // check balances of testERC20 token - checkBalancesForOneToken( - testERC20, - fromOldBalance, - toOldBalance, - feeOldBalance, - fromDiffBalanceExpected, - toDiffBalanceExpected, - feeDiffBalanceExpected, - ); - }); - - describe('batchERC20ConversionPaymentsMultiTokens', async () => { - it('make 1 payment with 1-step conversion', async () => { - batchConvFunction = testBatchConversionProxy.batchERC20ConversionPaymentsMultiTokens; - await onePaymentBatchConv([USD_hash, DAI_address]); - }); - it('make 1 payment with 2-steps conversion', async () => { - batchConvFunction = testBatchConversionProxy.batchERC20ConversionPaymentsMultiTokens; - await onePaymentBatchConv([EUR_hash, USD_hash, DAI_address]); - }); - it('make 3 payment with different tokens and conversion length', async () => { - batchConvFunction = testBatchConversionProxy.batchERC20ConversionPaymentsMultiTokens; - await manyPaymentsBatchConv([EUR_hash, USD_hash, DAI_address], [USD_hash, FAU_address]); - }); - }); - - describe('batchERC20ConversionPaymentsMultiTokens errors', async () => { - before(async () => { - batchConvFunction = testBatchConversionProxy.batchERC20ConversionPaymentsMultiTokens; - }); - it('cannot transfer with invalid path', async function () { - convDetail.path = [EUR_hash, ETH_hash, DAI_address]; - await expect(batchConvFunction([convDetail], feeAddress)).to.be.revertedWith( - 'revert No aggregator found', + describe('ERC20', async () => { + beforeEach(async () => { + fromDiffBalanceExpected = BigNumber.from(0); + toDiffBalanceExpected = BigNumber.from(0); + feeDiffBalanceExpected = BigNumber.from(0); + await testERC20.approve(testBatchConversionProxy.address, thousandWith18Decimal, { + from, + }); + await testERC20b.approve(testBatchConversionProxy.address, thousandWith18Decimal, { + from, + }); + // get balances of testERC20 token + fromOldBalance = await testERC20.balanceOf(from); + toOldBalance = await testERC20.balanceOf(to); + feeOldBalance = await testERC20.balanceOf(feeAddress); + + // create a default convDetail + setConvToPayAndConvDetail( + to, + [EUR_hash, USD_hash, DAI_address], + amountInFiat, + feesAmountInFiat, + 0, + chainlinkPath, ); }); - it('cannot transfer if max to spend too low', async function () { - convDetail.maxToSpend = conversionToPay.add(conversionFees).sub(1).toString(); - await expect(batchConvFunction([convDetail], feeAddress)).to.be.revertedWith( - 'Amount to pay is over the user limit', + afterEach(async () => { + // check balances of testERC20 token + checkBalancesForOneToken( + testERC20, + fromOldBalance, + toOldBalance, + feeOldBalance, + fromDiffBalanceExpected, + toDiffBalanceExpected, + feeDiffBalanceExpected, ); }); - it('cannot transfer if rate is too old', async function () { - convDetail.maxRateTimespan = 10; - await expect(batchConvFunction([convDetail], feeAddress)).to.be.revertedWith( - 'aggregator rate is outdated', - ); + describe('batchERC20ConversionPaymentsMultiTokens', async () => { + it('make 1 payment with 1-step conversion', async () => { + batchConvFunction = testBatchConversionProxy.batchERC20ConversionPaymentsMultiTokens; + await onePaymentBatchConv([USD_hash, DAI_address]); + }); + it('make 1 payment with 2-steps conversion', async () => { + batchConvFunction = testBatchConversionProxy.batchERC20ConversionPaymentsMultiTokens; + await onePaymentBatchConv([EUR_hash, USD_hash, DAI_address]); + }); + it('make 3 payment with different tokens and conversion length', async () => { + batchConvFunction = testBatchConversionProxy.batchERC20ConversionPaymentsMultiTokens; + await manyPaymentsBatchConv([EUR_hash, USD_hash, DAI_address], [USD_hash, FAU_address]); + }); }); - it('Not enough allowance', async function () { - // reduce signer1 allowance - await testERC20.approve( - testBatchConversionProxy.address, - BigNumber.from(convDetail.maxToSpend).sub(2), - { - from, - }, - ); - await expect(batchConvFunction([convDetail], feeAddress)).to.be.revertedWith( - 'Insufficient allowance for batch to pay', - ); + describe('batchERC20ConversionPaymentsMultiTokens errors', async () => { + before(async () => { + batchConvFunction = testBatchConversionProxy.batchERC20ConversionPaymentsMultiTokens; + }); + it('cannot transfer with invalid path', async function () { + convDetail.path = [EUR_hash, ETH_hash, DAI_address]; + await expect(batchConvFunction([convDetail], feeAddress)).to.be.revertedWith( + 'revert No aggregator found', + ); + }); + + it('cannot transfer if max to spend too low', async function () { + convDetail.maxToSpend = conversionToPay.add(conversionFees).sub(1).toString(); + await expect(batchConvFunction([convDetail], feeAddress)).to.be.revertedWith( + 'Amount to pay is over the user limit', + ); + }); + + it('cannot transfer if rate is too old', async function () { + convDetail.maxRateTimespan = 10; + await expect(batchConvFunction([convDetail], feeAddress)).to.be.revertedWith( + 'aggregator rate is outdated', + ); + }); + + it('Not enough allowance', async function () { + // reduce signer1 allowance + await testERC20.approve( + testBatchConversionProxy.address, + BigNumber.from(convDetail.maxToSpend).sub(2), + { + from, + }, + ); + await expect(batchConvFunction([convDetail], feeAddress)).to.be.revertedWith( + 'Insufficient allowance for batch to pay', + ); + }); + + it('Not enough funds even if partially enough funds', async function () { + // signer1 transfer enough token to pay just 1 invoice to signer4 + await testERC20 + .connect(signer1) + .transfer(await signer4.getAddress(), BigNumber.from(convDetail.maxToSpend)); + // increase signer4 allowance + await testERC20 + .connect(signer4) + .approve(testBatchConversionProxy.address, thousandWith18Decimal); + + batchConvFunction = + testBatchConversionProxy.connect(signer4).batchERC20ConversionPaymentsMultiTokens; + + // 3 invoices to pay + await expect( + batchConvFunction([convDetail, convDetail, convDetail], feeAddress), + ).to.be.revertedWith('not enough funds, including fees'); + + // signer4 transfer token to signer1 + await testERC20 + .connect(signer4) + .transfer(from, await testERC20.balanceOf(await signer4.getAddress())); + testERC20.connect(adminSigner); + testBatchConversionProxy = testBatchConversionProxy.connect(signer1); + }); }); - it('Not enough funds even if partially enough funds', async function () { - // signer1 transfer enough token to pay just 1 invoice to signer4 - await testERC20 - .connect(signer1) - .transfer(await signer4.getAddress(), BigNumber.from(convDetail.maxToSpend)); - // increase signer4 allowance - await testERC20 - .connect(signer4) - .approve(testBatchConversionProxy.address, thousandWith18Decimal); - - batchConvFunction = - testBatchConversionProxy.connect(signer4).batchERC20ConversionPaymentsMultiTokens; - - // 3 invoices to pay - await expect( - batchConvFunction([convDetail, convDetail, convDetail], feeAddress), - ).to.be.revertedWith('not enough funds, including fees'); - - // signer4 transfer token to signer1 - await testERC20 - .connect(signer4) - .transfer(from, await testERC20.balanceOf(await signer4.getAddress())); - testERC20.connect(adminSigner); - testBatchConversionProxy = testBatchConversionProxy.connect(signer1); + describe('batchRouter', async () => { + it(`1 payment with no conversion`, async function () { + await batchERC20Payments(true, 'batchERC20PaymentsMultiTokensWithReference'); + }); + it('make 3 payment with different tokens and conversion length', async () => { + batchConvFunction = testBatchConversionProxy.batchRouter; + await manyPaymentsBatchConv( + [EUR_hash, USD_hash, DAI_address], + [USD_hash, FAU_address], + true, + ); + }); + + it('make n heterogeneous payments', async () => { + // set convDetail: done "beforeEach" + + // set cryptoDetails + const tokenAddress = testERC20.address; + const cryptoDetails = { + tokenAddresses: [tokenAddress], + recipients: [to], + amounts: [amount], + paymentReferences: [referenceExample], + feeAmounts: [feeAmount], + }; + + testBatchConversionProxy.batchRouter( + [ + { + paymentNetworkId: 0, + conversionDetails: [convDetail], + cryptoDetails: emptyCryptoDetails, + }, + { + paymentNetworkId: 2, + conversionDetails: [], + cryptoDetails: cryptoDetails, + }, + ], + feeAddress, + ); + + const [ + conversionFromDiffBalanceExpected, + conversionToDiffBalanceExpected, + conversionFeeDiffBalanceExpected, + ] = expectedERC20Balances([conversionToPay], [conversionFees], batchConvFee); + + const [ + noConversionFromDiffBalanceExpected, + noConversionToDiffBalanceExpected, + noConversionFeeDiffBalanceExpected, + ] = expectedERC20Balances( + [BigNumber.from(amount)], + [BigNumber.from(feeAmount)], + batchFee, + false, + ); + + fromDiffBalanceExpected = conversionFromDiffBalanceExpected.add( + noConversionFromDiffBalanceExpected, + ); + toDiffBalanceExpected = conversionToDiffBalanceExpected.add( + noConversionToDiffBalanceExpected, + ); + feeDiffBalanceExpected = conversionFeeDiffBalanceExpected.add( + noConversionFeeDiffBalanceExpected, + ); + }); }); - }); - describe('batchRouter', async () => { - it(`1 payment with no conversion`, async function () { - await batchERC20Payments(true, 'batchERC20PaymentsMultiTokensWithReference'); - }); - it('make 3 payment with different tokens and conversion length', async () => { - batchConvFunction = testBatchConversionProxy.batchRouter; - await manyPaymentsBatchConv([EUR_hash, USD_hash, DAI_address], [USD_hash, FAU_address], true); - }); + /** Make sure the existing ERC20 functions from the parent contract BatchPaymentPublic.sol are still working */ + describe('Functions herited from contract BatchErc20Payments ', () => { + it(`batchERC20PaymentsWithReference 1 payment`, async function () { + await batchERC20Payments(false, 'batchERC20PaymentsWithReference'); + }); - it('make n heterogeneous payments', async () => { - // set convDetail: done "beforeEach" - - // set cryptoDetails - const amount = 200000; - const feeAmount = 3000; - const tokenAddress = testERC20.address; - const cryptoDetails = { - tokenAddresses: [tokenAddress], - recipients: [to], - amounts: [amount], - paymentReferences: [referenceExample], - feeAmounts: [feeAmount], - }; - - testBatchConversionProxy.batchRouter( - [ - { - paymentNetworkId: 0, - conversionDetails: [convDetail], - cryptoDetails: emptyCryptoDetails, - }, - { - paymentNetworkId: 2, - conversionDetails: [], - cryptoDetails: cryptoDetails, - }, - ], - feeAddress, - ); - - const [ - conversionFromDiffBalanceExpected, - conversionToDiffBalanceExpected, - conversionFeeDiffBalanceExpected, - ] = expectedERC20Balances([conversionToPay], [conversionFees], batchConvFee); - - const [ - noConversionFromDiffBalanceExpected, - noConversionToDiffBalanceExpected, - noConversionFeeDiffBalanceExpected, - ] = expectedERC20Balances( - [BigNumber.from(amount)], - [BigNumber.from(feeAmount)], - batchFee, - false, - ); - - fromDiffBalanceExpected = conversionFromDiffBalanceExpected.add( - noConversionFromDiffBalanceExpected, - ); - toDiffBalanceExpected = conversionToDiffBalanceExpected.add( - noConversionToDiffBalanceExpected, - ); - feeDiffBalanceExpected = conversionFeeDiffBalanceExpected.add( - noConversionFeeDiffBalanceExpected, - ); + it(`batchERC20PaymentsMultiTokensWithReference 1 payment`, async function () { + await batchERC20Payments(false, 'batchERC20PaymentsMultiTokensWithReference'); + }); }); }); - /** Make sure the existing ERC20 functions from the parent contract BatchPaymentPublic.sol are still working */ - describe('Functions herited from contract BatchErc20Payments ', () => { - it(`batchERC20PaymentsWithReference 1 payment`, async function () { - await batchERC20Payments(false, 'batchERC20PaymentsWithReference'); - }); - - it(`batchERC20PaymentsMultiTokensWithReference 1 payment`, async function () { - await batchERC20Payments(false, 'batchERC20PaymentsMultiTokensWithReference'); + for (const useBatchRouter of [true, false]) { + describe(`Test ETH batch functions ${ + useBatchRouter ? 'through batchRouter' : 'without batchRouter' + }`, () => { + before(async () => { + convDetail = { + recipient: to, + requestAmount: amount, + path: [USD_hash, ETH_hash], + paymentReference: referenceExample, + feeAmount: feeAmount, + maxToSpend: BigNumber.from(0), + maxRateTimespan: BigNumber.from(0), + }; + + // basic setup: 1 payment + usdConversionToPay = ( + await chainlinkPath.getConversion(convDetail.requestAmount, convDetail.path) + ).result; + usdConversionFee = ( + await chainlinkPath.getConversion(convDetail.feeAmount, convDetail.path) + ).result; + + if (useBatchRouter) { + batchConvFunction = testBatchConversionProxy.batchRouter; + } else { + batchConvFunction = testBatchConversionProxy.batchEthConversionPaymentsWithReference; + } + }); + + beforeEach(async () => { + beforeEthBalanceTo = await provider.getBalance(to); + beforeEthBalanceFee = await provider.getBalance(feeAddress); + beforeEthBalance = await provider.getBalance(await signer1.getAddress()); + + // expected balances, it can be modified for each test + amountToPayExpected = usdConversionToPay; + // fees does not include batch fees yet + feeToPayExpected = usdConversionFee; + }); + + describe('success functions', () => { + it('batchEthConversionPaymentsWithReference transfer 1 payment in ethers denominated in USD', async function () { + inputs = [convDetail]; + tx = await batchConvFunction(getEthConvInputs(useBatchRouter, inputs), feeAddress, { + value: BigNumber.from('100000000000000000'), + }); + await checkEthBalances(amountToPayExpected, feeToPayExpected); + }); + + it('batchEthConversionPaymentsWithReference transfer 3 payment in ethers denominated in USD', async function () { + amountToPayExpected = amountToPayExpected.mul(3); + feeToPayExpected = feeToPayExpected.mul(3); + inputs = [convDetail, convDetail, convDetail]; + tx = await batchConvFunction(getEthConvInputs(useBatchRouter, inputs), feeAddress, { + value: BigNumber.from('100000000000000000'), + }); + await checkEthBalances(amountToPayExpected, feeToPayExpected); + }); + + it('batchEthConversionPaymentsWithReference transfer 3 payments in ethers denominated in USD and EUR', async function () { + const EurConvDetail = Utils.deepCopy(convDetail); + EurConvDetail.path = [EUR_hash, USD_hash, ETH_hash]; + + const eurConversionToPay = await chainlinkPath.getConversion( + EurConvDetail.requestAmount, + EurConvDetail.path, + ); + const eurFeesToPay = await chainlinkPath.getConversion( + EurConvDetail.feeAmount, + EurConvDetail.path, + ); + + amountToPayExpected = eurConversionToPay.result.add(amountToPayExpected.mul(2)); + feeToPayExpected = eurFeesToPay.result.add(feeToPayExpected.mul(2)); + inputs = [convDetail, EurConvDetail, convDetail]; + + tx = await batchConvFunction(getEthConvInputs(useBatchRouter, inputs), feeAddress, { + value: BigNumber.from('100000000000000000'), + }); + await checkEthBalances(amountToPayExpected, feeToPayExpected); + }); + + it('batchEthPaymentsWithReference transfer 1 payment', async function () { + beforeEthBalanceTo = await provider.getBalance(to); + beforeEthBalanceFee = await provider.getBalance(feeAddress); + beforeEthBalance = await provider.getBalance(await signer1.getAddress()); + + const cryptoDetails = { + tokenAddresses: [], + recipients: [to], + amounts: [amount], // in ETH + paymentReferences: [referenceExample], + feeAmounts: [feeAmount], // in ETH + }; + if (useBatchRouter) { + await testBatchConversionProxy.batchRouter( + [ + { + paymentNetworkId: 4, + conversionDetails: [convDetail], // not used + cryptoDetails: cryptoDetails, + }, + ], + feeAddress, + { value: 1000000000 }, + ); + } else { + await testBatchConversionProxy.batchEthPaymentsWithReference( + cryptoDetails.recipients, + cryptoDetails.amounts, + cryptoDetails.paymentReferences, + cryptoDetails.feeAmounts, + feeAddress, + { value: 1000000000 }, + ); + } + + amountToPayExpected = amount; + feeToPayExpected = feeAmount; + const afterEthBalanceTo = await provider.getBalance(to); + const afterEthBalanceFee = await provider.getBalance(feeAddress); + const proxyBalance = await provider.getBalance(testBatchConversionProxy.address); + const _diffBalanceTo = afterEthBalanceTo.sub(beforeEthBalanceTo); + const _diffBalanceFee = afterEthBalanceFee.sub(beforeEthBalanceFee); + + expect(_diffBalanceTo).to.equals(amountToPayExpected.toString(), 'diffBalanceTo'); + + feeToPayExpected = amountToPayExpected.mul(batchFee).div(10000).add(feeToPayExpected); + expect(_diffBalanceFee.toString()).to.equals( + feeToPayExpected.toString(), + 'diffBalanceFee', + ); + expect(proxyBalance).to.equals('0', 'proxyBalance'); + }); + }); + describe('revert functions', () => { + it('batchEthConversionPaymentsWithReference transfer FAIL: not enough funds', async function () { + await expect( + batchConvFunction(getEthConvInputs(useBatchRouter, [convDetail]), feeAddress, { + value: 10000, + }), + ).to.be.revertedWith('paymentProxy transferExactEthWithReferenceAndFee failed'); + }); + it('batchEthPaymentsWithReference transfer FAIL: not enough funds', async function () { + const cryptoDetails = { + tokenAddresses: [], + recipients: [to], + amounts: [amount], + paymentReferences: [referenceExample], + feeAmounts: [feeAmount], + }; + + // it contains the function being just executed, and still processing + let batchEthPayments; + if (useBatchRouter) { + batchEthPayments = testBatchConversionProxy.batchRouter( + [ + { + paymentNetworkId: 4, + conversionDetails: [convDetail], // not used + cryptoDetails: cryptoDetails, + }, + ], + feeAddress, + { value: 10000 }, + ); + } else { + batchEthPayments = testBatchConversionProxy.batchEthPaymentsWithReference( + cryptoDetails.recipients, + cryptoDetails.amounts, + cryptoDetails.paymentReferences, + cryptoDetails.feeAmounts, + feeAddress, + { value: 10000 }, + ); + } + await expect(batchEthPayments).to.be.revertedWith('not enough funds'); + }); + }); }); - }); + } }); diff --git a/packages/smart-contracts/test/contracts/BatchConversionErc20PaymentsOLD.test.ts b/packages/smart-contracts/test/contracts/BatchConversionErc20PaymentsOLD.test.ts index 2f0fcd456..b75959c96 100644 --- a/packages/smart-contracts/test/contracts/BatchConversionErc20PaymentsOLD.test.ts +++ b/packages/smart-contracts/test/contracts/BatchConversionErc20PaymentsOLD.test.ts @@ -357,6 +357,7 @@ describe('contract: BatchConversionPayments', async () => { [fromDiffBalanceExpected, toDiffBalanceExpected, feeDiffBalanceExpected] = expectedERC20Balances([BigNumber.from(amount)], [BigNumber.from(feeAmount)], batchFee, false); }; + before(async () => { [, from, to, feeAddress] = (await ethers.getSigners()).map((s) => s.address); let adminSigner: Signer; From 7cd6854b81ef6ac78944fe9bf95629f8ff1929b1 Mon Sep 17 00:00:00 2001 From: olivier7delf <55892112+olivier7delf@users.noreply.github.com> Date: Thu, 1 Sep 2022 17:40:29 +0200 Subject: [PATCH 084/105] tests batchRouter erros --- .../scripts/test-deploy-all.ts | 2 - ...test-deploy-batch-conversion-deployment.ts | 2 +- .../src/contracts/BatchConversionPayments.sol | 58 +- ...blic.sol => BatchNoConversionPayments.sol} | 17 +- .../BatchConversionPayments/0.1.0.json | 124 +-- .../BatchConversionPayments/index.ts | 2 +- .../BatchNoConversionPayments/0.1.0.json | 281 ++++++ .../BatchNoConversionPayments/index.ts | 20 + .../src/lib/artifacts/index.ts | 1 + .../BatchConversionErc20Payments.test.ts | 817 ------------------ .../BatchConversionErc20PaymentsOLD.test.ts | 540 ------------ .../BatchConversionEthPayments.test.ts | 346 -------- .../contracts/BatchConversionPayments.test.ts | 815 +++++++++++++++++ ...=> BatchNoConversionErc20Payments.test.ts} | 86 +- ...s => BatchNoConversionEthPayments.test.ts} | 76 +- .../test/contracts/localArtifacts.ts | 2 +- 16 files changed, 1302 insertions(+), 1887 deletions(-) rename packages/smart-contracts/src/contracts/{BatchPaymentsPublic.sol => BatchNoConversionPayments.sol} (96%) create mode 100644 packages/smart-contracts/src/lib/artifacts/BatchNoConversionPayments/0.1.0.json create mode 100644 packages/smart-contracts/src/lib/artifacts/BatchNoConversionPayments/index.ts delete mode 100644 packages/smart-contracts/test/contracts/BatchConversionErc20Payments.test.ts delete mode 100644 packages/smart-contracts/test/contracts/BatchConversionErc20PaymentsOLD.test.ts delete mode 100644 packages/smart-contracts/test/contracts/BatchConversionEthPayments.test.ts create mode 100644 packages/smart-contracts/test/contracts/BatchConversionPayments.test.ts rename packages/smart-contracts/test/contracts/{BatchErc20Payments.test.ts => BatchNoConversionErc20Payments.test.ts} (92%) rename packages/smart-contracts/test/contracts/{BatchEthPayments.test.ts => BatchNoConversionEthPayments.test.ts} (85%) diff --git a/packages/smart-contracts/scripts/test-deploy-all.ts b/packages/smart-contracts/scripts/test-deploy-all.ts index 392f32525..bf0e40e13 100644 --- a/packages/smart-contracts/scripts/test-deploy-all.ts +++ b/packages/smart-contracts/scripts/test-deploy-all.ts @@ -3,7 +3,6 @@ import deployRequest from './test-deploy-request-storage'; import deployPayment from './test-deploy-main-payments'; import deployConversion from './test-deploy_chainlink_contract'; import { deployEscrow } from './test-deploy-escrow-deployment'; -import { deployBatchPayment } from './test-deploy-batch-erc-eth-deployment'; import { deploySuperFluid } from './test-deploy-superfluid'; import { deployBatchConversionPayment } from './test-deploy-batch-conversion-deployment'; @@ -13,7 +12,6 @@ export default async function deploy(_args: any, hre: HardhatRuntimeEnvironment) const mainPaymentAddresses = await deployPayment(_args, hre); await deployConversion(_args, hre, mainPaymentAddresses); await deployEscrow(hre); - await deployBatchPayment(_args, hre); await deploySuperFluid(hre); await deployBatchConversionPayment(_args, hre); } diff --git a/packages/smart-contracts/scripts/test-deploy-batch-conversion-deployment.ts b/packages/smart-contracts/scripts/test-deploy-batch-conversion-deployment.ts index 7901a43c2..839b7110b 100644 --- a/packages/smart-contracts/scripts/test-deploy-batch-conversion-deployment.ts +++ b/packages/smart-contracts/scripts/test-deploy-batch-conversion-deployment.ts @@ -60,7 +60,7 @@ export async function deployBatchConversionPayment( // Check the addresses of our contracts, to avoid misleading bugs in the tests // ref to secondLocalERC20AlphaArtifact.getAddress('private'), that cannot be used in deployment - const fakeFAU_addressExpected = '0xe4e47451AAd6C89a6D9E4aD104A7b77FfE1D3b36'; + const fakeFAU_addressExpected = '0x5034F49b27353CeDc562b49eA91C7438Ea351d36'; deployAddressChecking('testERC20FakeFAU', testERC20FakeFAU.address, fakeFAU_addressExpected); deployAddressChecking( 'batchConversionPayments', diff --git a/packages/smart-contracts/src/contracts/BatchConversionPayments.sol b/packages/smart-contracts/src/contracts/BatchConversionPayments.sol index 0c97c48d0..28aaf019a 100644 --- a/packages/smart-contracts/src/contracts/BatchConversionPayments.sol +++ b/packages/smart-contracts/src/contracts/BatchConversionPayments.sol @@ -3,7 +3,7 @@ pragma solidity ^0.8.4; import './interfaces/IERC20ConversionProxy.sol'; import './interfaces/IEthConversionProxy.sol'; -import './BatchPaymentsPublic.sol'; +import './BatchNoConversionPayments.sol'; /** * @title BatchConversionPayments @@ -19,7 +19,7 @@ import './BatchPaymentsPublic.sol'; * batchRouter is the main function, but other batch payment functions are "public" in order to do * gas optimization in some cases. */ -contract BatchConversionPayments is BatchPaymentsPublic { +contract BatchConversionPayments is BatchNoConversionPayments { using SafeERC20 for IERC20; IERC20ConversionProxy public paymentErc20ConversionProxy; @@ -49,7 +49,7 @@ contract BatchConversionPayments is BatchPaymentsPublic { } /** - * @dev BatchPaymentsPublic contract input structure. + * @dev BatchNoConversionPayments contract input structure. */ struct CryptoDetails { address[] tokenAddresses; @@ -62,8 +62,8 @@ contract BatchConversionPayments is BatchPaymentsPublic { /** * @dev Used by the batchRouter to handle information for heterogeneous batches, grouped by payment network. * - paymentNetworkId: from 0 to 4, cf. `batchRouter()` method. - * - conversionDetails all the data required for conversion requests to be paid, for paymentNetworkId = 0 or 3 - * - cryptoDetails all the data required to pay requests without conversion, for paymentNetworkId = 1, 2, or 4 + * - conversionDetails all the data required for conversion requests to be paid, for paymentNetworkId = 0 or 4 + * - cryptoDetails all the data required to pay requests without conversion, for paymentNetworkId = 1, 2, or 3 */ struct MetaDetail { uint256 paymentNetworkId; @@ -84,7 +84,7 @@ contract BatchConversionPayments is BatchPaymentsPublic { address _paymentErc20ConversionProxy, address _paymentEthConversionFeeProxy, address _owner - ) BatchPaymentsPublic(_paymentErc20Proxy, _paymentEthProxy, _owner) { + ) BatchNoConversionPayments(_paymentErc20Proxy, _paymentEthProxy, _owner) { paymentErc20ConversionProxy = IERC20ConversionProxy(_paymentErc20ConversionProxy); paymentEthConversionProxy = IEthConversionProxy(_paymentEthConversionFeeProxy); batchConversionFee = 0; @@ -93,26 +93,24 @@ contract BatchConversionPayments is BatchPaymentsPublic { /** * @notice Batch payments on different payment networks at once. * @param metaDetails contains paymentNetworkId, conversionDetails, and cryptoDetails - * - batchERC20ConversionPaymentsMultiTokens, paymentNetworkId=0 - * - batchERC20PaymentsWithReference, paymentNetworkId=1 - * - batchERC20PaymentsMultiTokensWithReference, paymentNetworkId=2 - * - batchEthConversionPaymentsWithReference, paymentNetworkId=3 - * - batchEthPaymentsWithReference, paymentNetworkId=4 + * - batchMultiERC20ConversionPayments, paymentNetworkId=0 + * - batchERC20Payments, paymentNetworkId=1 + * - batchMultiERC20Payments, paymentNetworkId=2 + * - batchEthPayments, paymentNetworkId=3 + * - batchEthConversionPayments, paymentNetworkId=4 + * If metaDetails use paymentNetworkId = 4, it must be at the end of the list, or the transaction can be reverted * @param _feeAddress The address where fees should be paid * @dev batchRouter only reduces gas consumption when using more than a single payment network. * For single payment network payments, it is more efficient to use the suited batch function. */ function batchRouter(MetaDetail[] calldata metaDetails, address _feeAddress) external payable { - require(metaDetails.length < 6, 'more than 5 conversionDetails'); + require(metaDetails.length < 6, 'more than 5 metaDetails'); for (uint256 i = 0; i < metaDetails.length; i++) { MetaDetail calldata metaConversionDetail = metaDetails[i]; if (metaConversionDetail.paymentNetworkId == 0) { - batchERC20ConversionPaymentsMultiTokens( - metaConversionDetail.conversionDetails, - _feeAddress - ); + batchMultiERC20ConversionPayments(metaConversionDetail.conversionDetails, _feeAddress); } else if (metaConversionDetail.paymentNetworkId == 1) { - batchERC20PaymentsWithReference( + batchERC20Payments( metaConversionDetail.cryptoDetails.tokenAddresses[0], metaConversionDetail.cryptoDetails.recipients, metaConversionDetail.cryptoDetails.amounts, @@ -121,7 +119,7 @@ contract BatchConversionPayments is BatchPaymentsPublic { _feeAddress ); } else if (metaConversionDetail.paymentNetworkId == 2) { - batchERC20PaymentsMultiTokensWithReference( + batchMultiERC20Payments( metaConversionDetail.cryptoDetails.tokenAddresses, metaConversionDetail.cryptoDetails.recipients, metaConversionDetail.cryptoDetails.amounts, @@ -130,18 +128,22 @@ contract BatchConversionPayments is BatchPaymentsPublic { _feeAddress ); } else if (metaConversionDetail.paymentNetworkId == 3) { - batchEthConversionPaymentsWithReference( - metaConversionDetail.conversionDetails, - payable(_feeAddress) - ); - } else if (metaConversionDetail.paymentNetworkId == 4) { - batchEthPaymentsWithReference( + if (metaDetails[metaDetails.length - 1].paymentNetworkId == 4) { + // Set to false only if batchEthConversionPayments is called after this function + transferBackRemainingEth = false; + } + batchEthPayments( metaConversionDetail.cryptoDetails.recipients, metaConversionDetail.cryptoDetails.amounts, metaConversionDetail.cryptoDetails.paymentReferences, metaConversionDetail.cryptoDetails.feeAmounts, payable(_feeAddress) ); + if (metaDetails[metaDetails.length - 1].paymentNetworkId == 4) { + transferBackRemainingEth = true; + } + } else if (metaConversionDetail.paymentNetworkId == 4) { + batchEthConversionPayments(metaConversionDetail.conversionDetails, payable(_feeAddress)); } else { revert('wrong paymentNetworkId'); } @@ -149,12 +151,12 @@ contract BatchConversionPayments is BatchPaymentsPublic { } /** - * @notice Makes a batch of transfers for multiple ERC20 tokens, with amounts based on a request - * currency (e.g. fiat) and with a reference per payment. + * @notice Send a batch of ERC20 payments with amounts based on a request + * currency (e.g. fiat), with fees and paymentReferences to multiple accounts, with multiple tokens. * @param conversionDetails list of requestInfo, each one containing all the information of a request * @param _feeAddress The fee recipient */ - function batchERC20ConversionPaymentsMultiTokens( + function batchMultiERC20ConversionPayments( ConversionDetail[] calldata conversionDetails, address _feeAddress ) public { @@ -259,7 +261,7 @@ contract BatchConversionPayments is BatchPaymentsPublic { * the following error is thrown: "revert paymentProxy transferExactEthWithReferenceAndFee failed" * This choice reduces the gas significantly, by delegating the whole conversion to the payment proxy. */ - function batchEthConversionPaymentsWithReference( + function batchEthConversionPayments( ConversionDetail[] calldata conversionDetails, address payable _feeAddress ) public payable { diff --git a/packages/smart-contracts/src/contracts/BatchPaymentsPublic.sol b/packages/smart-contracts/src/contracts/BatchNoConversionPayments.sol similarity index 96% rename from packages/smart-contracts/src/contracts/BatchPaymentsPublic.sol rename to packages/smart-contracts/src/contracts/BatchNoConversionPayments.sol index a3b78906a..343b4d7b6 100644 --- a/packages/smart-contracts/src/contracts/BatchPaymentsPublic.sol +++ b/packages/smart-contracts/src/contracts/BatchNoConversionPayments.sol @@ -8,7 +8,7 @@ import './interfaces/ERC20FeeProxy.sol'; import './interfaces/EthereumFeeProxy.sol'; /** - * @title BatchPaymentsPublic + * @title BatchNoConversionPayments * @notice This contract makes multiple payments with references, in one transaction: * - on: ERC20 Payment Proxy and ETH Payment Proxy of the Request Network protocol * - to: multiple addresses @@ -18,9 +18,9 @@ import './interfaces/EthereumFeeProxy.sol'; * @dev It is a clone of BatchPayment.sol, with three main modifications: * - function "receive" has one other condition: payerAuthorized * - fees are now divided by 10_000 instead of 1_000 in previous version - * - batch payment functions are now public, instead of external + * - batch payment functions have new names and are now public, instead of external */ -contract BatchPaymentsPublic is Ownable { +contract BatchNoConversionPayments is Ownable { using SafeERC20 for IERC20; IERC20FeeProxy public paymentErc20Proxy; @@ -33,6 +33,9 @@ contract BatchPaymentsPublic is Ownable { // payerAuthorized is set to true only when needed for batch Eth conversion bool internal payerAuthorized; + // transferBackRemainingEth is set to false only if the payer use batchRouter and call both batchEthPayments and batchConversionEthPaymentsWithReference + bool internal transferBackRemainingEth = true; + struct Token { address tokenAddress; uint256 amountAndFee; @@ -74,7 +77,7 @@ contract BatchPaymentsPublic is Ownable { * @dev It uses EthereumFeeProxy to pay an invoice and fees with a payment reference. * Make sure: msg.value >= sum(_amouts)+sum(_feeAmounts)+sumBatchFeeAmount */ - function batchEthPaymentsWithReference( + function batchEthPayments( address[] calldata _recipients, uint256[] calldata _amounts, bytes[] calldata _paymentReferences, @@ -112,7 +115,7 @@ contract BatchPaymentsPublic is Ownable { _feeAddress.transfer(amount); // Batch contract transfers the remaining ethers to the payer - if (address(this).balance > 0) { + if (transferBackRemainingEth && address(this).balance > 0) { (bool sendBackSuccess, ) = payable(msg.sender).call{value: address(this).balance}(''); require(sendBackSuccess, 'Could not send remaining funds to the payer'); } @@ -130,7 +133,7 @@ contract BatchPaymentsPublic is Ownable { * Make sure this contract has enough allowance to spend the payer's token. * Make sure the payer has enough tokens to pay the amount, the fee, and the batch fee. */ - function batchERC20PaymentsWithReference( + function batchERC20Payments( address _tokenAddress, address[] calldata _recipients, uint256[] calldata _amounts, @@ -206,7 +209,7 @@ contract BatchPaymentsPublic is Ownable { * Make sure this contract has enough allowance to spend the payer's token. * Make sure the payer has enough tokens to pay the amount, the fee, and the batch fee. */ - function batchERC20PaymentsMultiTokensWithReference( + function batchMultiERC20Payments( address[] calldata _tokenAddresses, address[] calldata _recipients, uint256[] calldata _amounts, diff --git a/packages/smart-contracts/src/lib/artifacts/BatchConversionPayments/0.1.0.json b/packages/smart-contracts/src/lib/artifacts/BatchConversionPayments/0.1.0.json index 33a9211dd..dc2dbe046 100644 --- a/packages/smart-contracts/src/lib/artifacts/BatchConversionPayments/0.1.0.json +++ b/packages/smart-contracts/src/lib/artifacts/BatchConversionPayments/0.1.0.json @@ -63,6 +63,44 @@ "stateMutability": "view", "type": "function" }, + { + "inputs": [ + { + "internalType": "address", + "name": "_tokenAddress", + "type": "address" + }, + { + "internalType": "address[]", + "name": "_recipients", + "type": "address[]" + }, + { + "internalType": "uint256[]", + "name": "_amounts", + "type": "uint256[]" + }, + { + "internalType": "bytes[]", + "name": "_paymentReferences", + "type": "bytes[]" + }, + { + "internalType": "uint256[]", + "name": "_feeAmounts", + "type": "uint256[]" + }, + { + "internalType": "address", + "name": "_feeAddress", + "type": "address" + } + ], + "name": "batchERC20Payments", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, { "inputs": [ { @@ -108,23 +146,18 @@ "type": "tuple[]" }, { - "internalType": "address", + "internalType": "address payable", "name": "_feeAddress", "type": "address" } ], - "name": "batchERC20ConversionPaymentsMultiTokens", + "name": "batchEthConversionPayments", "outputs": [], - "stateMutability": "nonpayable", + "stateMutability": "payable", "type": "function" }, { "inputs": [ - { - "internalType": "address[]", - "name": "_tokenAddresses", - "type": "address[]" - }, { "internalType": "address[]", "name": "_recipients", @@ -146,52 +179,27 @@ "type": "uint256[]" }, { - "internalType": "address", + "internalType": "address payable", "name": "_feeAddress", "type": "address" } ], - "name": "batchERC20PaymentsMultiTokensWithReference", + "name": "batchEthPayments", "outputs": [], - "stateMutability": "nonpayable", + "stateMutability": "payable", "type": "function" }, { - "inputs": [ - { - "internalType": "address", - "name": "_tokenAddress", - "type": "address" - }, - { - "internalType": "address[]", - "name": "_recipients", - "type": "address[]" - }, - { - "internalType": "uint256[]", - "name": "_amounts", - "type": "uint256[]" - }, - { - "internalType": "bytes[]", - "name": "_paymentReferences", - "type": "bytes[]" - }, - { - "internalType": "uint256[]", - "name": "_feeAmounts", - "type": "uint256[]" - }, + "inputs": [], + "name": "batchFee", + "outputs": [ { - "internalType": "address", - "name": "_feeAddress", - "type": "address" + "internalType": "uint256", + "name": "", + "type": "uint256" } ], - "name": "batchERC20PaymentsWithReference", - "outputs": [], - "stateMutability": "nonpayable", + "stateMutability": "view", "type": "function" }, { @@ -239,18 +247,23 @@ "type": "tuple[]" }, { - "internalType": "address payable", + "internalType": "address", "name": "_feeAddress", "type": "address" } ], - "name": "batchEthConversionPaymentsWithReference", + "name": "batchMultiERC20ConversionPayments", "outputs": [], - "stateMutability": "payable", + "stateMutability": "nonpayable", "type": "function" }, { "inputs": [ + { + "internalType": "address[]", + "name": "_tokenAddresses", + "type": "address[]" + }, { "internalType": "address[]", "name": "_recipients", @@ -272,27 +285,14 @@ "type": "uint256[]" }, { - "internalType": "address payable", + "internalType": "address", "name": "_feeAddress", "type": "address" } ], - "name": "batchEthPaymentsWithReference", + "name": "batchMultiERC20Payments", "outputs": [], - "stateMutability": "payable", - "type": "function" - }, - { - "inputs": [], - "name": "batchFee", - "outputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "stateMutability": "view", + "stateMutability": "nonpayable", "type": "function" }, { diff --git a/packages/smart-contracts/src/lib/artifacts/BatchConversionPayments/index.ts b/packages/smart-contracts/src/lib/artifacts/BatchConversionPayments/index.ts index d83849d90..aed8e0166 100644 --- a/packages/smart-contracts/src/lib/artifacts/BatchConversionPayments/index.ts +++ b/packages/smart-contracts/src/lib/artifacts/BatchConversionPayments/index.ts @@ -10,7 +10,7 @@ export const batchConversionPaymentsArtifact = new ContractArtifact( + { + '0.1.0': { + abi: ABI_0_1_0, + deployment: { + private: { + address: '0x1411CB266FCEd1587b0AA29E9d5a9Ef3Db64A9C5', + 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 e807a3e58..9a6987fa8 100644 --- a/packages/smart-contracts/src/lib/artifacts/index.ts +++ b/packages/smart-contracts/src/lib/artifacts/index.ts @@ -12,6 +12,7 @@ export * from './EthereumFeeProxy'; export * from './EthConversionProxy'; export * from './ERC20EscrowToPay'; export * from './BatchPayments'; +export * from './BatchNoConversionPayments'; export * from './BatchConversionPayments'; /** * Request Storage diff --git a/packages/smart-contracts/test/contracts/BatchConversionErc20Payments.test.ts b/packages/smart-contracts/test/contracts/BatchConversionErc20Payments.test.ts deleted file mode 100644 index eaa928141..000000000 --- a/packages/smart-contracts/test/contracts/BatchConversionErc20Payments.test.ts +++ /dev/null @@ -1,817 +0,0 @@ -import { ethers, network } from 'hardhat'; -import { - ERC20FeeProxy__factory, - Erc20ConversionProxy__factory, - EthConversionProxy__factory, - EthereumFeeProxy__factory, - ChainlinkConversionPath, - TestERC20, - Erc20ConversionProxy, - EthConversionProxy, - TestERC20__factory, - BatchConversionPayments__factory, - BatchConversionPayments, -} from '../../src/types'; -import { BigNumber, BigNumberish, BytesLike, ContractTransaction, Signer } from 'ethers'; -import { expect } from 'chai'; -import { CurrencyManager } from '@requestnetwork/currency'; -import { chainlinkConversionPath } from '../../src/lib'; -import { localERC20AlphaArtifact, secondLocalERC20AlphaArtifact } from './localArtifacts'; -import Utils from '@requestnetwork/utils'; -import { HttpNetworkConfig } from 'hardhat/types'; - -describe('contract: BatchConversionPayments', async () => { - const networkConfig = network.config as HttpNetworkConfig; - const provider = new ethers.providers.JsonRpcProvider(networkConfig.url); - - let from: string; - let to: string; - let feeAddress: string; - let adminSigner: Signer; - let signer1: Signer; - let signer4: Signer; - - let tx: ContractTransaction; - - // constants used to set up batch conversion proxy, and also requests payment - const batchFee = 50; - const batchConvFee = 100; - const amountInFiat = '100000000'; // 1 with 8 decimal - const feesAmountInFiat = '100000'; // 0.001 with 8 decimal - const thousandWith18Decimal = '1000000000000000000000'; - const referenceExample = '0xaaaa'; - - // constants and variables to set up proxies and paths - 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 DAI_address: string; - let FAU_address: string; - - let erc20ConversionProxy: Erc20ConversionProxy; - let ethConversionProxy: EthConversionProxy; - let testBatchConversionProxy: BatchConversionPayments; - let testERC20: TestERC20; - let testERC20b: TestERC20; - let chainlinkPath: ChainlinkConversionPath; - - // variables used to check testERC20 balances - let fromOldBalance: BigNumber; - let toOldBalance: BigNumber; - let feeOldBalance: BigNumber; - - let fromDiffBalanceExpected: BigNumber; - let toDiffBalanceExpected: BigNumber; - let feeDiffBalanceExpected: BigNumber; - - let beforeEthBalanceTo: BigNumber; - let beforeEthBalanceFee: BigNumber; - let beforeEthBalance: BigNumber; - - let amountToPayExpected: BigNumber; - let feeToPayExpected: BigNumber; - - // amount and feeAmount are usually in fiat for conversion inputs, else in ETH - const amount = BigNumber.from(100000); - const feeAmount = amount.mul(10).div(10000); - - // variables needed for chainlink and conversion payments - let conversionToPay: BigNumber; - let conversionFees: BigNumber; - // TODO check coherence - let usdConversionToPay: BigNumber; - let usdConversionFee: BigNumber; - - // type required by Erc20 conversion batch function inputs - type ConversionDetail = { - recipient: string; - requestAmount: BigNumberish; - path: string[]; - paymentReference: BytesLike; - feeAmount: BigNumberish; - maxToSpend: BigNumberish; - maxRateTimespan: BigNumberish; - }; - let convDetail: ConversionDetail; - let inputs: Array; - - const emptyCryptoDetails = { - tokenAddresses: [], - recipients: [], - amounts: [], - paymentReferences: [], - feeAmounts: [], - }; - - /** - * @notice Function batch conversion, it can be the batchRouter function, - * used with conversion args, or directly batchERC20ConversionPaymentsMultiTokens - */ - let batchConvFunction: ( - args: any, - feeAddress: string, - optional?: any, - ) => Promise; - - /** - * @notice it sets the conversions including fees to be paid, and it set the convDetail input - */ - const setConvToPayAndConvDetail = async ( - _recipient: string, - _path: string[], - _requestAmount: string, - _feeAmount: string, - _maxRateTimespan: number, - _chainlinkPath: ChainlinkConversionPath, - ) => { - conversionToPay = (await _chainlinkPath.getConversion(_requestAmount, _path)).result; - conversionFees = (await _chainlinkPath.getConversion(_feeAmount, _path)).result; - convDetail = { - recipient: _recipient, - requestAmount: _requestAmount, - path: _path, - paymentReference: referenceExample, - feeAmount: _feeAmount, - maxToSpend: conversionToPay.add(conversionFees).toString(), - maxRateTimespan: _maxRateTimespan, - }; - }; - - /** - * check testERC20 balances of: the payer (from), the recipient (to), the feeAddress, and the batch contract - */ - const checkBalancesForOneToken = async ( - _testERC20: TestERC20, - _fromOldBalance: BigNumber, - _toOldBalance: BigNumber, - _feeOldBalance: BigNumber, - _fromDiffBalanceExpected: BigNumber, - _toDiffBalanceExpected: BigNumber, - _feeDiffBalanceExpected: BigNumber, - ) => { - // Get balances - const fromBalance = await _testERC20.balanceOf(from); - const toBalance = await _testERC20.balanceOf(to); - const feeBalance = await _testERC20.balanceOf(feeAddress); - const batchBalance = await _testERC20.balanceOf(testBatchConversionProxy.address); - - // Calculate the difference of the balance : now - before - const fromDiffBalance = BigNumber.from(fromBalance).sub(_fromOldBalance); - const toDiffBalance = BigNumber.from(toBalance).sub(_toOldBalance); - const feeDiffBalance = BigNumber.from(feeBalance).sub(_feeOldBalance); - // Check balance changes - expect(fromDiffBalance).to.equals(_fromDiffBalanceExpected, 'fromDiffBalance'); - expect(toDiffBalance).to.equals(_toDiffBalanceExpected, 'toDiffBalance'); - expect(feeDiffBalance).to.equals(_feeDiffBalanceExpected, 'feeDiffBalance'); - expect(batchBalance).to.equals('0', 'batchBalance'); - }; - - /** - * @notice Used to calculate the expected new ERC20 balance of a single token for batch conversion. - * @dev fees are not exactly calculated with the same formula, depending if it is with conversion or not - */ - const expectedERC20Balances = ( - _conversionToPay_results: BigNumber[], - _conversionFees_results: BigNumber[], - appliedFees: number, - withConversion = true, - ) => { - let _fromDiffBalanceExpected = _conversionToPay_results.reduce( - (prev, x) => prev.sub(x), - BigNumber.from(0), - ); - let _toDiffBalanceExpected = _fromDiffBalanceExpected.mul(-1); - let _feeDiffBalanceExpected = _conversionFees_results.reduce( - (prev, x) => prev.add(x), - BigNumber.from(0), - ); - - _feeDiffBalanceExpected = withConversion - ? _toDiffBalanceExpected - .add(_feeDiffBalanceExpected) - .mul(appliedFees) - .div(10000) - .add(_feeDiffBalanceExpected) - : _toDiffBalanceExpected.mul(appliedFees).div(10000).add(_feeDiffBalanceExpected); - - _fromDiffBalanceExpected = _fromDiffBalanceExpected.sub(_feeDiffBalanceExpected); - return [_fromDiffBalanceExpected, _toDiffBalanceExpected, _feeDiffBalanceExpected]; - }; - - /** - * @notice update convDetail, do an ERC20 conversion batch payment with a single payment inside and calculate the balances - * @param path to update the convDetail - */ - const onePaymentBatchConv = async (path: string[]) => { - await setConvToPayAndConvDetail(to, path, amountInFiat, feesAmountInFiat, 0, chainlinkPath); - await batchConvFunction([convDetail], feeAddress); - [fromDiffBalanceExpected, toDiffBalanceExpected, feeDiffBalanceExpected] = - expectedERC20Balances([conversionToPay], [conversionFees], batchConvFee); - }; - - /** - * @notice generate nTimes 2 convDetails, do an ERC20 conv batch payment with theses 2*nTimes payments - * and calculate the balances - * @param path2 to update the second convDetail - */ - const manyPaymentsBatchConv = async ( - path1: string[], - path2: string[], - withBatchRouter = false, - ) => { - await setConvToPayAndConvDetail(to, path1, amountInFiat, feesAmountInFiat, 0, chainlinkPath); - // define a second payment request - const conversionToPay2 = (await chainlinkPath.getConversion(amountInFiat, path2)).result; - const conversionFees2 = (await chainlinkPath.getConversion(feesAmountInFiat, path2)).result; - const convDetail2 = Utils.deepCopy(convDetail); - convDetail2.path = path2; - convDetail2.maxToSpend = conversionToPay2.add(conversionFees2).toString(); - - // define conversionsToPays & conversionsFees to calculate the expected balances - const conversionsToPays = [conversionToPay, conversionToPay, conversionToPay2]; - const conversionsFees = [conversionFees, conversionFees, conversionFees2]; - - // get balances of the 2nd token, useful when there are 2 different tokens used - const fromOldBalance2 = await testERC20b.balanceOf(from); - const toOldBalance2 = await testERC20b.balanceOf(to); - const feeOldBalance2 = await testERC20b.balanceOf(feeAddress); - - if (withBatchRouter) { - await batchConvFunction( - [ - { - paymentNetworkId: '0', - conversionDetails: [convDetail, convDetail, convDetail2], - cryptoDetails: emptyCryptoDetails, - }, - ], - feeAddress, - ); - } else { - await batchConvFunction([convDetail, convDetail, convDetail2], feeAddress); - } - - // 1st token: testERC20 - calculate the expected balances - [fromDiffBalanceExpected, toDiffBalanceExpected, feeDiffBalanceExpected] = - expectedERC20Balances( - conversionsToPays.slice(0, 2), - conversionsFees.slice(0, 2), - batchConvFee, - ); - - // 2nd token: testERC20b - calculate the expected balances - const [fromDiffBalanceExpected2, toDiffBalanceExpected2, feeDiffBalanceExpected2] = - expectedERC20Balances( - conversionsToPays.slice(2, 3), - conversionsFees.slice(2, 3), - batchConvFee, - ); - - // check the balance of 2nd token, which is not checked in "afterEach" as 1st token. - checkBalancesForOneToken( - testERC20b, - fromOldBalance2, - toOldBalance2, - feeOldBalance2, - fromDiffBalanceExpected2, - toDiffBalanceExpected2, - feeDiffBalanceExpected2, - ); - }; - - /** - * @notice Use to test one batch payment execution for a given ERC20 batch function (no conversion). - * It tests the ERC20 transfer and fee proxy `TransferWithReferenceAndFee` events - * @param useBatchRouter allows to use a function through the batchRouter or not - * @param erc20Function selects the batch function name tested: "batchERC20PaymentsWithReference" - * or "batchERC20PaymentsMultiTokensWithReference" - */ - const batchERC20Payments = async (useBatchRouter: boolean, erc20Function: string) => { - // set up main variables - const tokenAddress = testERC20.address; - - // Select the batch function and pay - let batchFunction: Function; - if (useBatchRouter) { - batchFunction = testBatchConversionProxy.batchRouter; - await batchFunction( - [ - { - paymentNetworkId: erc20Function === 'batchERC20PaymentsWithReference' ? 1 : 2, - conversionDetails: [], - cryptoDetails: { - tokenAddresses: [tokenAddress], - recipients: [to], - amounts: [amount], - paymentReferences: [referenceExample], - feeAmounts: [feeAmount], - }, - }, - ], - feeAddress, - ); - } else { - batchFunction = - erc20Function === 'batchERC20PaymentsWithReference' - ? testBatchConversionProxy.batchERC20PaymentsWithReference - : testBatchConversionProxy.batchERC20PaymentsMultiTokensWithReference; - await batchFunction( - erc20Function === 'batchERC20PaymentsWithReference' ? tokenAddress : [tokenAddress], - [to], - [amount], - [referenceExample], - [feeAmount], - feeAddress, - ); - } - - [fromDiffBalanceExpected, toDiffBalanceExpected, feeDiffBalanceExpected] = - expectedERC20Balances([BigNumber.from(amount)], [BigNumber.from(feeAmount)], batchFee, false); - }; - - /** - * @notice it modify the Eth batch conversion inputs if needed, depending it is - * directly or through batchRouter - * @param useBatchRouter - * @param inputs a list of convDetail - */ - const getEthConvInputs = (useBatchRouter: boolean, inputs: Array) => { - if (useBatchRouter) { - return [ - { - paymentNetworkId: '3', - conversionDetails: inputs, - cryptoDetails: { - tokenAddresses: [], - recipients: [], - amounts: [], - paymentReferences: [], - feeAmounts: [], - }, // cryptoDetails is not used - }, - ]; - } - return inputs; - }; - - const checkEthBalances = async (amountToPayExpected: BigNumber, feeToPayExpected: BigNumber) => { - const receipt = await tx.wait(); - const gasUsed = receipt.gasUsed.mul(2 * 10 ** 10); - - const afterEthBalance = await provider.getBalance(await signer1.getAddress()); - const afterEthBalanceTo = await provider.getBalance(to); - const afterEthBalanceFee = await provider.getBalance(feeAddress); - const proxyBalance = await provider.getBalance(testBatchConversionProxy.address); - - // Calculate the difference of the balance : now - before - const _diffBalance = beforeEthBalance.sub(afterEthBalance); - const _diffBalanceTo = afterEthBalanceTo.sub(beforeEthBalanceTo); - const _diffBalanceFee = afterEthBalanceFee.sub(beforeEthBalanceFee); - - // feeToPayExpected includes batch conversion fees now - feeToPayExpected = amountToPayExpected - .add(feeToPayExpected) - .mul(batchConvFee) - .div(10000) - .add(feeToPayExpected); - const _diffBalanceExpect = gasUsed.add(amountToPayExpected).add(feeToPayExpected); - - // Check balance changes - expect(_diffBalance).to.equals(_diffBalanceExpect, 'DiffBalance'); - expect(_diffBalanceTo).to.equals(amountToPayExpected, 'diffBalanceTo'); - expect(_diffBalanceFee).to.equals(feeToPayExpected, 'diffBalanceFee'); - expect(proxyBalance).to.equals('0', 'proxyBalance'); - }; - - before(async () => { - [, from, to, feeAddress] = (await ethers.getSigners()).map((s) => s.address); - [adminSigner, signer1, , , signer4] = await ethers.getSigners(); - - chainlinkPath = chainlinkConversionPath.connect(network.name, signer1); - - const erc20FeeProxy = await new ERC20FeeProxy__factory(adminSigner).deploy(); - const ethFeeProxy = await new EthereumFeeProxy__factory(adminSigner).deploy(); - erc20ConversionProxy = await new Erc20ConversionProxy__factory(adminSigner).deploy( - erc20FeeProxy.address, - chainlinkPath.address, - await adminSigner.getAddress(), - ); - ethConversionProxy = await new EthConversionProxy__factory(adminSigner).deploy( - ethFeeProxy.address, - chainlinkPath.address, - ETH_hash, - ); - - testBatchConversionProxy = await new BatchConversionPayments__factory(adminSigner).deploy( - erc20FeeProxy.address, - ethFeeProxy.address, - erc20ConversionProxy.address, - ethConversionProxy.address, - await adminSigner.getAddress(), - ); - - // set batch proxy fees - await testBatchConversionProxy.setBatchFee(batchFee); - await testBatchConversionProxy.setBatchConversionFee(batchConvFee); - testBatchConversionProxy = testBatchConversionProxy.connect(signer1); - - // set ERC20 tokens - DAI_address = localERC20AlphaArtifact.getAddress(network.name); - testERC20 = new TestERC20__factory(adminSigner).attach(DAI_address); - await testERC20.transfer(from, BigNumber.from(thousandWith18Decimal)); - testERC20 = TestERC20__factory.connect(testERC20.address, signer1); - - FAU_address = secondLocalERC20AlphaArtifact.getAddress(network.name); - testERC20b = new TestERC20__factory(adminSigner).attach(FAU_address); - await testERC20b.transfer(from, BigNumber.from(thousandWith18Decimal)); - testERC20b = TestERC20__factory.connect(testERC20b.address, signer1); - }); - describe('ERC20', async () => { - beforeEach(async () => { - fromDiffBalanceExpected = BigNumber.from(0); - toDiffBalanceExpected = BigNumber.from(0); - feeDiffBalanceExpected = BigNumber.from(0); - await testERC20.approve(testBatchConversionProxy.address, thousandWith18Decimal, { - from, - }); - await testERC20b.approve(testBatchConversionProxy.address, thousandWith18Decimal, { - from, - }); - // get balances of testERC20 token - fromOldBalance = await testERC20.balanceOf(from); - toOldBalance = await testERC20.balanceOf(to); - feeOldBalance = await testERC20.balanceOf(feeAddress); - - // create a default convDetail - setConvToPayAndConvDetail( - to, - [EUR_hash, USD_hash, DAI_address], - amountInFiat, - feesAmountInFiat, - 0, - chainlinkPath, - ); - }); - - afterEach(async () => { - // check balances of testERC20 token - checkBalancesForOneToken( - testERC20, - fromOldBalance, - toOldBalance, - feeOldBalance, - fromDiffBalanceExpected, - toDiffBalanceExpected, - feeDiffBalanceExpected, - ); - }); - - describe('batchERC20ConversionPaymentsMultiTokens', async () => { - it('make 1 payment with 1-step conversion', async () => { - batchConvFunction = testBatchConversionProxy.batchERC20ConversionPaymentsMultiTokens; - await onePaymentBatchConv([USD_hash, DAI_address]); - }); - it('make 1 payment with 2-steps conversion', async () => { - batchConvFunction = testBatchConversionProxy.batchERC20ConversionPaymentsMultiTokens; - await onePaymentBatchConv([EUR_hash, USD_hash, DAI_address]); - }); - it('make 3 payment with different tokens and conversion length', async () => { - batchConvFunction = testBatchConversionProxy.batchERC20ConversionPaymentsMultiTokens; - await manyPaymentsBatchConv([EUR_hash, USD_hash, DAI_address], [USD_hash, FAU_address]); - }); - }); - - describe('batchERC20ConversionPaymentsMultiTokens errors', async () => { - before(async () => { - batchConvFunction = testBatchConversionProxy.batchERC20ConversionPaymentsMultiTokens; - }); - it('cannot transfer with invalid path', async function () { - convDetail.path = [EUR_hash, ETH_hash, DAI_address]; - await expect(batchConvFunction([convDetail], feeAddress)).to.be.revertedWith( - 'revert No aggregator found', - ); - }); - - it('cannot transfer if max to spend too low', async function () { - convDetail.maxToSpend = conversionToPay.add(conversionFees).sub(1).toString(); - await expect(batchConvFunction([convDetail], feeAddress)).to.be.revertedWith( - 'Amount to pay is over the user limit', - ); - }); - - it('cannot transfer if rate is too old', async function () { - convDetail.maxRateTimespan = 10; - await expect(batchConvFunction([convDetail], feeAddress)).to.be.revertedWith( - 'aggregator rate is outdated', - ); - }); - - it('Not enough allowance', async function () { - // reduce signer1 allowance - await testERC20.approve( - testBatchConversionProxy.address, - BigNumber.from(convDetail.maxToSpend).sub(2), - { - from, - }, - ); - await expect(batchConvFunction([convDetail], feeAddress)).to.be.revertedWith( - 'Insufficient allowance for batch to pay', - ); - }); - - it('Not enough funds even if partially enough funds', async function () { - // signer1 transfer enough token to pay just 1 invoice to signer4 - await testERC20 - .connect(signer1) - .transfer(await signer4.getAddress(), BigNumber.from(convDetail.maxToSpend)); - // increase signer4 allowance - await testERC20 - .connect(signer4) - .approve(testBatchConversionProxy.address, thousandWith18Decimal); - - batchConvFunction = - testBatchConversionProxy.connect(signer4).batchERC20ConversionPaymentsMultiTokens; - - // 3 invoices to pay - await expect( - batchConvFunction([convDetail, convDetail, convDetail], feeAddress), - ).to.be.revertedWith('not enough funds, including fees'); - - // signer4 transfer token to signer1 - await testERC20 - .connect(signer4) - .transfer(from, await testERC20.balanceOf(await signer4.getAddress())); - testERC20.connect(adminSigner); - testBatchConversionProxy = testBatchConversionProxy.connect(signer1); - }); - }); - - describe('batchRouter', async () => { - it(`1 payment with no conversion`, async function () { - await batchERC20Payments(true, 'batchERC20PaymentsMultiTokensWithReference'); - }); - it('make 3 payment with different tokens and conversion length', async () => { - batchConvFunction = testBatchConversionProxy.batchRouter; - await manyPaymentsBatchConv( - [EUR_hash, USD_hash, DAI_address], - [USD_hash, FAU_address], - true, - ); - }); - - it('make n heterogeneous payments', async () => { - // set convDetail: done "beforeEach" - - // set cryptoDetails - const tokenAddress = testERC20.address; - const cryptoDetails = { - tokenAddresses: [tokenAddress], - recipients: [to], - amounts: [amount], - paymentReferences: [referenceExample], - feeAmounts: [feeAmount], - }; - - testBatchConversionProxy.batchRouter( - [ - { - paymentNetworkId: 0, - conversionDetails: [convDetail], - cryptoDetails: emptyCryptoDetails, - }, - { - paymentNetworkId: 2, - conversionDetails: [], - cryptoDetails: cryptoDetails, - }, - ], - feeAddress, - ); - - const [ - conversionFromDiffBalanceExpected, - conversionToDiffBalanceExpected, - conversionFeeDiffBalanceExpected, - ] = expectedERC20Balances([conversionToPay], [conversionFees], batchConvFee); - - const [ - noConversionFromDiffBalanceExpected, - noConversionToDiffBalanceExpected, - noConversionFeeDiffBalanceExpected, - ] = expectedERC20Balances( - [BigNumber.from(amount)], - [BigNumber.from(feeAmount)], - batchFee, - false, - ); - - fromDiffBalanceExpected = conversionFromDiffBalanceExpected.add( - noConversionFromDiffBalanceExpected, - ); - toDiffBalanceExpected = conversionToDiffBalanceExpected.add( - noConversionToDiffBalanceExpected, - ); - feeDiffBalanceExpected = conversionFeeDiffBalanceExpected.add( - noConversionFeeDiffBalanceExpected, - ); - }); - }); - - /** Make sure the existing ERC20 functions from the parent contract BatchPaymentPublic.sol are still working */ - describe('Functions herited from contract BatchErc20Payments ', () => { - it(`batchERC20PaymentsWithReference 1 payment`, async function () { - await batchERC20Payments(false, 'batchERC20PaymentsWithReference'); - }); - - it(`batchERC20PaymentsMultiTokensWithReference 1 payment`, async function () { - await batchERC20Payments(false, 'batchERC20PaymentsMultiTokensWithReference'); - }); - }); - }); - - for (const useBatchRouter of [true, false]) { - describe(`Test ETH batch functions ${ - useBatchRouter ? 'through batchRouter' : 'without batchRouter' - }`, () => { - before(async () => { - convDetail = { - recipient: to, - requestAmount: amount, - path: [USD_hash, ETH_hash], - paymentReference: referenceExample, - feeAmount: feeAmount, - maxToSpend: BigNumber.from(0), - maxRateTimespan: BigNumber.from(0), - }; - - // basic setup: 1 payment - usdConversionToPay = ( - await chainlinkPath.getConversion(convDetail.requestAmount, convDetail.path) - ).result; - usdConversionFee = ( - await chainlinkPath.getConversion(convDetail.feeAmount, convDetail.path) - ).result; - - if (useBatchRouter) { - batchConvFunction = testBatchConversionProxy.batchRouter; - } else { - batchConvFunction = testBatchConversionProxy.batchEthConversionPaymentsWithReference; - } - }); - - beforeEach(async () => { - beforeEthBalanceTo = await provider.getBalance(to); - beforeEthBalanceFee = await provider.getBalance(feeAddress); - beforeEthBalance = await provider.getBalance(await signer1.getAddress()); - - // expected balances, it can be modified for each test - amountToPayExpected = usdConversionToPay; - // fees does not include batch fees yet - feeToPayExpected = usdConversionFee; - }); - - describe('success functions', () => { - it('batchEthConversionPaymentsWithReference transfer 1 payment in ethers denominated in USD', async function () { - inputs = [convDetail]; - tx = await batchConvFunction(getEthConvInputs(useBatchRouter, inputs), feeAddress, { - value: BigNumber.from('100000000000000000'), - }); - await checkEthBalances(amountToPayExpected, feeToPayExpected); - }); - - it('batchEthConversionPaymentsWithReference transfer 3 payment in ethers denominated in USD', async function () { - amountToPayExpected = amountToPayExpected.mul(3); - feeToPayExpected = feeToPayExpected.mul(3); - inputs = [convDetail, convDetail, convDetail]; - tx = await batchConvFunction(getEthConvInputs(useBatchRouter, inputs), feeAddress, { - value: BigNumber.from('100000000000000000'), - }); - await checkEthBalances(amountToPayExpected, feeToPayExpected); - }); - - it('batchEthConversionPaymentsWithReference transfer 3 payments in ethers denominated in USD and EUR', async function () { - const EurConvDetail = Utils.deepCopy(convDetail); - EurConvDetail.path = [EUR_hash, USD_hash, ETH_hash]; - - const eurConversionToPay = await chainlinkPath.getConversion( - EurConvDetail.requestAmount, - EurConvDetail.path, - ); - const eurFeesToPay = await chainlinkPath.getConversion( - EurConvDetail.feeAmount, - EurConvDetail.path, - ); - - amountToPayExpected = eurConversionToPay.result.add(amountToPayExpected.mul(2)); - feeToPayExpected = eurFeesToPay.result.add(feeToPayExpected.mul(2)); - inputs = [convDetail, EurConvDetail, convDetail]; - - tx = await batchConvFunction(getEthConvInputs(useBatchRouter, inputs), feeAddress, { - value: BigNumber.from('100000000000000000'), - }); - await checkEthBalances(amountToPayExpected, feeToPayExpected); - }); - - it('batchEthPaymentsWithReference transfer 1 payment', async function () { - beforeEthBalanceTo = await provider.getBalance(to); - beforeEthBalanceFee = await provider.getBalance(feeAddress); - beforeEthBalance = await provider.getBalance(await signer1.getAddress()); - - const cryptoDetails = { - tokenAddresses: [], - recipients: [to], - amounts: [amount], // in ETH - paymentReferences: [referenceExample], - feeAmounts: [feeAmount], // in ETH - }; - if (useBatchRouter) { - await testBatchConversionProxy.batchRouter( - [ - { - paymentNetworkId: 4, - conversionDetails: [convDetail], // not used - cryptoDetails: cryptoDetails, - }, - ], - feeAddress, - { value: 1000000000 }, - ); - } else { - await testBatchConversionProxy.batchEthPaymentsWithReference( - cryptoDetails.recipients, - cryptoDetails.amounts, - cryptoDetails.paymentReferences, - cryptoDetails.feeAmounts, - feeAddress, - { value: 1000000000 }, - ); - } - - amountToPayExpected = amount; - feeToPayExpected = feeAmount; - const afterEthBalanceTo = await provider.getBalance(to); - const afterEthBalanceFee = await provider.getBalance(feeAddress); - const proxyBalance = await provider.getBalance(testBatchConversionProxy.address); - const _diffBalanceTo = afterEthBalanceTo.sub(beforeEthBalanceTo); - const _diffBalanceFee = afterEthBalanceFee.sub(beforeEthBalanceFee); - - expect(_diffBalanceTo).to.equals(amountToPayExpected.toString(), 'diffBalanceTo'); - - feeToPayExpected = amountToPayExpected.mul(batchFee).div(10000).add(feeToPayExpected); - expect(_diffBalanceFee.toString()).to.equals( - feeToPayExpected.toString(), - 'diffBalanceFee', - ); - expect(proxyBalance).to.equals('0', 'proxyBalance'); - }); - }); - describe('revert functions', () => { - it('batchEthConversionPaymentsWithReference transfer FAIL: not enough funds', async function () { - await expect( - batchConvFunction(getEthConvInputs(useBatchRouter, [convDetail]), feeAddress, { - value: 10000, - }), - ).to.be.revertedWith('paymentProxy transferExactEthWithReferenceAndFee failed'); - }); - it('batchEthPaymentsWithReference transfer FAIL: not enough funds', async function () { - const cryptoDetails = { - tokenAddresses: [], - recipients: [to], - amounts: [amount], - paymentReferences: [referenceExample], - feeAmounts: [feeAmount], - }; - - // it contains the function being just executed, and still processing - let batchEthPayments; - if (useBatchRouter) { - batchEthPayments = testBatchConversionProxy.batchRouter( - [ - { - paymentNetworkId: 4, - conversionDetails: [convDetail], // not used - cryptoDetails: cryptoDetails, - }, - ], - feeAddress, - { value: 10000 }, - ); - } else { - batchEthPayments = testBatchConversionProxy.batchEthPaymentsWithReference( - cryptoDetails.recipients, - cryptoDetails.amounts, - cryptoDetails.paymentReferences, - cryptoDetails.feeAmounts, - feeAddress, - { value: 10000 }, - ); - } - await expect(batchEthPayments).to.be.revertedWith('not enough funds'); - }); - }); - }); - } -}); diff --git a/packages/smart-contracts/test/contracts/BatchConversionErc20PaymentsOLD.test.ts b/packages/smart-contracts/test/contracts/BatchConversionErc20PaymentsOLD.test.ts deleted file mode 100644 index b75959c96..000000000 --- a/packages/smart-contracts/test/contracts/BatchConversionErc20PaymentsOLD.test.ts +++ /dev/null @@ -1,540 +0,0 @@ -import { ethers, network } from 'hardhat'; -import { - ERC20FeeProxy__factory, - Erc20ConversionProxy__factory, - EthConversionProxy__factory, - EthereumFeeProxy__factory, - ChainlinkConversionPath, - TestERC20, - Erc20ConversionProxy, - EthConversionProxy, - TestERC20__factory, - BatchConversionPayments__factory, - BatchConversionPayments, -} from '../../src/types'; -import { BigNumber, BigNumberish, BytesLike, ContractTransaction, Signer } from 'ethers'; -import { expect } from 'chai'; -import { CurrencyManager } from '@requestnetwork/currency'; -import { chainlinkConversionPath } from '../../src/lib'; -import { localERC20AlphaArtifact, secondLocalERC20AlphaArtifact } from './localArtifacts'; -import Utils from '@requestnetwork/utils'; - -describe('contract: BatchConversionPayments', async () => { - let from: string; - let to: string; - let feeAddress: string; - let signer1: Signer; - let signer4: Signer; - - // constants used to set up batch conversion proxy, and also requests payment - const batchFee = 50; - const batchConvFee = 100; - const amountInFiat = '100000000'; // 1 with 8 decimal - const feesAmountInFiat = '100000'; // 0.001 with 8 decimal - const thousandWith18Decimal = '1000000000000000000000'; - const referenceExample = '0xaaaa'; - - // constants and variables to set up proxies and paths - 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 DAI_address: string; - let FAU_address: string; - - let erc20ConversionProxy: Erc20ConversionProxy; - let ethConversionProxy: EthConversionProxy; - let testBatchConversionProxy: BatchConversionPayments; - let testERC20: TestERC20; - let testERC20b: TestERC20; - let chainlinkPath: ChainlinkConversionPath; - - // variables used to check testERC20 balances - let fromOldBalance: BigNumber; - let toOldBalance: BigNumber; - let feeOldBalance: BigNumber; - - let fromDiffBalanceExpected: BigNumber; - let toDiffBalanceExpected: BigNumber; - let feeDiffBalanceExpected: BigNumber; - - // variables needed for chainlink and conversion payments - let conversionToPay: BigNumber; - let conversionFees: BigNumber; - - // type required by Erc20 conversion batch function inputs - type ConversionDetail = { - recipient: string; - requestAmount: BigNumberish; - path: string[]; - paymentReference: BytesLike; - feeAmount: BigNumberish; - maxToSpend: BigNumberish; - maxRateTimespan: BigNumberish; - }; - let convDetail: ConversionDetail; - - /** - * @notice Function batch conversion, it can be the batchRouter function, - * used with conversion args, or directly batchERC20ConversionPaymentsMultiTokens - */ - let batchConvFunction: ( - args: any, - feeAddress: string, - optional?: any, - ) => Promise; - - /** - * @notice it gets the conversions including fees to be paid, and it set the convDetail input - */ - const getConvToPayAndConvDetail = async ( - _recipient: string, - _path: string[], - _requestAmount: string, - _feeAmount: string, - _maxRateTimespan: number, - _chainlinkPath: ChainlinkConversionPath, - ) => { - conversionToPay = (await _chainlinkPath.getConversion(_requestAmount, _path)).result; - conversionFees = (await _chainlinkPath.getConversion(_feeAmount, _path)).result; - convDetail = { - recipient: _recipient, - requestAmount: _requestAmount, - path: _path, - paymentReference: referenceExample, - feeAmount: _feeAmount, - maxToSpend: conversionToPay.add(conversionFees).toString(), - maxRateTimespan: _maxRateTimespan, - }; - }; - - /** - * check testERC20 balances of: the payer (from), the recipient (to), the feeAddress, and the batch contract - */ - const checkBalancesForOneToken = async ( - _testERC20: TestERC20, - _fromOldBalance: BigNumber, - _toOldBalance: BigNumber, - _feeOldBalance: BigNumber, - _fromDiffBalanceExpected: BigNumber, - _toDiffBalanceExpected: BigNumber, - _feeDiffBalanceExpected: BigNumber, - ) => { - // Get balances - const fromBalance = await _testERC20.balanceOf(from); - const toBalance = await _testERC20.balanceOf(to); - const feeBalance = await _testERC20.balanceOf(feeAddress); - const batchBalance = await _testERC20.balanceOf(testBatchConversionProxy.address); - - // Calculate the difference of the balance : now - before - const fromDiffBalance = BigNumber.from(fromBalance).sub(_fromOldBalance); - const toDiffBalance = BigNumber.from(toBalance).sub(_toOldBalance); - const feeDiffBalance = BigNumber.from(feeBalance).sub(_feeOldBalance); - // Check balance changes - expect(fromDiffBalance).to.equals(_fromDiffBalanceExpected, 'fromDiffBalance'); - expect(toDiffBalance).to.equals(_toDiffBalanceExpected, 'toDiffBalance'); - expect(feeDiffBalance).to.equals(_feeDiffBalanceExpected, 'feeDiffBalance'); - expect(batchBalance).to.equals('0', 'batchBalance'); - }; - - /** - * @notice Used to calculate the expected new ERC20 balance of a single token for batch conversion. - * @dev fees are not exactly calculated with the same formula, depending if it is with conversion or not - */ - const expectedERC20Balances = ( - _conversionToPay_results: BigNumber[], - _conversionFees_results: BigNumber[], - appliedFees: number, - withConversion = true, - ) => { - let _fromDiffBalanceExpected = _conversionToPay_results.reduce( - (prev, x) => prev.sub(x), - BigNumber.from(0), - ); - let _toDiffBalanceExpected = _fromDiffBalanceExpected.mul(-1); - let _feeDiffBalanceExpected = _conversionFees_results.reduce( - (prev, x) => prev.add(x), - BigNumber.from(0), - ); - - _feeDiffBalanceExpected = withConversion - ? _toDiffBalanceExpected - .add(_feeDiffBalanceExpected) - .mul(appliedFees) - .div(10000) - .add(_feeDiffBalanceExpected) - : _toDiffBalanceExpected.mul(appliedFees).div(10000).add(_feeDiffBalanceExpected); - - _fromDiffBalanceExpected = _fromDiffBalanceExpected.sub(_feeDiffBalanceExpected); - return [_fromDiffBalanceExpected, _toDiffBalanceExpected, _feeDiffBalanceExpected]; - }; - - /** - * It sets the right batch conversion function, with the associated arguments format - * @param useBatchRouter allows to use batchERC20ConversionPaymentsMultiTokens with batchRouter - * @param _signer - */ - const setBatchConvFunction = async (useBatchRouter: boolean, _signer: Signer) => { - batchConvFunction = ( - convDetails: ConversionDetail[], - feeAddress: string, - ): Promise => { - return useBatchRouter - ? testBatchConversionProxy.connect(_signer).batchRouter( - [ - { - paymentNetworkId: '0', - conversionDetails: convDetails, - cryptoDetails: { - tokenAddresses: [], - recipients: [], - amounts: [], - paymentReferences: [], - feeAmounts: [], - }, - }, - ], - feeAddress, - ) - : testBatchConversionProxy - .connect(_signer) - .batchERC20ConversionPaymentsMultiTokens(convDetails, feeAddress); - }; - }; - - /** - * @notice update convDetail, do an ERC20 conversion batch payment with a single payment inside and calculate the balances - * @param path to update the convDetail - */ - const onePaymentBatchConv = async (path: string[]) => { - await getConvToPayAndConvDetail(to, path, amountInFiat, feesAmountInFiat, 0, chainlinkPath); - - const result = batchConvFunction([convDetail], feeAddress); - await expect(result) - .to.emit(erc20ConversionProxy, 'TransferWithConversionAndReference') - .withArgs( - convDetail.requestAmount, - ethers.utils.getAddress(convDetail.path[0]), - ethers.utils.keccak256(referenceExample), - convDetail.feeAmount, - '0', - ) - .to.emit(erc20ConversionProxy, 'TransferWithReferenceAndFee') - .withArgs( - ethers.utils.getAddress(DAI_address), - ethers.utils.getAddress(convDetail.recipient), - conversionToPay, - ethers.utils.keccak256(referenceExample), - conversionFees, - feeAddress, - ); - - [fromDiffBalanceExpected, toDiffBalanceExpected, feeDiffBalanceExpected] = - expectedERC20Balances([conversionToPay], [conversionFees], batchConvFee); - }; - - /** - * @notice generate nTimes 2 convDetails, do an ERC20 conv batch payment with theses 2*nTimes payments - * and calculate the balances - * @param path2 to update the second convDetail - */ - const manyPaymentsBatchConv = async (path2: string[], nTimes: number) => { - // define a second payment request - const amountInFiat2 = BigNumber.from(amountInFiat).mul(2).toString(); - const feesAmountInFiat2 = BigNumber.from(feesAmountInFiat).mul(2).toString(); - - const conversionToPay2 = (await chainlinkPath.getConversion(amountInFiat2, path2)).result; - const conversionFees2 = (await chainlinkPath.getConversion(feesAmountInFiat2, path2)).result; - - let convDetail2 = Utils.deepCopy(convDetail); - - convDetail2.path = path2; - convDetail2.requestAmount = amountInFiat2; - convDetail2.feeAmount = feesAmountInFiat2; - convDetail2.maxToSpend = conversionToPay2.add(conversionFees2).toString(); - - // define the new arg convDetails for the function, - // and conversionsToPays & conversionsFees to calculate the expected balances - let convDetails: ConversionDetail[] = []; - let conversionsToPays: BigNumber[] = []; - let conversionsFees: BigNumber[] = []; - for (let i = 0; i < nTimes; i++) { - convDetails = convDetails.concat([convDetail, convDetail2]); - conversionsToPays = conversionsToPays.concat([conversionToPay, conversionToPay2]); - conversionsFees = conversionsFees.concat([conversionFees, conversionFees2]); - } - - // get balances of the 2nd token, useful when there are 2 different tokens used - const fromOldBalance2 = await testERC20b.balanceOf(from); - const toOldBalance2 = await testERC20b.balanceOf(to); - const feeOldBalance2 = await testERC20b.balanceOf(feeAddress); - - await batchConvFunction(convDetails, feeAddress); - - // 1st condition: every tokens (end of the paths) are identicals - if ( - convDetail.path[convDetail.path.length - 1] === convDetail2.path[convDetail2.path.length - 1] - ) { - [fromDiffBalanceExpected, toDiffBalanceExpected, feeDiffBalanceExpected] = - expectedERC20Balances(conversionsToPays, conversionsFees, batchConvFee); - } - // else: there are 2 different tokens used (end of the paths): testERC20 and testERC20b - else { - // calculate the expected balances of the 1st token: testERC20 - const conversionsToPayToken1 = conversionsToPays.filter((_, i) => i % 2 === 0); - const conversionsFeesToken1 = conversionsFees.filter((_, i) => i % 2 === 0); - [fromDiffBalanceExpected, toDiffBalanceExpected, feeDiffBalanceExpected] = - expectedERC20Balances(conversionsToPayToken1, conversionsFeesToken1, batchConvFee); - - // calculate the expected balances of the 2nd token: testERC20b - const conversionsToPayToken2 = conversionsToPays.filter((_, i) => i % 2 === 1); - const conversionsFeesToken2 = conversionsFees.filter((_, i) => i % 2 === 1); - const [fromDiffBalanceExpected2, toDiffBalanceExpected2, feeDiffBalanceExpected2] = - expectedERC20Balances(conversionsToPayToken2, conversionsFeesToken2, batchConvFee); - - // check the balance of testERC20b token, which is not checked in "afterEach" as testERC20 token. - checkBalancesForOneToken( - testERC20b, - fromOldBalance2, - toOldBalance2, - feeOldBalance2, - fromDiffBalanceExpected2, - toDiffBalanceExpected2, - feeDiffBalanceExpected2, - ); - } - }; - - /** - * @notice Use to test one batch payment execution for a given ERC20 batch function (no conversion). - * It tests the ERC20 transfer and fee proxy `TransferWithReferenceAndFee` events - * @param useBatchRouter allows to use a function through the batchRouter or not - * @param erc20Function selects the batch function name tested: "batchERC20PaymentsWithReference" - * or "batchERC20PaymentsMultiTokensWithReference" - */ - const batchERC20Payments = async (useBatchRouter: boolean, erc20Function: string) => { - // set up main variables - const amount = 200000; - const feeAmount = 3000; - const tokenAddress = testERC20.address; - - // Select the batch function and pay - let batchFunction: Function; - if (useBatchRouter) { - batchFunction = testBatchConversionProxy.batchRouter; - await batchFunction( - [ - { - paymentNetworkId: erc20Function === 'batchERC20PaymentsWithReference' ? 1 : 2, - conversionDetails: [], - cryptoDetails: { - tokenAddresses: [tokenAddress], - recipients: [to], - amounts: [amount], - paymentReferences: [referenceExample], - feeAmounts: [feeAmount], - }, - }, - ], - feeAddress, - ); - } else { - batchFunction = - erc20Function === 'batchERC20PaymentsWithReference' - ? testBatchConversionProxy.batchERC20PaymentsWithReference - : testBatchConversionProxy.batchERC20PaymentsMultiTokensWithReference; - await batchFunction( - erc20Function === 'batchERC20PaymentsWithReference' ? tokenAddress : [tokenAddress], - [to], - [amount], - [referenceExample], - [feeAmount], - feeAddress, - ); - } - - [fromDiffBalanceExpected, toDiffBalanceExpected, feeDiffBalanceExpected] = - expectedERC20Balances([BigNumber.from(amount)], [BigNumber.from(feeAmount)], batchFee, false); - }; - - before(async () => { - [, from, to, feeAddress] = (await ethers.getSigners()).map((s) => s.address); - let adminSigner: Signer; - [adminSigner, signer1, , , signer4] = await ethers.getSigners(); - - chainlinkPath = chainlinkConversionPath.connect(network.name, signer1); - - const erc20FeeProxy = await new ERC20FeeProxy__factory(adminSigner).deploy(); - const ethFeeProxy = await new EthereumFeeProxy__factory(adminSigner).deploy(); - erc20ConversionProxy = await new Erc20ConversionProxy__factory(adminSigner).deploy( - erc20FeeProxy.address, - chainlinkPath.address, - await adminSigner.getAddress(), - ); - ethConversionProxy = await new EthConversionProxy__factory(adminSigner).deploy( - ethFeeProxy.address, - chainlinkPath.address, - ETH_hash, - ); - - testBatchConversionProxy = await new BatchConversionPayments__factory(adminSigner).deploy( - erc20FeeProxy.address, - ethFeeProxy.address, - erc20ConversionProxy.address, - ethConversionProxy.address, - await adminSigner.getAddress(), - ); - - // set batch proxy fees - await testBatchConversionProxy.setBatchFee(batchFee); - await testBatchConversionProxy.setBatchConversionFee(batchConvFee); - testBatchConversionProxy = testBatchConversionProxy.connect(signer1); - - // set ERC20 tokens - DAI_address = localERC20AlphaArtifact.getAddress(network.name); - testERC20 = new TestERC20__factory(adminSigner).attach(DAI_address); - await testERC20.transfer(from, BigNumber.from(thousandWith18Decimal)); - testERC20 = TestERC20__factory.connect(testERC20.address, signer1); - - FAU_address = secondLocalERC20AlphaArtifact.getAddress(network.name); - testERC20b = new TestERC20__factory(adminSigner).attach(FAU_address); - await testERC20b.transfer(from, BigNumber.from(thousandWith18Decimal)); - testERC20b = TestERC20__factory.connect(testERC20b.address, signer1); - }); - - /** - * @notice it contains all the tests related to the ERC20 batch payment, and its context required - * @param useBatchRouter allows to use the function "batchERC20ConversionPaymentsMultiTokens" - * through the batchRouter or directly - */ - for (const useBatchRouter of [true, false]) { - beforeEach(async () => { - setBatchConvFunction(useBatchRouter, signer1); - - fromDiffBalanceExpected = BigNumber.from(0); - toDiffBalanceExpected = BigNumber.from(0); - feeDiffBalanceExpected = BigNumber.from(0); - await testERC20.approve(testBatchConversionProxy.address, thousandWith18Decimal, { - from, - }); - await testERC20b.approve(testBatchConversionProxy.address, thousandWith18Decimal, { - from, - }); - // get balances of testERC20 token - fromOldBalance = await testERC20.balanceOf(from); - toOldBalance = await testERC20.balanceOf(to); - feeOldBalance = await testERC20.balanceOf(feeAddress); - - // create a default convDetail - getConvToPayAndConvDetail( - to, - [USD_hash, DAI_address], - amountInFiat, - feesAmountInFiat, - 0, - chainlinkPath, - ); - }); - - afterEach(async () => { - // check balances of testERC20 token - checkBalancesForOneToken( - testERC20, - fromOldBalance, - toOldBalance, - feeOldBalance, - fromDiffBalanceExpected, - toDiffBalanceExpected, - feeDiffBalanceExpected, - ); - }); - - describe(useBatchRouter ? 'Through batchRouter' : 'Without batchRouter ', async () => { - describe('batchERC20ConversionPaymentsMultiTokens with DAI', async () => { - it('allows to transfer DAI tokens for USD payment', async () => { - await onePaymentBatchConv([USD_hash, DAI_address]); - }); - it('allows to transfer DAI tokens for EUR payment', async () => { - await onePaymentBatchConv([EUR_hash, USD_hash, DAI_address]); - }); - it('allows to transfer 2 transactions DAI tokens for USD payment', async function () { - await manyPaymentsBatchConv([USD_hash, DAI_address], 1); - }); - it('allows to transfer DAI tokens for EUR payment', async () => { - await onePaymentBatchConv([EUR_hash, USD_hash, DAI_address]); - }); - it('allows to transfer 2 transactions DAI tokens for USD and EUR payments', async function () { - await manyPaymentsBatchConv([EUR_hash, USD_hash, DAI_address], 1); - }); - it('allows to transfer two kinds of tokens for USD', async function () { - await manyPaymentsBatchConv([USD_hash, FAU_address], 1); - }); - }); - - describe('batchERC20ConversionPaymentsMultiTokens with errors', () => { - it('cannot transfer with invalid path', async function () { - convDetail.path = [EUR_hash, ETH_hash, DAI_address]; - await expect(batchConvFunction([convDetail], feeAddress)).to.be.revertedWith( - 'revert No aggregator found', - ); - }); - - it('cannot transfer if max to spend too low', async function () { - convDetail.maxToSpend = conversionToPay.add(conversionFees).sub(1).toString(); - await expect(batchConvFunction([convDetail], feeAddress)).to.be.revertedWith( - 'Amount to pay is over the user limit', - ); - }); - - it('cannot transfer if rate is too old', async function () { - convDetail.maxRateTimespan = 10; - - await expect(batchConvFunction([convDetail], feeAddress)).to.be.revertedWith( - 'aggregator rate is outdated', - ); - }); - - it('Not enough allowance', async function () { - // signer4 connect to the batch function - setBatchConvFunction(useBatchRouter, signer4); - await expect(batchConvFunction([convDetail], feeAddress)).to.be.revertedWith( - 'Insufficient allowance for batch to pay', - ); - // reset: signer1 connect to the batch function - setBatchConvFunction(useBatchRouter, signer1); - }); - - it('Not enough funds', async function () { - // increase signer4 allowance - await testERC20 - .connect(signer4) - .approve(testBatchConversionProxy.address, thousandWith18Decimal); - // signer4 connect to the batch function - setBatchConvFunction(useBatchRouter, signer4); - - await expect(batchConvFunction([convDetail], feeAddress)).to.be.revertedWith( - 'not enough funds, including fees', - ); - - // reset: decrease signer4 allowance and reconnect with signer1 - await testERC20.connect(signer4).approve(testBatchConversionProxy.address, '0'); - testERC20.connect(signer1); - // reset: signer1 connect to the batch function - setBatchConvFunction(useBatchRouter, signer1); - }); - }); - - /** Make sure the existing ERC20 functions from the parent contract BatchPaymentPublic.sol are still working */ - describe('Herited from contract BatchErc20Payments functions', () => { - it(`batchERC20PaymentsWithReference transfers token`, async function () { - await batchERC20Payments(useBatchRouter, 'batchERC20PaymentsWithReference'); - }); - - it(`batchERC20PaymentsMultiTokensWithReference transfers token`, async function () { - await batchERC20Payments(useBatchRouter, 'batchERC20PaymentsMultiTokensWithReference'); - }); - }); - }); - } -}); diff --git a/packages/smart-contracts/test/contracts/BatchConversionEthPayments.test.ts b/packages/smart-contracts/test/contracts/BatchConversionEthPayments.test.ts deleted file mode 100644 index c654f87ad..000000000 --- a/packages/smart-contracts/test/contracts/BatchConversionEthPayments.test.ts +++ /dev/null @@ -1,346 +0,0 @@ -import { ethers, network } from 'hardhat'; -import { - EthConversionProxy__factory, - EthereumFeeProxy__factory, - EthereumFeeProxy, - ChainlinkConversionPath, - EthConversionProxy, - BatchConversionPayments, -} from '../../src/types'; -import { BigNumber, BigNumberish, BytesLike, ContractTransaction, Signer } from 'ethers'; -import { expect } from 'chai'; -import { CurrencyManager } from '@requestnetwork/currency'; -import { chainlinkConversionPath, batchConversionPaymentsArtifact } from '../../src/lib'; -import Utils from '@requestnetwork/utils'; -import { HttpNetworkConfig } from 'hardhat/types'; - -// set to true to log batch payments's gas consumption -const logGas = false; - -describe('contract: BatchConversionPayments', () => { - const networkConfig = network.config as HttpNetworkConfig; - const provider = new ethers.providers.JsonRpcProvider(networkConfig.url); - - let from: string; - let to: string; - let feeAddress: string; - let signer: Signer; - const batchFee = 50; - const batchConvFee = 100; - 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 testBatchConversionProxy: BatchConversionPayments; - let ethereumFeeProxy: EthereumFeeProxy; - let chainlinkPath: ChainlinkConversionPath; - - let conversionToPay: BigNumber; - let feesToPay: BigNumber; - - type ConversionDetail = { - recipient: string; - requestAmount: BigNumberish; - path: string[]; - paymentReference: BytesLike; - feeAmount: BigNumberish; - maxToSpend: BigNumberish; - maxRateTimespan: BigNumberish; - }; - let convDetail: ConversionDetail; - let inputs: Array; - - let tx: ContractTransaction; - - let beforeEthBalanceTo: BigNumber; - let beforeEthBalanceFee: BigNumber; - let beforeEthBalance: BigNumber; - - let amountToPayExpected: BigNumber; - let feeToPayExpected: BigNumber; - // amount and feeAmount are usually in fiat for conversion inputs, else in ETH - const amount = BigNumber.from(100000); - const feeAmount = amount.mul(10).div(10000); - const pathUsdEth = [USD_hash, ETH_hash]; - - /** - * @notice Function batch conversion, it can be the batchRouter function, used with conversion args, - * or directly batchERC20ConversionPaymentsMultiTokens - * */ - let batchConvFunction: ( - args: any, - feeAddress: string, - optional?: any, - ) => Promise; - - /** - * @notice it modify the Eth batch conversion inputs if needed, depending it is - * directly or through batchRouter - * @param useBatchRouter - * @param inputs a list of convDetail - */ - const getEthConvInputs = (useBatchRouter: boolean, inputs: Array) => { - if (useBatchRouter) { - return [ - { - paymentNetworkId: '3', - conversionDetails: inputs, - cryptoDetails: { - tokenAddresses: [], - recipients: [], - amounts: [], - paymentReferences: [], - feeAmounts: [], - }, // cryptoDetails is not used - }, - ]; - } - return inputs; - }; - - const checkEthBalances = async (amountToPayExpected: BigNumber, feeToPayExpected: BigNumber) => { - const receipt = await tx.wait(); - if (logGas) console.log('gas consumption: ', receipt.gasUsed.toString()); // get balances - const gasUsed = receipt.gasUsed.mul(2 * 10 ** 10); - - const afterEthBalance = await provider.getBalance(await signer.getAddress()); - const afterEthBalanceTo = await provider.getBalance(to); - const afterEthBalanceFee = await provider.getBalance(feeAddress); - const proxyBalance = await provider.getBalance(testBatchConversionProxy.address); - - // Calculate the difference of the balance : now - before - const _diffBalance = beforeEthBalance.sub(afterEthBalance); - const _diffBalanceTo = afterEthBalanceTo.sub(beforeEthBalanceTo); - const _diffBalanceFee = afterEthBalanceFee.sub(beforeEthBalanceFee); - - // feeToPayExpected includes batch conversion fees now - feeToPayExpected = amountToPayExpected - .add(feeToPayExpected) - .mul(batchConvFee) - .div(10000) - .add(feeToPayExpected); - const _diffBalanceExpect = gasUsed.add(amountToPayExpected).add(feeToPayExpected); - - // Check balance changes - expect(_diffBalance).to.equals(_diffBalanceExpect, 'DiffBalance'); - expect(_diffBalanceTo).to.equals(amountToPayExpected, 'diffBalanceTo'); - expect(_diffBalanceFee).to.equals(feeToPayExpected, 'diffBalanceFee'); - expect(proxyBalance).to.equals('0', 'proxyBalance'); - }; - /** - * @notice it contains all the tests related to the Eth batch payment, and its context required. - * It tests the 2 functions directly, or through batchRouter function. - * Functions: batchEthConversionPaymentsWithReference, and batchEthPaymentsWithReference - * @param useBatchRouter - */ - for (const useBatchRouter of [true, false]) { - describe(`Test ETH batch functions ${ - useBatchRouter ? 'through batchRouter' : 'without batchRouter' - }`, () => { - before(async () => { - [from, to, feeAddress] = (await ethers.getSigners()).map((s) => s.address); - from; - [signer] = await ethers.getSigners(); - chainlinkPath = chainlinkConversionPath.connect(network.name, signer); - ethereumFeeProxy = await new EthereumFeeProxy__factory(signer).deploy(); - testEthConversionProxy = await new EthConversionProxy__factory(signer).deploy( - ethereumFeeProxy.address, - chainlinkPath.address, - ETH_hash, - ); - - testBatchConversionProxy = batchConversionPaymentsArtifact.connect(network.name, signer); - - // update batch payment proxies, and batch fees - await testBatchConversionProxy.setPaymentEthProxy(ethereumFeeProxy.address); - await testBatchConversionProxy.setPaymentEthConversionProxy(testEthConversionProxy.address); - await testBatchConversionProxy.setBatchFee(batchFee); - await testBatchConversionProxy.setBatchConversionFee(batchConvFee); - - convDetail = { - recipient: to, - requestAmount: amount, - path: pathUsdEth, - paymentReference: referenceExample, - feeAmount: feeAmount, - maxToSpend: BigNumber.from(0), - maxRateTimespan: BigNumber.from(0), - }; - - // basic setup: 1 payment - const conversionToPayFull = await chainlinkPath.getConversion( - convDetail.requestAmount, - convDetail.path, - ); - conversionToPay = conversionToPayFull.result; - const feesToPayFull = await chainlinkPath.getConversion( - convDetail.feeAmount, - convDetail.path, - ); - feesToPay = feesToPayFull.result; - - if (useBatchRouter) { - batchConvFunction = testBatchConversionProxy.batchRouter; - } else { - batchConvFunction = testBatchConversionProxy.batchEthConversionPaymentsWithReference; - } - }); - - beforeEach(async () => { - beforeEthBalanceTo = await provider.getBalance(to); - beforeEthBalanceFee = await provider.getBalance(feeAddress); - beforeEthBalance = await provider.getBalance(await signer.getAddress()); - - // expected balances, it can be modified for each test - amountToPayExpected = conversionToPay; - // fees does not include batch fees yet - feeToPayExpected = feesToPay; - }); - - describe('success functions', () => { - it('batchEthConversionPaymentsWithReference transfer 1 payment in ethers denominated in USD', async function () { - inputs = [convDetail]; - tx = await batchConvFunction(getEthConvInputs(useBatchRouter, inputs), feeAddress, { - value: BigNumber.from('100000000000000000'), - }); - await checkEthBalances(amountToPayExpected, feeToPayExpected); - }); - - it('batchEthConversionPaymentsWithReference transfer 3 payment in ethers denominated in USD', async function () { - amountToPayExpected = amountToPayExpected.mul(3); - feeToPayExpected = feeToPayExpected.mul(3); - inputs = [convDetail, convDetail, convDetail]; - tx = await batchConvFunction(getEthConvInputs(useBatchRouter, inputs), feeAddress, { - value: BigNumber.from('100000000000000000'), - }); - await checkEthBalances(amountToPayExpected, feeToPayExpected); - }); - - it('batchEthConversionPaymentsWithReference transfer 3 payments in ethers denominated in USD and EUR', async function () { - const EurConvDetail = Utils.deepCopy(convDetail); - EurConvDetail.path = [EUR_hash, USD_hash, ETH_hash]; - - const eurConversionToPay = await chainlinkPath.getConversion( - EurConvDetail.requestAmount, - EurConvDetail.path, - ); - const eurFeesToPay = await chainlinkPath.getConversion( - EurConvDetail.feeAmount, - EurConvDetail.path, - ); - - amountToPayExpected = eurConversionToPay.result.add(amountToPayExpected.mul(2)); - feeToPayExpected = eurFeesToPay.result.add(feeToPayExpected.mul(2)); - inputs = [convDetail, EurConvDetail, convDetail]; - - tx = await batchConvFunction(getEthConvInputs(useBatchRouter, inputs), feeAddress, { - value: BigNumber.from('100000000000000000'), - }); - await checkEthBalances(amountToPayExpected, feeToPayExpected); - }); - - it('batchEthPaymentsWithReference transfer 1 payment', async function () { - beforeEthBalanceTo = await provider.getBalance(to); - beforeEthBalanceFee = await provider.getBalance(feeAddress); - beforeEthBalance = await provider.getBalance(await signer.getAddress()); - - const cryptoDetails = { - tokenAddresses: [], - recipients: [to], - amounts: [amount], // in ETH - paymentReferences: [referenceExample], - feeAmounts: [feeAmount], // in ETH - }; - if (useBatchRouter) { - await testBatchConversionProxy.batchRouter( - [ - { - paymentNetworkId: 4, - conversionDetails: [convDetail], // not used - cryptoDetails: cryptoDetails, - }, - ], - feeAddress, - { value: 1000000000 }, - ); - } else { - await testBatchConversionProxy.batchEthPaymentsWithReference( - cryptoDetails.recipients, - cryptoDetails.amounts, - cryptoDetails.paymentReferences, - cryptoDetails.feeAmounts, - feeAddress, - { value: 1000000000 }, - ); - } - - amountToPayExpected = amount; - feeToPayExpected = feeAmount; - const afterEthBalanceTo = await provider.getBalance(to); - const afterEthBalanceFee = await provider.getBalance(feeAddress); - const proxyBalance = await provider.getBalance(testBatchConversionProxy.address); - const _diffBalanceTo = afterEthBalanceTo.sub(beforeEthBalanceTo); - const _diffBalanceFee = afterEthBalanceFee.sub(beforeEthBalanceFee); - - expect(_diffBalanceTo).to.equals(amountToPayExpected.toString(), 'diffBalanceTo'); - - feeToPayExpected = amountToPayExpected.mul(batchFee).div(10000).add(feeToPayExpected); - expect(_diffBalanceFee.toString()).to.equals( - feeToPayExpected.toString(), - 'diffBalanceFee', - ); - expect(proxyBalance).to.equals('0', 'proxyBalance'); - }); - }); - describe('revert functions', () => { - it('batchEthConversionPaymentsWithReference transfer FAIL: not enough funds', async function () { - await expect( - batchConvFunction(getEthConvInputs(useBatchRouter, [convDetail]), feeAddress, { - value: 10000, - }), - ).to.be.revertedWith('paymentProxy transferExactEthWithReferenceAndFee failed'); - }); - it('batchEthPaymentsWithReference transfer FAIL: not enough funds', async function () { - const cryptoDetails = { - tokenAddresses: [], - recipients: [to], - amounts: [amount], - paymentReferences: [referenceExample], - feeAmounts: [feeAmount], - }; - - // it contains the function being just executed, and still processing - let batchEthPayments; - if (useBatchRouter) { - batchEthPayments = testBatchConversionProxy.batchRouter( - [ - { - paymentNetworkId: 4, - conversionDetails: [convDetail], // not used - cryptoDetails: cryptoDetails, - }, - ], - feeAddress, - { value: 10000 }, - ); - } else { - batchEthPayments = testBatchConversionProxy.batchEthPaymentsWithReference( - cryptoDetails.recipients, - cryptoDetails.amounts, - cryptoDetails.paymentReferences, - cryptoDetails.feeAmounts, - feeAddress, - { value: 10000 }, - ); - } - await expect(batchEthPayments).to.be.revertedWith('not enough funds'); - }); - }); - }); - } -}); diff --git a/packages/smart-contracts/test/contracts/BatchConversionPayments.test.ts b/packages/smart-contracts/test/contracts/BatchConversionPayments.test.ts new file mode 100644 index 000000000..4d9e31941 --- /dev/null +++ b/packages/smart-contracts/test/contracts/BatchConversionPayments.test.ts @@ -0,0 +1,815 @@ +import { ethers, network } from 'hardhat'; +import { + ERC20FeeProxy__factory, + Erc20ConversionProxy__factory, + EthConversionProxy__factory, + EthereumFeeProxy__factory, + ChainlinkConversionPath, + TestERC20, + Erc20ConversionProxy, + EthConversionProxy, + TestERC20__factory, + BatchConversionPayments__factory, + BatchConversionPayments, +} from '../../src/types'; +import { BigNumber, ContractTransaction, Signer } from 'ethers'; +import { expect } from 'chai'; +import { CurrencyManager } from '@requestnetwork/currency'; +import { chainlinkConversionPath } from '../../src/lib'; +import { localERC20AlphaArtifact, secondLocalERC20AlphaArtifact } from './localArtifacts'; +import Utils from '@requestnetwork/utils'; +import { HttpNetworkConfig } from 'hardhat/types'; + +describe('contract: BatchConversionPayments', async () => { + const networkConfig = network.config as HttpNetworkConfig; + const provider = new ethers.providers.JsonRpcProvider(networkConfig.url); + + let from: string; + let to: string; + let feeAddress: string; + let adminSigner: Signer; + let signer1: Signer; + let signer4: Signer; + + let tx: ContractTransaction; + + // constants used to set up batch conversion proxy, and also requests payment + const batchFee = 50; + const batchConvFee = 100; + const thousandWith18Decimal = '1000000000000000000000'; + const referenceExample = '0xaaaa'; + /** + * amount and feeAmount are in: + * - EUR, or USD for conversion inputs + * - DAI for non-conversion ERC20 inputs + * - ETH for non-conversion ETH inputs + */ + const amount = BigNumber.from(100000); + const feeAmount = amount.div(1000); + + // constants and variables to set up proxies and paths + 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 DAI_address: string; + let FAU_address: string; + + let erc20ConversionProxy: Erc20ConversionProxy; + let ethConversionProxy: EthConversionProxy; + let batchConversionProxy: BatchConversionPayments; + let daiERC20: TestERC20; + let fauERC20: TestERC20; + let chainlinkPath: ChainlinkConversionPath; + + // variables used to check daiERC20 balances (1st token) + let fromOldBalance1: BigNumber; + let toOldBalance1: BigNumber; + let feeOldBalance1: BigNumber; + + let fromDiffBalanceExpected1: BigNumber; + let toDiffBalanceExpected1: BigNumber; + let feeDiffBalanceExpected1: BigNumber; + + // variables used to check ETH balances + let beforeEthBalanceTo: BigNumber; + let beforeEthBalanceFee: BigNumber; + let beforeEthBalance: BigNumber; + + // variables used for chainlink and conversion payments + let conversionToPay: BigNumber; + let conversionFees: BigNumber; + + // variables used for Eth conversion payments, and also as expected value + let ethConversionToPay: BigNumber; + let ethConversionFee: BigNumber; + + // type required by Erc20 conversion batch function inputs + let convDetail: any; + let ethConvDetail: any; + + const emptyCryptoDetails = { + tokenAddresses: [], + recipients: [], + amounts: [], + paymentReferences: [], + feeAmounts: [], + }; + + /** + * @notice it sets the conversions including fees to be paid, and it set the convDetail input + * @dev it update 3 global variables: conversionToPay, conversionFees, and convDetail + */ + const setConvToPayAndConvDetail = async ( + recipient: string, + path: string[], + requestAmount: string, + feeAmount: string, + maxRateTimespan: number, + chainlinkPath: ChainlinkConversionPath, + ) => { + conversionToPay = (await chainlinkPath.getConversion(requestAmount, path)).result; + conversionFees = (await chainlinkPath.getConversion(feeAmount, path)).result; + convDetail = { + recipient: recipient, + requestAmount: requestAmount, + path: path, + paymentReference: referenceExample, + feeAmount: feeAmount, + maxToSpend: conversionToPay.add(conversionFees).toString(), + maxRateTimespan: maxRateTimespan, + }; + }; + + /** + * check token ERC20 balances of: the payer (from), the recipient (to), the feeAddress, and the batch contract + */ + const checkBalancesForOneToken = async ( + testERC20: TestERC20, + fromOldBalance: BigNumber, + toOldBalance: BigNumber, + feeOldBalance: BigNumber, + fromDiffBalanceExpected: BigNumber, + toDiffBalanceExpected: BigNumber, + feeDiffBalanceExpected: BigNumber, + ) => { + // Get balances + const fromBalance = await testERC20.balanceOf(from); + const toBalance = await testERC20.balanceOf(to); + const feeBalance = await testERC20.balanceOf(feeAddress); + const batchBalance = await testERC20.balanceOf(batchConversionProxy.address); + + // Calculate the difference of the balance : now - before + const fromDiffBalance = BigNumber.from(fromBalance).sub(fromOldBalance); + const toDiffBalance = BigNumber.from(toBalance).sub(toOldBalance); + const feeDiffBalance = BigNumber.from(feeBalance).sub(feeOldBalance); + // Check balance changes + expect(fromDiffBalance).to.equals(fromDiffBalanceExpected, 'fromDiffBalance'); + expect(toDiffBalance).to.equals(toDiffBalanceExpected, 'toDiffBalance'); + expect(feeDiffBalance).to.equals(feeDiffBalanceExpected, 'feeDiffBalance'); + expect(batchBalance).to.equals('0', 'batchBalance'); + }; + + /** + * @notice Used to calculate the expected new ERC20 balance of a single token for batch conversion. + * @dev fees are not exactly calculated with the same formula, depending if it is with conversion or not + */ + const expectedERC20Balances = ( + conversionToPay_results: BigNumber[], + conversionFees_results: BigNumber[], + appliedFees: number, + withConversion = true, + ) => { + let fromDiffBalanceExpected = conversionToPay_results.reduce( + (prev, x) => prev.sub(x), + BigNumber.from(0), + ); + let toDiffBalanceExpected = fromDiffBalanceExpected.mul(-1); + let feeDiffBalanceExpected = conversionFees_results.reduce( + (prev, x) => prev.add(x), + BigNumber.from(0), + ); + + feeDiffBalanceExpected = withConversion + ? toDiffBalanceExpected + .add(feeDiffBalanceExpected) + .mul(appliedFees) + .div(10000) + .add(feeDiffBalanceExpected) + : toDiffBalanceExpected.mul(appliedFees).div(10000).add(feeDiffBalanceExpected); + + fromDiffBalanceExpected = fromDiffBalanceExpected.sub(feeDiffBalanceExpected); + return [fromDiffBalanceExpected, toDiffBalanceExpected, feeDiffBalanceExpected]; + }; + + /** + * Pays 3 ERC20 conversions payments, with DAI and FAU tokens and it calculates the balances + * It also check the balances expected for FAU token. + * @param path2 to update the copy of convDetail: convDetail2 + */ + const manyPaymentsBatchConv = async ( + path1: string[], + path2: string[], + withBatchRouter = false, + ) => { + // set convDetail with "path1" + await setConvToPayAndConvDetail( + to, + path1, + amount.toString(), + feeAmount.toString(), + 0, + chainlinkPath, + ); + // define a second payment request + const conversionToPay2 = (await chainlinkPath.getConversion(amount.toString(), path2)).result; + const conversionFees2 = (await chainlinkPath.getConversion(feeAmount.toString(), path2)).result; + const convDetail2 = Utils.deepCopy(convDetail); + convDetail2.path = path2; + convDetail2.maxToSpend = conversionToPay2.add(conversionFees2).toString(); + + // define conversionsToPays & conversionsFees to calculate the expected balances + const conversionsToPays = [conversionToPay, conversionToPay, conversionToPay2]; + const conversionsFees = [conversionFees, conversionFees, conversionFees2]; + + // get balances of the 2nd token, useful when there are 2 different tokens used + const fromOldBalance2 = await fauERC20.balanceOf(from); + const toOldBalance2 = await fauERC20.balanceOf(to); + const feeOldBalance2 = await fauERC20.balanceOf(feeAddress); + + if (withBatchRouter) { + await batchConversionProxy.batchRouter( + [ + { + paymentNetworkId: '0', + conversionDetails: [convDetail, convDetail, convDetail2], + cryptoDetails: emptyCryptoDetails, + }, + ], + feeAddress, + ); + } else { + await batchConversionProxy.batchMultiERC20ConversionPayments( + [convDetail, convDetail, convDetail2], + feeAddress, + ); + } + + // 1st token: daiERC20 - calculate the expected balances + [fromDiffBalanceExpected1, toDiffBalanceExpected1, feeDiffBalanceExpected1] = + expectedERC20Balances( + conversionsToPays.slice(0, 2), + conversionsFees.slice(0, 2), + batchConvFee, + ); + + // 2nd token: fauERC20 - calculate the expected balances + const [fromDiffBalanceExpected2, toDiffBalanceExpected2, feeDiffBalanceExpected2] = + expectedERC20Balances( + conversionsToPays.slice(2, 3), + conversionsFees.slice(2, 3), + batchConvFee, + ); + + // check the balance of the 2nd token, which is not checked in "afterEach" contrary to the 1st token. + checkBalancesForOneToken( + fauERC20, + fromOldBalance2, + toOldBalance2, + feeOldBalance2, + fromDiffBalanceExpected2, + toDiffBalanceExpected2, + feeDiffBalanceExpected2, + ); + }; + + /** + * Gets the balances, calculates the difference between "before" and "after" and raise an error if needed + * @param ethAmount the amount of ETH to pay + * @param ethFeeAmount the fee amount of ETH to pay, before to apply batch fees + * @param feeApplied the batch fees to apply: batchConvFee, or batchFee + */ + const checkEthBalances = async ( + ethAmount: BigNumber, + ethFeeAmount: BigNumber, + feeApplied = batchConvFee, + ) => { + const receipt = await tx.wait(); + const gasUsed = receipt.gasUsed.mul(2 * 10 ** 10); + + const afterEthBalance = await provider.getBalance(await signer1.getAddress()); + const afterEthBalanceTo = await provider.getBalance(to); + const afterEthBalanceFee = await provider.getBalance(feeAddress); + const proxyBalance = await provider.getBalance(batchConversionProxy.address); + + // Calculate the difference of the balance : now - before + const diffBalance = beforeEthBalance.sub(afterEthBalance); + const diffBalanceTo = afterEthBalanceTo.sub(beforeEthBalanceTo); + const diffBalanceFee = afterEthBalanceFee.sub(beforeEthBalanceFee); + + // ethFeeAmount includes batch conversion fees now + ethFeeAmount = ethAmount.add(ethFeeAmount).mul(feeApplied).div(10000).add(ethFeeAmount); + const diffBalanceExpect = gasUsed.add(ethAmount).add(ethFeeAmount); + // Check balance changes + expect(diffBalance).to.equals(diffBalanceExpect, 'DiffBalance'); + expect(diffBalanceTo).to.equals(ethAmount, 'diffBalanceTo'); + expect(diffBalanceFee).to.equals(ethFeeAmount, 'diffBalanceFee'); + expect(proxyBalance).to.equals('0', 'proxyBalance'); + }; + + before(async () => { + [, from, to, feeAddress] = (await ethers.getSigners()).map((s) => s.address); + [adminSigner, signer1, , , signer4] = await ethers.getSigners(); + + chainlinkPath = chainlinkConversionPath.connect(network.name, signer1); + + const erc20FeeProxy = await new ERC20FeeProxy__factory(adminSigner).deploy(); + const ethFeeProxy = await new EthereumFeeProxy__factory(adminSigner).deploy(); + erc20ConversionProxy = await new Erc20ConversionProxy__factory(adminSigner).deploy( + erc20FeeProxy.address, + chainlinkPath.address, + await adminSigner.getAddress(), + ); + ethConversionProxy = await new EthConversionProxy__factory(adminSigner).deploy( + ethFeeProxy.address, + chainlinkPath.address, + ETH_hash, + ); + + batchConversionProxy = await new BatchConversionPayments__factory(adminSigner).deploy( + erc20FeeProxy.address, + ethFeeProxy.address, + erc20ConversionProxy.address, + ethConversionProxy.address, + await adminSigner.getAddress(), + ); + + // set batch proxy fees and connect signer1 + await batchConversionProxy.setBatchFee(batchFee); + await batchConversionProxy.setBatchConversionFee(batchConvFee); + batchConversionProxy = batchConversionProxy.connect(signer1); + + // set ERC20 tokens and transfer token to "from" (signer1) + DAI_address = localERC20AlphaArtifact.getAddress(network.name); + daiERC20 = new TestERC20__factory(adminSigner).attach(DAI_address); + await daiERC20.transfer(from, BigNumber.from(thousandWith18Decimal)); + daiERC20 = daiERC20.connect(signer1); + + FAU_address = secondLocalERC20AlphaArtifact.getAddress(network.name); + fauERC20 = new TestERC20__factory(adminSigner).attach(FAU_address); + await fauERC20.transfer(from, BigNumber.from(thousandWith18Decimal)); + fauERC20 = fauERC20.connect(signer1); + }); + + beforeEach(async () => { + fromDiffBalanceExpected1 = BigNumber.from(0); + toDiffBalanceExpected1 = BigNumber.from(0); + feeDiffBalanceExpected1 = BigNumber.from(0); + await daiERC20.approve(batchConversionProxy.address, thousandWith18Decimal, { + from, + }); + await fauERC20.approve(batchConversionProxy.address, thousandWith18Decimal, { + from, + }); + // get balances of daiERC20 token + fromOldBalance1 = await daiERC20.balanceOf(from); + toOldBalance1 = await daiERC20.balanceOf(to); + feeOldBalance1 = await daiERC20.balanceOf(feeAddress); + + // create a default ERC20 convDetail + setConvToPayAndConvDetail( + to, + [EUR_hash, USD_hash, DAI_address], + amount.toString(), + feeAmount.toString(), + 0, + chainlinkPath, + ); + + // get Eth balances + beforeEthBalanceTo = await provider.getBalance(to); + beforeEthBalanceFee = await provider.getBalance(feeAddress); + beforeEthBalance = await provider.getBalance(await signer1.getAddress()); + + ethConvDetail = { + recipient: to, + requestAmount: amount, + path: [USD_hash, ETH_hash], + paymentReference: referenceExample, + feeAmount: feeAmount, + maxToSpend: BigNumber.from(0), + maxRateTimespan: BigNumber.from(0), + }; + + // expected balances, it can be modified for each test + ethConversionToPay = ( + await chainlinkPath.getConversion(ethConvDetail.requestAmount, ethConvDetail.path) + ).result; + // fees does not include batch fees yet + ethConversionFee = ( + await chainlinkPath.getConversion(ethConvDetail.feeAmount, ethConvDetail.path) + ).result; + }); + + afterEach(async () => { + // check balances of daiERC20 token + checkBalancesForOneToken( + daiERC20, + fromOldBalance1, + toOldBalance1, + feeOldBalance1, + fromDiffBalanceExpected1, + toDiffBalanceExpected1, + feeDiffBalanceExpected1, + ); + }); + describe('batchRouter', async () => { + it(`make ERC20 payment with no conversion`, async function () { + await batchConversionProxy.batchRouter( + [ + { + paymentNetworkId: 2, + conversionDetails: [], + cryptoDetails: { + tokenAddresses: [DAI_address], + recipients: [to], + amounts: [amount], + paymentReferences: [referenceExample], + feeAmounts: [feeAmount], + }, + }, + ], + feeAddress, + ); + + [fromDiffBalanceExpected1, toDiffBalanceExpected1, feeDiffBalanceExpected1] = + expectedERC20Balances( + [BigNumber.from(amount)], + [BigNumber.from(feeAmount)], + batchFee, + false, + ); + }); + it('make 3 ERC20 payments with different tokens and conversion lengths', async () => { + await manyPaymentsBatchConv([EUR_hash, USD_hash, DAI_address], [USD_hash, FAU_address], true); + }); + + it('make ETH payment without conversion', async function () { + const cryptoDetails = { + tokenAddresses: [], + recipients: [to], + amounts: [amount], // in ETH + paymentReferences: [referenceExample], + feeAmounts: [feeAmount], // in ETH + }; + tx = await batchConversionProxy.batchRouter( + [ + { + paymentNetworkId: 3, + conversionDetails: [], + cryptoDetails: cryptoDetails, + }, + ], + feeAddress, + { value: '1000000000' }, + ); + await checkEthBalances(amount, feeAmount, batchFee); + }); + + it('make ETH payment with 1-step conversion', async function () { + tx = await batchConversionProxy.batchRouter( + [ + { + paymentNetworkId: 4, + conversionDetails: [ethConvDetail], + cryptoDetails: emptyCryptoDetails, + }, + ], + feeAddress, + { + value: ethConversionToPay.mul(2), + }, + ); + await checkEthBalances(ethConversionToPay, ethConversionFee); + }); + + it('make n heterogeneous (ERC20 and ETH) payments with and without conversion', async () => { + // set convDetail: done within "beforeEach" + + // set ERC20 cryptoDetails + const cryptoDetails = { + tokenAddresses: [DAI_address], + recipients: [to], + amounts: [amount], + paymentReferences: [referenceExample], + feeAmounts: [feeAmount], + }; + const ethCryptoDetails = Utils.deepCopy(cryptoDetails); + ethCryptoDetails.tokenAddresses = []; + + await batchConversionProxy.batchRouter( + [ + { + paymentNetworkId: 0, + conversionDetails: [convDetail], + cryptoDetails: emptyCryptoDetails, + }, + { + paymentNetworkId: 2, + conversionDetails: [], + cryptoDetails: cryptoDetails, + }, + { + paymentNetworkId: 3, + conversionDetails: [], + cryptoDetails: ethCryptoDetails, + }, + { + paymentNetworkId: 4, + conversionDetails: [ethConvDetail], + cryptoDetails: emptyCryptoDetails, + }, + ], + feeAddress, + { value: ethConversionToPay.mul(2).add(amount) }, + ); + + const [ + conversionFromDiffBalanceExpected1, + conversionToDiffBalanceExpected1, + conversionFeeDiffBalanceExpected1, + ] = expectedERC20Balances([conversionToPay], [conversionFees], batchConvFee); + + const [ + noConversionFromDiffBalanceExpected1, + noConversionToDiffBalanceExpected1, + noConversionFeeDiffBalanceExpected1, + ] = expectedERC20Balances( + [BigNumber.from(amount)], + [BigNumber.from(feeAmount)], + batchFee, + false, + ); + + fromDiffBalanceExpected1 = conversionFromDiffBalanceExpected1.add( + noConversionFromDiffBalanceExpected1, + ); + toDiffBalanceExpected1 = conversionToDiffBalanceExpected1.add( + noConversionToDiffBalanceExpected1, + ); + feeDiffBalanceExpected1 = conversionFeeDiffBalanceExpected1.add( + noConversionFeeDiffBalanceExpected1, + ); + }); + }); + + describe('batchRouter errors', async () => { + it(`Too many elements within batchRouter metaDetails input`, async function () { + await expect( + batchConversionProxy.batchRouter( + [ + { + paymentNetworkId: 2, + conversionDetails: [], + cryptoDetails: emptyCryptoDetails, + }, + { + paymentNetworkId: 2, + conversionDetails: [], + cryptoDetails: emptyCryptoDetails, + }, + { + paymentNetworkId: 2, + conversionDetails: [], + cryptoDetails: emptyCryptoDetails, + }, + { + paymentNetworkId: 2, + conversionDetails: [], + cryptoDetails: emptyCryptoDetails, + }, + { + paymentNetworkId: 2, + conversionDetails: [], + cryptoDetails: emptyCryptoDetails, + }, + { + paymentNetworkId: 2, + conversionDetails: [], + cryptoDetails: emptyCryptoDetails, + }, + ], + feeAddress, + ), + ).to.be.revertedWith('more than 5 metaDetails'); + }); + it(`Too many elements within batchRouter metaDetails input`, async function () { + await expect( + batchConversionProxy.batchRouter( + [ + { + paymentNetworkId: 6, + conversionDetails: [], + cryptoDetails: emptyCryptoDetails, + }, + ], + feeAddress, + ), + ).to.be.revertedWith('wrong paymentNetworkId'); + }); + }); + describe('batchMultiERC20ConversionPayments', async () => { + it('make 1 payment with 1-step conversion', async () => { + await setConvToPayAndConvDetail( + to, + [USD_hash, DAI_address], + amount.toString(), + feeAmount.toString(), + 0, + chainlinkPath, + ); + await batchConversionProxy.batchMultiERC20ConversionPayments([convDetail], feeAddress); + [fromDiffBalanceExpected1, toDiffBalanceExpected1, feeDiffBalanceExpected1] = + expectedERC20Balances([conversionToPay], [conversionFees], batchConvFee); + }); + it('make 1 payment with 2-steps conversion', async () => { + await setConvToPayAndConvDetail( + to, + [EUR_hash, USD_hash, DAI_address], + amount.toString(), + feeAmount.toString(), + 0, + chainlinkPath, + ); + await batchConversionProxy.batchMultiERC20ConversionPayments([convDetail], feeAddress); + [fromDiffBalanceExpected1, toDiffBalanceExpected1, feeDiffBalanceExpected1] = + expectedERC20Balances([conversionToPay], [conversionFees], batchConvFee); + }); + it('make 3 payment with different tokens and conversion length', async () => { + await manyPaymentsBatchConv([EUR_hash, USD_hash, DAI_address], [USD_hash, FAU_address]); + }); + }); + + describe('batchMultiERC20ConversionPayments errors', async () => { + it('cannot transfer with invalid path', async function () { + convDetail.path = [EUR_hash, ETH_hash, DAI_address]; + await expect( + batchConversionProxy.batchMultiERC20ConversionPayments([convDetail], feeAddress), + ).to.be.revertedWith('revert No aggregator found'); + }); + + it('cannot transfer if max to spend too low', async function () { + convDetail.maxToSpend = conversionToPay.add(conversionFees).sub(1).toString(); + await expect( + batchConversionProxy.batchMultiERC20ConversionPayments([convDetail], feeAddress), + ).to.be.revertedWith('Amount to pay is over the user limit'); + }); + + it('cannot transfer if rate is too old', async function () { + convDetail.maxRateTimespan = 10; + await expect( + batchConversionProxy.batchMultiERC20ConversionPayments([convDetail], feeAddress), + ).to.be.revertedWith('aggregator rate is outdated'); + }); + + it('Not enough allowance', async function () { + // reduce signer1 allowance + await daiERC20.approve( + batchConversionProxy.address, + BigNumber.from(convDetail.maxToSpend).sub(2), + { + from, + }, + ); + await expect( + batchConversionProxy.batchMultiERC20ConversionPayments([convDetail], feeAddress), + ).to.be.revertedWith('Insufficient allowance for batch to pay'); + }); + + it('Not enough funds even if partially enough funds', async function () { + // signer1 transfer enough token to pay just 1 invoice to signer4 + await daiERC20 + .connect(signer1) + .transfer(await signer4.getAddress(), BigNumber.from(convDetail.maxToSpend)); + // increase signer4 allowance + await daiERC20.connect(signer4).approve(batchConversionProxy.address, thousandWith18Decimal); + + // 3 invoices to pay + await expect( + batchConversionProxy + .connect(signer4) + .batchMultiERC20ConversionPayments([convDetail, convDetail, convDetail], feeAddress), + ).to.be.revertedWith('not enough funds, including fees'); + + // signer4 transfer token to signer1 + await daiERC20 + .connect(signer4) + .transfer(from, await daiERC20.balanceOf(await signer4.getAddress())); + }); + }); + describe(`batchEthConversionPayments`, () => { + it('make 1 payment with 1-step conversion', async function () { + tx = await batchConversionProxy.batchEthConversionPayments([ethConvDetail], feeAddress, { + value: BigNumber.from('100000000000000000'), + }); + await checkEthBalances(ethConversionToPay, ethConversionFee); + }); + + it('make 3 payments with different conversion lengths', async function () { + const EurConvDetail = Utils.deepCopy(ethConvDetail); + EurConvDetail.path = [EUR_hash, USD_hash, ETH_hash]; + + const eurConversionToPay = await chainlinkPath.getConversion( + EurConvDetail.requestAmount, + EurConvDetail.path, + ); + const eurFeesToPay = await chainlinkPath.getConversion( + EurConvDetail.feeAmount, + EurConvDetail.path, + ); + + tx = await batchConversionProxy.batchEthConversionPayments( + [ethConvDetail, EurConvDetail, ethConvDetail], + feeAddress, + { + value: BigNumber.from('100000000000000000'), + }, + ); + await checkEthBalances( + eurConversionToPay.result.add(ethConversionToPay.mul(2)), + eurFeesToPay.result.add(ethConversionFee.mul(2)), + ); + }); + }); + describe('batchEthConversionPayments errors', () => { + it('cannot transfer with invalid path', async function () { + const wrongConvDetail = Utils.deepCopy(ethConvDetail); + wrongConvDetail.path = [USD_hash, EUR_hash, ETH_hash]; + await expect( + batchConversionProxy.batchEthConversionPayments([wrongConvDetail], feeAddress, { + value: ethConversionToPay.mul(2), + }), + ).to.be.revertedWith('No aggregator found'); + }); + it('not enough funds even if partially enough funds', async function () { + await expect( + batchConversionProxy.batchEthConversionPayments( + [ethConvDetail, ethConvDetail], + feeAddress, + { + value: ethConversionToPay.mul(2), // no enough to pay the amount AND the fees + }, + ), + ).to.be.revertedWith('paymentProxy transferExactEthWithReferenceAndFee failed'); + }); + + it('cannot transfer if rate is too old', async function () { + const wrongConvDetail = Utils.deepCopy(ethConvDetail); + wrongConvDetail.maxRateTimespan = BigNumber.from('1'); + await expect( + batchConversionProxy.batchEthConversionPayments([wrongConvDetail], feeAddress, { + value: ethConversionToPay.mul(2), + }), + ).to.be.revertedWith('aggregator rate is outdated'); + }); + }); + describe('Functions herited from contract BatchErc20Payments ', () => { + it(`batchERC20Payments 1 payment`, async function () { + await batchConversionProxy.batchERC20Payments( + DAI_address, + [to], + [amount], + [referenceExample], + [feeAmount], + feeAddress, + ); + + [fromDiffBalanceExpected1, toDiffBalanceExpected1, feeDiffBalanceExpected1] = + expectedERC20Balances( + [BigNumber.from(amount)], + [BigNumber.from(feeAmount)], + batchFee, + false, + ); + }); + + it(`batchMultiERC20Payments 1 payment`, async function () { + await batchConversionProxy.batchMultiERC20Payments( + [DAI_address], + [to], + [amount], + [referenceExample], + [feeAmount], + feeAddress, + ); + + [fromDiffBalanceExpected1, toDiffBalanceExpected1, feeDiffBalanceExpected1] = + expectedERC20Balances( + [BigNumber.from(amount)], + [BigNumber.from(feeAmount)], + batchFee, + false, + ); + }); + + it('make 1 payment without conversion', async function () { + const cryptoDetails = { + tokenAddresses: [], + recipients: [to], + amounts: [amount], // in ETH + paymentReferences: [referenceExample], + feeAmounts: [feeAmount], // in ETH + }; + tx = await batchConversionProxy.batchEthPayments( + cryptoDetails.recipients, + cryptoDetails.amounts, + cryptoDetails.paymentReferences, + cryptoDetails.feeAmounts, + feeAddress, + { value: 1000000000 }, + ); + await checkEthBalances(amount, feeAmount, batchFee); + }); + }); +}); diff --git a/packages/smart-contracts/test/contracts/BatchErc20Payments.test.ts b/packages/smart-contracts/test/contracts/BatchNoConversionErc20Payments.test.ts similarity index 92% rename from packages/smart-contracts/test/contracts/BatchErc20Payments.test.ts rename to packages/smart-contracts/test/contracts/BatchNoConversionErc20Payments.test.ts index 03daf1676..2b8e87a56 100644 --- a/packages/smart-contracts/test/contracts/BatchErc20Payments.test.ts +++ b/packages/smart-contracts/test/contracts/BatchNoConversionErc20Payments.test.ts @@ -1,12 +1,19 @@ -import { ethers, network } from 'hardhat'; +import { ethers } from 'hardhat'; import { BigNumber, Signer } from 'ethers'; import { expect } from 'chai'; -import { TestERC20__factory, TestERC20, BatchPayments, ERC20FeeProxy } from '../../src/types'; -import { batchPaymentsArtifact, erc20FeeProxyArtifact } from '../../src/lib'; +import { + TestERC20__factory, + TestERC20, + ERC20FeeProxy, + EthereumFeeProxy__factory, + BatchNoConversionPayments, + ERC20FeeProxy__factory, + BatchNoConversionPayments__factory, +} from '../../src/types'; const logGasInfos = false; -describe('contract: BatchPayments: ERC20', () => { +describe('contract: batchNoConversionPayments: ERC20', () => { let payee1: string; let payee2: string; let payee3: string; @@ -20,7 +27,7 @@ describe('contract: BatchPayments: ERC20', () => { let token1: TestERC20; let token2: TestERC20; let token3: TestERC20; - let batch: BatchPayments; + let batch: BatchNoConversionPayments; let erc20FeeProxy: ERC20FeeProxy; let token1Address: string; @@ -50,8 +57,13 @@ describe('contract: BatchPayments: ERC20', () => { [, payee1, payee2, payee3, feeAddress] = (await ethers.getSigners()).map((s) => s.address); [owner, spender1, spender2, spender3] = await ethers.getSigners(); - erc20FeeProxy = erc20FeeProxyArtifact.connect(network.name, owner); - batch = batchPaymentsArtifact.connect(network.name, owner); + erc20FeeProxy = await new ERC20FeeProxy__factory(owner).deploy(); + const ethFeeProxy = await new EthereumFeeProxy__factory(owner).deploy(); + batch = await new BatchNoConversionPayments__factory(owner).deploy( + erc20FeeProxy.address, + ethFeeProxy.address, + await owner.getAddress(), + ); token1 = await new TestERC20__factory(owner).deploy(erc20Decimal.mul(10000)); token2 = await new TestERC20__factory(owner).deploy(erc20Decimal.mul(10000)); token3 = await new TestERC20__factory(owner).deploy(erc20Decimal.mul(10000)); @@ -65,7 +77,7 @@ describe('contract: BatchPayments: ERC20', () => { token3Address = token3.address; batchAddress = batch.address; - await batch.connect(owner).setBatchFee(100); + await batch.connect(owner).setBatchFee(1000); }); beforeEach(async () => { @@ -86,10 +98,6 @@ describe('contract: BatchPayments: ERC20', () => { await token3.connect(spender3).approve(batchAddress, 0); }); - after(async () => { - await batch.connect(owner).setBatchFee(10); - }); - describe('Batch working well: right args, and approvals', () => { it('Should pay 3 ERC20 payments with paymentRef and pay batch fee', async function () { await token1.connect(owner).transfer(spender3Address, 1000); @@ -102,7 +110,7 @@ describe('contract: BatchPayments: ERC20', () => { await expect( batch .connect(spender3) - .batchERC20PaymentsWithReference( + .batchERC20Payments( token1Address, [payee1, payee2, payee2], [200, 30, 40], @@ -181,7 +189,7 @@ describe('contract: BatchPayments: ERC20', () => { await expect( batch .connect(spender3) - .batchERC20PaymentsMultiTokensWithReference( + .batchMultiERC20Payments( [token1Address, token2Address, token3Address], [payee1, payee2, payee2], [500, 300, 400], @@ -272,7 +280,7 @@ describe('contract: BatchPayments: ERC20', () => { const tx = await batch .connect(spender3) - .batchERC20PaymentsMultiTokensWithReference( + .batchMultiERC20Payments( [token1Address, token2Address, token3Address], [payee1, payee2, payee2], [500, 0, 400], @@ -320,7 +328,7 @@ describe('contract: BatchPayments: ERC20', () => { const tx = await batch .connect(spender3) - .batchERC20PaymentsMultiTokensWithReference( + .batchMultiERC20Payments( tokenAddresses, recipients, amounts, @@ -359,7 +367,7 @@ describe('contract: BatchPayments: ERC20', () => { const tx = await batch .connect(spender3) - .batchERC20PaymentsWithReference( + .batchERC20Payments( token1Addresses[0], recipients, amounts, @@ -405,7 +413,7 @@ describe('contract: BatchPayments: ERC20', () => { const tx = await batch .connect(spender3) - .batchERC20PaymentsMultiTokensWithReference( + .batchMultiERC20Payments( tokenAddresses, recipients, amounts, @@ -434,7 +442,7 @@ describe('contract: BatchPayments: ERC20', () => { await expect( batch .connect(spender3) - .batchERC20PaymentsWithReference( + .batchERC20Payments( token1Address, [payee1, payee2, payee3], [5, 30, 400], @@ -442,7 +450,7 @@ describe('contract: BatchPayments: ERC20', () => { [1, 2, 3], feeAddress, ), - ).revertedWith('revert not enough funds'); + ).revertedWith('not enough funds'); }); it('Should revert batch if not enough funds to pay the batch fee', async function () { @@ -452,7 +460,7 @@ describe('contract: BatchPayments: ERC20', () => { await expect( batch .connect(spender3) - .batchERC20PaymentsWithReference( + .batchERC20Payments( token1Address, [payee1, payee2], [100, 200], @@ -469,7 +477,7 @@ describe('contract: BatchPayments: ERC20', () => { await expect( batch .connect(spender3) - .batchERC20PaymentsWithReference( + .batchERC20Payments( token1Address, [payee1, payee2, payee3], [20, 30, 40], @@ -477,7 +485,7 @@ describe('contract: BatchPayments: ERC20', () => { [1, 2, 3], feeAddress, ), - ).revertedWith('revert Not sufficient allowance for batch to pay'); + ).revertedWith('Insufficient allowance for batch to pay'); }); it('Should revert batch multi tokens if not enough funds', async function () { @@ -487,7 +495,7 @@ describe('contract: BatchPayments: ERC20', () => { await expect( batch .connect(spender3) - .batchERC20PaymentsMultiTokensWithReference( + .batchMultiERC20Payments( [token1Address, token1Address, token1Address], [payee1, payee2, payee3], [5, 30, 400], @@ -495,7 +503,7 @@ describe('contract: BatchPayments: ERC20', () => { [1, 2, 3], feeAddress, ), - ).revertedWith('revert not enough funds'); + ).revertedWith('not enough funds'); }); it('Should revert batch multi tokens if not enough funds to pay the batch fee', async function () { @@ -505,7 +513,7 @@ describe('contract: BatchPayments: ERC20', () => { await expect( batch .connect(spender3) - .batchERC20PaymentsMultiTokensWithReference( + .batchMultiERC20Payments( [token1Address, token1Address, token1Address], [payee1, payee2, payee2], [100, 200, 300], @@ -513,7 +521,7 @@ describe('contract: BatchPayments: ERC20', () => { [1, 2, 3], feeAddress, ), - ).revertedWith('revert not enough funds'); + ).revertedWith('not enough funds'); }); it('Should revert batch multi tokens without approval', async function () { @@ -523,7 +531,7 @@ describe('contract: BatchPayments: ERC20', () => { await expect( batch .connect(spender3) - .batchERC20PaymentsMultiTokensWithReference( + .batchMultiERC20Payments( [token1Address, token1Address, token1Address], [payee1, payee2, payee3], [100, 200, 300], @@ -531,14 +539,14 @@ describe('contract: BatchPayments: ERC20', () => { [1, 2, 3], feeAddress, ), - ).revertedWith('revert Not sufficient allowance for batch to pay'); + ).revertedWith('Insufficient allowance for batch to pay'); }); it('Should revert batch multi tokens if input s arrays do not have same size', async function () { await expect( batch .connect(spender3) - .batchERC20PaymentsMultiTokensWithReference( + .batchMultiERC20Payments( [token1Address, token1Address], [payee1, payee2, payee3], [5, 30, 40], @@ -551,7 +559,7 @@ describe('contract: BatchPayments: ERC20', () => { await expect( batch .connect(spender3) - .batchERC20PaymentsMultiTokensWithReference( + .batchMultiERC20Payments( [token1Address, token1Address, token1Address], [payee1, payee2], [5, 30, 40], @@ -564,7 +572,7 @@ describe('contract: BatchPayments: ERC20', () => { await expect( batch .connect(spender3) - .batchERC20PaymentsMultiTokensWithReference( + .batchMultiERC20Payments( [token1Address, token1Address, token1Address], [payee1, payee2, payee3], [5, 30], @@ -577,7 +585,7 @@ describe('contract: BatchPayments: ERC20', () => { await expect( batch .connect(spender3) - .batchERC20PaymentsMultiTokensWithReference( + .batchMultiERC20Payments( [token1Address, token1Address, token1Address], [payee1, payee2, payee3], [5, 30, 40], @@ -590,7 +598,7 @@ describe('contract: BatchPayments: ERC20', () => { await expect( batch .connect(spender3) - .batchERC20PaymentsMultiTokensWithReference( + .batchMultiERC20Payments( [token1Address, token1Address, token1Address], [payee1, payee2, payee3], [5, 30, 40], @@ -605,7 +613,7 @@ describe('contract: BatchPayments: ERC20', () => { await expect( batch .connect(spender3) - .batchERC20PaymentsWithReference( + .batchERC20Payments( token1Address, [payee1, payee2, payee3], [5, 30, 40], @@ -618,7 +626,7 @@ describe('contract: BatchPayments: ERC20', () => { await expect( batch .connect(spender3) - .batchERC20PaymentsWithReference( + .batchERC20Payments( token1Address, [payee1, payee2], [5, 30, 40], @@ -631,7 +639,7 @@ describe('contract: BatchPayments: ERC20', () => { await expect( batch .connect(spender3) - .batchERC20PaymentsWithReference( + .batchERC20Payments( token1Address, [payee1, payee2, payee3], [5, 30], @@ -644,7 +652,7 @@ describe('contract: BatchPayments: ERC20', () => { await expect( batch .connect(spender3) - .batchERC20PaymentsWithReference( + .batchERC20Payments( token1Address, [payee1, payee2, payee3], [5, 30, 40], @@ -657,7 +665,7 @@ describe('contract: BatchPayments: ERC20', () => { }); }); -// Allow to create easly BatchPayments input, especially for gas optimization +// Allow to create easly batchNoConversionPayments input, especially for gas optimization const getBatchPaymentsInputs = function ( nbTxs: number, tokenAddress: string, diff --git a/packages/smart-contracts/test/contracts/BatchEthPayments.test.ts b/packages/smart-contracts/test/contracts/BatchNoConversionEthPayments.test.ts similarity index 85% rename from packages/smart-contracts/test/contracts/BatchEthPayments.test.ts rename to packages/smart-contracts/test/contracts/BatchNoConversionEthPayments.test.ts index 1f2fc2d02..e3b3cb4ea 100644 --- a/packages/smart-contracts/test/contracts/BatchEthPayments.test.ts +++ b/packages/smart-contracts/test/contracts/BatchNoConversionEthPayments.test.ts @@ -1,15 +1,17 @@ import { ethers, network } from 'hardhat'; import { BigNumber, Signer } from 'ethers'; import { expect } from 'chai'; -import { EthereumFeeProxy, BatchPayments } from '../../src/types'; -import { batchPaymentsArtifact } from '../../src/lib'; - -import { ethereumFeeProxyArtifact } from '../../src/lib'; +import { + EthereumFeeProxy__factory, + BatchNoConversionPayments__factory, + ERC20FeeProxy__factory, +} from '../../src/types'; +import { EthereumFeeProxy, BatchNoConversionPayments } from '../../src/types'; import { HttpNetworkConfig } from 'hardhat/types'; const logGasInfos = false; -describe('contract: BatchPayments: Ethereum', () => { +describe('contract: batchNoConversionPayments: Ethereum', () => { let payee1: string; let payee2: string; let feeAddress: string; @@ -27,7 +29,7 @@ describe('contract: BatchPayments: Ethereum', () => { const referenceExample2 = '0xbbbb'; let ethFeeProxy: EthereumFeeProxy; - let batch: BatchPayments; + let batch: BatchNoConversionPayments; const networkConfig = network.config as HttpNetworkConfig; const provider = new ethers.providers.JsonRpcProvider(networkConfig.url); @@ -35,14 +37,15 @@ describe('contract: BatchPayments: Ethereum', () => { [, payee1, payee2, feeAddress] = (await ethers.getSigners()).map((s) => s.address); [owner, payee1Sig] = await ethers.getSigners(); - ethFeeProxy = ethereumFeeProxyArtifact.connect(network.name, owner); - batch = batchPaymentsArtifact.connect(network.name, owner); + const erc20FeeProxy = await new ERC20FeeProxy__factory(owner).deploy(); + ethFeeProxy = await new EthereumFeeProxy__factory(owner).deploy(); + batch = await new BatchNoConversionPayments__factory(owner).deploy( + erc20FeeProxy.address, + ethFeeProxy.address, + await owner.getAddress(), + ); batchAddress = batch.address; - await batch.connect(owner).setBatchFee(10); - }); - - after(async () => { - await batch.connect(owner).setBatchFee(10); + await batch.connect(owner).setBatchFee(100); }); describe('Batch Eth normal flow', () => { @@ -54,7 +57,7 @@ describe('contract: BatchPayments: Ethereum', () => { await expect( batch .connect(owner) - .batchEthPaymentsWithReference( + .batchEthPayments( [payee1, payee2], [2000, 3000], [referenceExample1, referenceExample2], @@ -90,7 +93,7 @@ describe('contract: BatchPayments: Ethereum', () => { const tx = await batch .connect(owner) - .batchEthPaymentsWithReference( + .batchEthPayments( [payee1, payee2], [200, 300], [referenceExample1, referenceExample2], @@ -129,16 +132,9 @@ describe('contract: BatchPayments: Ethereum', () => { const tx = await batch .connect(owner) - .batchEthPaymentsWithReference( - recipients, - amounts, - paymentReferences, - feeAmounts, - feeAddress, - { - value: totalAmount, - }, - ); + .batchEthPayments(recipients, amounts, paymentReferences, feeAmounts, feeAddress, { + value: totalAmount, + }); const receipt = await tx.wait(); if (logGasInfos) { @@ -162,7 +158,7 @@ describe('contract: BatchPayments: Ethereum', () => { await expect( batch .connect(owner) - .batchEthPaymentsWithReference( + .batchEthPayments( [payee1, payee2], [200, 300], [referenceExample1, referenceExample2], @@ -192,7 +188,7 @@ describe('contract: BatchPayments: Ethereum', () => { await expect( batch .connect(owner) - .batchEthPaymentsWithReference( + .batchEthPayments( [payee1, payee2], [200, 300], [referenceExample1, referenceExample2], @@ -217,7 +213,7 @@ describe('contract: BatchPayments: Ethereum', () => { await expect( batch .connect(owner) - .batchEthPaymentsWithReference( + .batchEthPayments( [payee1, payee2], [5, 30], [referenceExample1, referenceExample2], @@ -229,7 +225,7 @@ describe('contract: BatchPayments: Ethereum', () => { await expect( batch .connect(owner) - .batchEthPaymentsWithReference( + .batchEthPayments( [payee1], [5, 30], [referenceExample1, referenceExample2], @@ -241,7 +237,7 @@ describe('contract: BatchPayments: Ethereum', () => { await expect( batch .connect(owner) - .batchEthPaymentsWithReference( + .batchEthPayments( [payee1, payee2], [5], [referenceExample1, referenceExample2], @@ -253,13 +249,7 @@ describe('contract: BatchPayments: Ethereum', () => { await expect( batch .connect(owner) - .batchEthPaymentsWithReference( - [payee1, payee2], - [5, 30], - [referenceExample1], - [1, 2], - feeAddress, - ), + .batchEthPayments([payee1, payee2], [5, 30], [referenceExample1], [1, 2], feeAddress), ).revertedWith('the input arrays must have the same length'); expect(await provider.getBalance(batchAddress)).to.be.equal(0); @@ -269,10 +259,10 @@ describe('contract: BatchPayments: Ethereum', () => { describe('Function allowed only to the owner', () => { it('Should allow the owner to update batchFee', async function () { const beforeBatchFee = await batch.batchFee.call({ from: owner }); - let tx = await batch.connect(owner).setBatchFee(beforeBatchFee.add(10)); + let tx = await batch.connect(owner).setBatchFee(beforeBatchFee.add(100)); await tx.wait(); const afterBatchFee = await batch.batchFee.call({ from: owner }); - expect(afterBatchFee).to.be.equal(beforeBatchFee.add(10)); + expect(afterBatchFee).to.be.equal(beforeBatchFee.add(100)); }); it('Should applied the new batchFee', async function () { @@ -281,7 +271,7 @@ describe('contract: BatchPayments: Ethereum', () => { const tx = await batch .connect(owner) - .batchEthPaymentsWithReference( + .batchEthPayments( [payee1, payee2], [200, 300], [referenceExample1, referenceExample2], @@ -298,14 +288,14 @@ describe('contract: BatchPayments: Ethereum', () => { }); it('Should revert if it is not the owner that try to update batchFee', async function () { - await expect(batch.connect(payee1Sig).setBatchFee(30)).revertedWith( - 'revert Ownable: caller is not the owner', + await expect(batch.connect(payee1Sig).setBatchFee(300)).revertedWith( + 'Ownable: caller is not the owner', ); }); }); }); -// Allow to create easly BatchPayments input, especially for gas optimization. +// Allow to create easly batchNoConversionPayments input, especially for gas optimization. const getBatchPaymentsInputs = function ( nbTxs: number, tokenAddress: string, diff --git a/packages/smart-contracts/test/contracts/localArtifacts.ts b/packages/smart-contracts/test/contracts/localArtifacts.ts index f87e518d2..ab8075efe 100644 --- a/packages/smart-contracts/test/contracts/localArtifacts.ts +++ b/packages/smart-contracts/test/contracts/localArtifacts.ts @@ -22,7 +22,7 @@ export const secondLocalERC20AlphaArtifact = new ContractArtifact( abi: [], deployment: { private: { - address: '0xe4e47451AAd6C89a6D9E4aD104A7b77FfE1D3b36', + address: '0x5034F49b27353CeDc562b49eA91C7438Ea351d36', creationBlockNumber: 0, }, }, From 9439c7d682bbfc605a23b629470d838acea1e3a6 Mon Sep 17 00:00:00 2001 From: olivier7delf <55892112+olivier7delf@users.noreply.github.com> Date: Fri, 2 Sep 2022 12:08:29 +0200 Subject: [PATCH 085/105] abi update prettier contract comments deploy and addresses --- packages/smart-contracts/hardhat.config.ts | 1 - .../scripts/test-deploy-all.ts | 2 + ...test-deploy-batch-conversion-deployment.ts | 2 +- .../contracts/BatchNoConversionPayments.sol | 3 +- .../BatchConversionPayments/index.ts | 2 +- .../BatchNoConversionPayments/0.1.0.json | 81 ++++++++----------- .../test/contracts/localArtifacts.ts | 2 +- 7 files changed, 41 insertions(+), 52 deletions(-) diff --git a/packages/smart-contracts/hardhat.config.ts b/packages/smart-contracts/hardhat.config.ts index dc8b444cb..fc0dcc591 100644 --- a/packages/smart-contracts/hardhat.config.ts +++ b/packages/smart-contracts/hardhat.config.ts @@ -59,7 +59,6 @@ export default { private: { url: 'http://127.0.0.1:8545', accounts: undefined, - setTimeout: 60000, }, mainnet: { url: process.env.WEB3_PROVIDER_URL || 'https://mainnet.infura.io/v3/YOUR_API_KEY', diff --git a/packages/smart-contracts/scripts/test-deploy-all.ts b/packages/smart-contracts/scripts/test-deploy-all.ts index bf0e40e13..392f32525 100644 --- a/packages/smart-contracts/scripts/test-deploy-all.ts +++ b/packages/smart-contracts/scripts/test-deploy-all.ts @@ -3,6 +3,7 @@ import deployRequest from './test-deploy-request-storage'; import deployPayment from './test-deploy-main-payments'; import deployConversion from './test-deploy_chainlink_contract'; import { deployEscrow } from './test-deploy-escrow-deployment'; +import { deployBatchPayment } from './test-deploy-batch-erc-eth-deployment'; import { deploySuperFluid } from './test-deploy-superfluid'; import { deployBatchConversionPayment } from './test-deploy-batch-conversion-deployment'; @@ -12,6 +13,7 @@ export default async function deploy(_args: any, hre: HardhatRuntimeEnvironment) const mainPaymentAddresses = await deployPayment(_args, hre); await deployConversion(_args, hre, mainPaymentAddresses); await deployEscrow(hre); + await deployBatchPayment(_args, hre); await deploySuperFluid(hre); await deployBatchConversionPayment(_args, hre); } diff --git a/packages/smart-contracts/scripts/test-deploy-batch-conversion-deployment.ts b/packages/smart-contracts/scripts/test-deploy-batch-conversion-deployment.ts index 839b7110b..7901a43c2 100644 --- a/packages/smart-contracts/scripts/test-deploy-batch-conversion-deployment.ts +++ b/packages/smart-contracts/scripts/test-deploy-batch-conversion-deployment.ts @@ -60,7 +60,7 @@ export async function deployBatchConversionPayment( // Check the addresses of our contracts, to avoid misleading bugs in the tests // ref to secondLocalERC20AlphaArtifact.getAddress('private'), that cannot be used in deployment - const fakeFAU_addressExpected = '0x5034F49b27353CeDc562b49eA91C7438Ea351d36'; + const fakeFAU_addressExpected = '0xe4e47451AAd6C89a6D9E4aD104A7b77FfE1D3b36'; deployAddressChecking('testERC20FakeFAU', testERC20FakeFAU.address, fakeFAU_addressExpected); deployAddressChecking( 'batchConversionPayments', diff --git a/packages/smart-contracts/src/contracts/BatchNoConversionPayments.sol b/packages/smart-contracts/src/contracts/BatchNoConversionPayments.sol index 343b4d7b6..5516ac23f 100644 --- a/packages/smart-contracts/src/contracts/BatchNoConversionPayments.sol +++ b/packages/smart-contracts/src/contracts/BatchNoConversionPayments.sol @@ -33,7 +33,8 @@ contract BatchNoConversionPayments is Ownable { // payerAuthorized is set to true only when needed for batch Eth conversion bool internal payerAuthorized; - // transferBackRemainingEth is set to false only if the payer use batchRouter and call both batchEthPayments and batchConversionEthPaymentsWithReference + // transferBackRemainingEth is set to false only if the payer use batchRouter + // and call both batchEthPayments and batchConversionEthPaymentsWithReference bool internal transferBackRemainingEth = true; struct Token { diff --git a/packages/smart-contracts/src/lib/artifacts/BatchConversionPayments/index.ts b/packages/smart-contracts/src/lib/artifacts/BatchConversionPayments/index.ts index aed8e0166..d83849d90 100644 --- a/packages/smart-contracts/src/lib/artifacts/BatchConversionPayments/index.ts +++ b/packages/smart-contracts/src/lib/artifacts/BatchConversionPayments/index.ts @@ -10,7 +10,7 @@ export const batchConversionPaymentsArtifact = new ContractArtifact( abi: [], deployment: { private: { - address: '0x5034F49b27353CeDc562b49eA91C7438Ea351d36', + address: '0xe4e47451AAd6C89a6D9E4aD104A7b77FfE1D3b36', creationBlockNumber: 0, }, }, From bc407d069a357a3a4d650889f202de1e2d641264 Mon Sep 17 00:00:00 2001 From: olivier7delf <55892112+olivier7delf@users.noreply.github.com> Date: Fri, 2 Sep 2022 17:10:39 +0200 Subject: [PATCH 086/105] batch conv tests delete proxy global variables --- .../test/contracts/BatchConversionPayments.test.ts | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/packages/smart-contracts/test/contracts/BatchConversionPayments.test.ts b/packages/smart-contracts/test/contracts/BatchConversionPayments.test.ts index 4d9e31941..e2784ce07 100644 --- a/packages/smart-contracts/test/contracts/BatchConversionPayments.test.ts +++ b/packages/smart-contracts/test/contracts/BatchConversionPayments.test.ts @@ -6,8 +6,6 @@ import { EthereumFeeProxy__factory, ChainlinkConversionPath, TestERC20, - Erc20ConversionProxy, - EthConversionProxy, TestERC20__factory, BatchConversionPayments__factory, BatchConversionPayments, @@ -56,8 +54,6 @@ describe('contract: BatchConversionPayments', async () => { let DAI_address: string; let FAU_address: string; - let erc20ConversionProxy: Erc20ConversionProxy; - let ethConversionProxy: EthConversionProxy; let batchConversionProxy: BatchConversionPayments; let daiERC20: TestERC20; let fauERC20: TestERC20; @@ -306,12 +302,12 @@ describe('contract: BatchConversionPayments', async () => { const erc20FeeProxy = await new ERC20FeeProxy__factory(adminSigner).deploy(); const ethFeeProxy = await new EthereumFeeProxy__factory(adminSigner).deploy(); - erc20ConversionProxy = await new Erc20ConversionProxy__factory(adminSigner).deploy( + const erc20ConversionProxy = await new Erc20ConversionProxy__factory(adminSigner).deploy( erc20FeeProxy.address, chainlinkPath.address, await adminSigner.getAddress(), ); - ethConversionProxy = await new EthConversionProxy__factory(adminSigner).deploy( + const ethConversionProxy = await new EthConversionProxy__factory(adminSigner).deploy( ethFeeProxy.address, chainlinkPath.address, ETH_hash, From 345996639541d7caeef4cc21b5b6514891266aff Mon Sep 17 00:00:00 2001 From: olivier7delf <55892112+olivier7delf@users.noreply.github.com> Date: Tue, 6 Sep 2022 20:42:15 +0200 Subject: [PATCH 087/105] test refactored --- ...test-deploy-batch-conversion-deployment.ts | 5 +- .../contracts/BatchConversionPayments.test.ts | 937 +++++++++--------- 2 files changed, 497 insertions(+), 445 deletions(-) diff --git a/packages/smart-contracts/scripts/test-deploy-batch-conversion-deployment.ts b/packages/smart-contracts/scripts/test-deploy-batch-conversion-deployment.ts index 7901a43c2..27c15d0b9 100644 --- a/packages/smart-contracts/scripts/test-deploy-batch-conversion-deployment.ts +++ b/packages/smart-contracts/scripts/test-deploy-batch-conversion-deployment.ts @@ -12,6 +12,9 @@ import { import { chainlinkConversionPath as chainlinkConvArtifact } from '../src/lib'; import { CurrencyManager } from '@requestnetwork/currency'; import { deployAddressChecking } from './utils'; +import { BigNumber } from 'ethers'; + +export const FAU_USD_RATE = 201; // 2.01 // Deploys, set up the contracts export async function deployBatchConversionPayment( @@ -47,7 +50,7 @@ export async function deployBatchConversionPayment( const erc20Factory = await hre.ethers.getContractFactory('TestERC20'); const testERC20FakeFAU = await erc20Factory.deploy('1000000000000000000000000000000'); const { address: AggFakeFAU_USD_address } = await deployOne(args, hre, 'AggregatorMock', { - constructorArguments: [201000000, 8, 60], + constructorArguments: [BigNumber.from(FAU_USD_RATE).mul(1000000), 8, 60], }); const conversionPathInstance = chainlinkConvArtifact.connect('private', owner); const currencyManager = CurrencyManager.getDefault(); diff --git a/packages/smart-contracts/test/contracts/BatchConversionPayments.test.ts b/packages/smart-contracts/test/contracts/BatchConversionPayments.test.ts index e2784ce07..c5cacecf3 100644 --- a/packages/smart-contracts/test/contracts/BatchConversionPayments.test.ts +++ b/packages/smart-contracts/test/contracts/BatchConversionPayments.test.ts @@ -14,6 +14,7 @@ import { BigNumber, ContractTransaction, Signer } from 'ethers'; import { expect } from 'chai'; import { CurrencyManager } from '@requestnetwork/currency'; import { chainlinkConversionPath } from '../../src/lib'; +import { FAU_USD_RATE } from '../../scripts/test-deploy-batch-conversion-deployment'; import { localERC20AlphaArtifact, secondLocalERC20AlphaArtifact } from './localArtifacts'; import Utils from '@requestnetwork/utils'; import { HttpNetworkConfig } from 'hardhat/types'; @@ -26,24 +27,20 @@ describe('contract: BatchConversionPayments', async () => { let to: string; let feeAddress: string; let adminSigner: Signer; - let signer1: Signer; + let fromSigner: Signer; let signer4: Signer; let tx: ContractTransaction; // constants used to set up batch conversion proxy, and also requests payment - const batchFee = 50; - const batchConvFee = 100; + const BATCH_FEE = 50; + const BATCH_CONV_FEE = 100; // 1% + const BATCH_DENOMINATOR = 10000; + const daiDecimals = '1000000000000000000'; + const millionDai = daiDecimals + '000000'; + const fiatDecimals = '00000000'; const thousandWith18Decimal = '1000000000000000000000'; const referenceExample = '0xaaaa'; - /** - * amount and feeAmount are in: - * - EUR, or USD for conversion inputs - * - DAI for non-conversion ERC20 inputs - * - ETH for non-conversion ETH inputs - */ - const amount = BigNumber.from(100000); - const feeAmount = amount.div(1000); // constants and variables to set up proxies and paths const currencyManager = CurrencyManager.getDefault(); @@ -51,40 +48,17 @@ describe('contract: BatchConversionPayments', async () => { 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 FAU_address: string; + const DAI_address = localERC20AlphaArtifact.getAddress(network.name); + const FAU_address = secondLocalERC20AlphaArtifact.getAddress(network.name); + + const EUR_USD = 120; // const eurDaiRate = 1.2 / 1.01; + const DAI_USD = 101; let batchConversionProxy: BatchConversionPayments; let daiERC20: TestERC20; let fauERC20: TestERC20; let chainlinkPath: ChainlinkConversionPath; - // variables used to check daiERC20 balances (1st token) - let fromOldBalance1: BigNumber; - let toOldBalance1: BigNumber; - let feeOldBalance1: BigNumber; - - let fromDiffBalanceExpected1: BigNumber; - let toDiffBalanceExpected1: BigNumber; - let feeDiffBalanceExpected1: BigNumber; - - // variables used to check ETH balances - let beforeEthBalanceTo: BigNumber; - let beforeEthBalanceFee: BigNumber; - let beforeEthBalance: BigNumber; - - // variables used for chainlink and conversion payments - let conversionToPay: BigNumber; - let conversionFees: BigNumber; - - // variables used for Eth conversion payments, and also as expected value - let ethConversionToPay: BigNumber; - let ethConversionFee: BigNumber; - - // type required by Erc20 conversion batch function inputs - let convDetail: any; - let ethConvDetail: any; - const emptyCryptoDetails = { tokenAddresses: [], recipients: [], @@ -93,212 +67,41 @@ describe('contract: BatchConversionPayments', async () => { feeAmounts: [], }; - /** - * @notice it sets the conversions including fees to be paid, and it set the convDetail input - * @dev it update 3 global variables: conversionToPay, conversionFees, and convDetail - */ - const setConvToPayAndConvDetail = async ( - recipient: string, - path: string[], - requestAmount: string, - feeAmount: string, - maxRateTimespan: number, - chainlinkPath: ChainlinkConversionPath, - ) => { - conversionToPay = (await chainlinkPath.getConversion(requestAmount, path)).result; - conversionFees = (await chainlinkPath.getConversion(feeAmount, path)).result; - convDetail = { - recipient: recipient, - requestAmount: requestAmount, - path: path, - paymentReference: referenceExample, - feeAmount: feeAmount, - maxToSpend: conversionToPay.add(conversionFees).toString(), - maxRateTimespan: maxRateTimespan, - }; + const fauConvDetail = { + recipient: '', + requestAmount: '100000' + fiatDecimals, + path: [USD_hash, FAU_address], + paymentReference: referenceExample, + feeAmount: '100' + fiatDecimals, + maxToSpend: '20000000000000000000' + fiatDecimals, // Way enough + maxRateTimespan: '0', }; - /** - * check token ERC20 balances of: the payer (from), the recipient (to), the feeAddress, and the batch contract - */ - const checkBalancesForOneToken = async ( - testERC20: TestERC20, - fromOldBalance: BigNumber, - toOldBalance: BigNumber, - feeOldBalance: BigNumber, - fromDiffBalanceExpected: BigNumber, - toDiffBalanceExpected: BigNumber, - feeDiffBalanceExpected: BigNumber, - ) => { - // Get balances - const fromBalance = await testERC20.balanceOf(from); - const toBalance = await testERC20.balanceOf(to); - const feeBalance = await testERC20.balanceOf(feeAddress); - const batchBalance = await testERC20.balanceOf(batchConversionProxy.address); - - // Calculate the difference of the balance : now - before - const fromDiffBalance = BigNumber.from(fromBalance).sub(fromOldBalance); - const toDiffBalance = BigNumber.from(toBalance).sub(toOldBalance); - const feeDiffBalance = BigNumber.from(feeBalance).sub(feeOldBalance); - // Check balance changes - expect(fromDiffBalance).to.equals(fromDiffBalanceExpected, 'fromDiffBalance'); - expect(toDiffBalance).to.equals(toDiffBalanceExpected, 'toDiffBalance'); - expect(feeDiffBalance).to.equals(feeDiffBalanceExpected, 'feeDiffBalance'); - expect(batchBalance).to.equals('0', 'batchBalance'); + const daiConvDetail = { + recipient: '', + requestAmount: '100000' + fiatDecimals, + path: [EUR_hash, USD_hash, DAI_address], + paymentReference: referenceExample, + feeAmount: '100' + fiatDecimals, + maxToSpend: '30000000000000000000' + fiatDecimals, // Way enough + maxRateTimespan: '0', }; - /** - * @notice Used to calculate the expected new ERC20 balance of a single token for batch conversion. - * @dev fees are not exactly calculated with the same formula, depending if it is with conversion or not - */ - const expectedERC20Balances = ( - conversionToPay_results: BigNumber[], - conversionFees_results: BigNumber[], - appliedFees: number, - withConversion = true, - ) => { - let fromDiffBalanceExpected = conversionToPay_results.reduce( - (prev, x) => prev.sub(x), - BigNumber.from(0), - ); - let toDiffBalanceExpected = fromDiffBalanceExpected.mul(-1); - let feeDiffBalanceExpected = conversionFees_results.reduce( - (prev, x) => prev.add(x), - BigNumber.from(0), - ); - - feeDiffBalanceExpected = withConversion - ? toDiffBalanceExpected - .add(feeDiffBalanceExpected) - .mul(appliedFees) - .div(10000) - .add(feeDiffBalanceExpected) - : toDiffBalanceExpected.mul(appliedFees).div(10000).add(feeDiffBalanceExpected); - - fromDiffBalanceExpected = fromDiffBalanceExpected.sub(feeDiffBalanceExpected); - return [fromDiffBalanceExpected, toDiffBalanceExpected, feeDiffBalanceExpected]; - }; - - /** - * Pays 3 ERC20 conversions payments, with DAI and FAU tokens and it calculates the balances - * It also check the balances expected for FAU token. - * @param path2 to update the copy of convDetail: convDetail2 - */ - const manyPaymentsBatchConv = async ( - path1: string[], - path2: string[], - withBatchRouter = false, - ) => { - // set convDetail with "path1" - await setConvToPayAndConvDetail( - to, - path1, - amount.toString(), - feeAmount.toString(), - 0, - chainlinkPath, - ); - // define a second payment request - const conversionToPay2 = (await chainlinkPath.getConversion(amount.toString(), path2)).result; - const conversionFees2 = (await chainlinkPath.getConversion(feeAmount.toString(), path2)).result; - const convDetail2 = Utils.deepCopy(convDetail); - convDetail2.path = path2; - convDetail2.maxToSpend = conversionToPay2.add(conversionFees2).toString(); - - // define conversionsToPays & conversionsFees to calculate the expected balances - const conversionsToPays = [conversionToPay, conversionToPay, conversionToPay2]; - const conversionsFees = [conversionFees, conversionFees, conversionFees2]; - - // get balances of the 2nd token, useful when there are 2 different tokens used - const fromOldBalance2 = await fauERC20.balanceOf(from); - const toOldBalance2 = await fauERC20.balanceOf(to); - const feeOldBalance2 = await fauERC20.balanceOf(feeAddress); - - if (withBatchRouter) { - await batchConversionProxy.batchRouter( - [ - { - paymentNetworkId: '0', - conversionDetails: [convDetail, convDetail, convDetail2], - cryptoDetails: emptyCryptoDetails, - }, - ], - feeAddress, - ); - } else { - await batchConversionProxy.batchMultiERC20ConversionPayments( - [convDetail, convDetail, convDetail2], - feeAddress, - ); - } - - // 1st token: daiERC20 - calculate the expected balances - [fromDiffBalanceExpected1, toDiffBalanceExpected1, feeDiffBalanceExpected1] = - expectedERC20Balances( - conversionsToPays.slice(0, 2), - conversionsFees.slice(0, 2), - batchConvFee, - ); - - // 2nd token: fauERC20 - calculate the expected balances - const [fromDiffBalanceExpected2, toDiffBalanceExpected2, feeDiffBalanceExpected2] = - expectedERC20Balances( - conversionsToPays.slice(2, 3), - conversionsFees.slice(2, 3), - batchConvFee, - ); - - // check the balance of the 2nd token, which is not checked in "afterEach" contrary to the 1st token. - checkBalancesForOneToken( - fauERC20, - fromOldBalance2, - toOldBalance2, - feeOldBalance2, - fromDiffBalanceExpected2, - toDiffBalanceExpected2, - feeDiffBalanceExpected2, - ); - }; - - /** - * Gets the balances, calculates the difference between "before" and "after" and raise an error if needed - * @param ethAmount the amount of ETH to pay - * @param ethFeeAmount the fee amount of ETH to pay, before to apply batch fees - * @param feeApplied the batch fees to apply: batchConvFee, or batchFee - */ - const checkEthBalances = async ( - ethAmount: BigNumber, - ethFeeAmount: BigNumber, - feeApplied = batchConvFee, - ) => { - const receipt = await tx.wait(); - const gasUsed = receipt.gasUsed.mul(2 * 10 ** 10); - - const afterEthBalance = await provider.getBalance(await signer1.getAddress()); - const afterEthBalanceTo = await provider.getBalance(to); - const afterEthBalanceFee = await provider.getBalance(feeAddress); - const proxyBalance = await provider.getBalance(batchConversionProxy.address); - - // Calculate the difference of the balance : now - before - const diffBalance = beforeEthBalance.sub(afterEthBalance); - const diffBalanceTo = afterEthBalanceTo.sub(beforeEthBalanceTo); - const diffBalanceFee = afterEthBalanceFee.sub(beforeEthBalanceFee); - - // ethFeeAmount includes batch conversion fees now - ethFeeAmount = ethAmount.add(ethFeeAmount).mul(feeApplied).div(10000).add(ethFeeAmount); - const diffBalanceExpect = gasUsed.add(ethAmount).add(ethFeeAmount); - // Check balance changes - expect(diffBalance).to.equals(diffBalanceExpect, 'DiffBalance'); - expect(diffBalanceTo).to.equals(ethAmount, 'diffBalanceTo'); - expect(diffBalanceFee).to.equals(ethFeeAmount, 'diffBalanceFee'); - expect(proxyBalance).to.equals('0', 'proxyBalance'); + const ethConvDetail = { + recipient: '', + requestAmount: '1000', + path: [USD_hash, ETH_hash], + paymentReference: referenceExample, + feeAmount: '1', + maxToSpend: BigNumber.from(0), + maxRateTimespan: BigNumber.from(0), }; before(async () => { [, from, to, feeAddress] = (await ethers.getSigners()).map((s) => s.address); - [adminSigner, signer1, , , signer4] = await ethers.getSigners(); + [adminSigner, fromSigner, , , signer4] = await ethers.getSigners(); - chainlinkPath = chainlinkConversionPath.connect(network.name, signer1); + chainlinkPath = chainlinkConversionPath.connect(network.name, fromSigner); const erc20FeeProxy = await new ERC20FeeProxy__factory(adminSigner).deploy(); const ethFeeProxy = await new EthereumFeeProxy__factory(adminSigner).deploy(); @@ -321,139 +124,283 @@ describe('contract: BatchConversionPayments', async () => { await adminSigner.getAddress(), ); - // set batch proxy fees and connect signer1 - await batchConversionProxy.setBatchFee(batchFee); - await batchConversionProxy.setBatchConversionFee(batchConvFee); - batchConversionProxy = batchConversionProxy.connect(signer1); + fauConvDetail.recipient = to; + daiConvDetail.recipient = to; + ethConvDetail.recipient = to; + + // set batch proxy fees and connect fromSigner + await batchConversionProxy.setBatchFee(BATCH_FEE); + await batchConversionProxy.setBatchConversionFee(BATCH_CONV_FEE); + batchConversionProxy = batchConversionProxy.connect(fromSigner); - // set ERC20 tokens and transfer token to "from" (signer1) - DAI_address = localERC20AlphaArtifact.getAddress(network.name); + // set ERC20 tokens and transfer token to "from" (fromSigner) daiERC20 = new TestERC20__factory(adminSigner).attach(DAI_address); - await daiERC20.transfer(from, BigNumber.from(thousandWith18Decimal)); - daiERC20 = daiERC20.connect(signer1); + await daiERC20.transfer(from, BigNumber.from(thousandWith18Decimal + '0000000')); + daiERC20 = daiERC20.connect(fromSigner); - FAU_address = secondLocalERC20AlphaArtifact.getAddress(network.name); fauERC20 = new TestERC20__factory(adminSigner).attach(FAU_address); - await fauERC20.transfer(from, BigNumber.from(thousandWith18Decimal)); - fauERC20 = fauERC20.connect(signer1); - }); + await fauERC20.transfer(from, BigNumber.from(thousandWith18Decimal + '0000000')); + fauERC20 = fauERC20.connect(fromSigner); - beforeEach(async () => { - fromDiffBalanceExpected1 = BigNumber.from(0); - toDiffBalanceExpected1 = BigNumber.from(0); - feeDiffBalanceExpected1 = BigNumber.from(0); - await daiERC20.approve(batchConversionProxy.address, thousandWith18Decimal, { + await daiERC20.approve(batchConversionProxy.address, thousandWith18Decimal + fiatDecimals, { from, }); - await fauERC20.approve(batchConversionProxy.address, thousandWith18Decimal, { + await fauERC20.approve(batchConversionProxy.address, thousandWith18Decimal + fiatDecimals, { from, }); - // get balances of daiERC20 token - fromOldBalance1 = await daiERC20.balanceOf(from); - toOldBalance1 = await daiERC20.balanceOf(to); - feeOldBalance1 = await daiERC20.balanceOf(feeAddress); - - // create a default ERC20 convDetail - setConvToPayAndConvDetail( - to, - [EUR_hash, USD_hash, DAI_address], - amount.toString(), - feeAmount.toString(), - 0, - chainlinkPath, - ); - - // get Eth balances - beforeEthBalanceTo = await provider.getBalance(to); - beforeEthBalanceFee = await provider.getBalance(feeAddress); - beforeEthBalance = await provider.getBalance(await signer1.getAddress()); - - ethConvDetail = { - recipient: to, - requestAmount: amount, - path: [USD_hash, ETH_hash], - paymentReference: referenceExample, - feeAmount: feeAmount, - maxToSpend: BigNumber.from(0), - maxRateTimespan: BigNumber.from(0), - }; - - // expected balances, it can be modified for each test - ethConversionToPay = ( - await chainlinkPath.getConversion(ethConvDetail.requestAmount, ethConvDetail.path) - ).result; - // fees does not include batch fees yet - ethConversionFee = ( - await chainlinkPath.getConversion(ethConvDetail.feeAmount, ethConvDetail.path) - ).result; }); - afterEach(async () => { - // check balances of daiERC20 token - checkBalancesForOneToken( - daiERC20, - fromOldBalance1, - toOldBalance1, - feeOldBalance1, - fromDiffBalanceExpected1, - toDiffBalanceExpected1, - feeDiffBalanceExpected1, + const getERC20Balances = async (testERC20: TestERC20) => { + const fromDAIBalance = await testERC20.balanceOf(from); + const toDAIBalance = await testERC20.balanceOf(to); + const feeDAIBalance = await testERC20.balanceOf(feeAddress); + const batchDAIBalance = await testERC20.balanceOf(batchConversionProxy.address); + return [fromDAIBalance, toDAIBalance, feeDAIBalance, batchDAIBalance]; + }; + + const getExpectedConvERC20Balances = ( + amount: number, + fee: number, + nPayment: number, + path: string, + ) => { + // to get the exact result, we use millionDai + const conversionRate = + path === 'EUR_DAI' + ? BigNumber.from(millionDai).mul(EUR_USD).div(DAI_USD) + : BigNumber.from(millionDai).mul(100).div(FAU_USD_RATE); + const expectedToDAIBalanceDiff = BigNumber.from(amount).mul(conversionRate).mul(nPayment); + const expectedDAIFeeBalanceDiff = + // fee added by the batch + expectedToDAIBalanceDiff + .add(BigNumber.from(fee).mul(conversionRate).mul(nPayment)) + .mul(BATCH_CONV_FEE) + .div(BATCH_DENOMINATOR) + // fee within the invoice: .1% of the amount, + .add(BigNumber.from(fee).mul(conversionRate).mul(nPayment)); + fee; + const expectedFromDAIBalanceDiff = expectedToDAIBalanceDiff + .add(expectedDAIFeeBalanceDiff) + .mul(-1); + return [ + expectedFromDAIBalanceDiff.div('1000000'), // divide by 1 million because we used millionDai + expectedToDAIBalanceDiff.div('1000000'), + expectedDAIFeeBalanceDiff.div('1000000'), + ]; + }; + + /** No conversion */ + const getExpectedERC20Balances = (amount: number, fee: number, nPayment: number) => { + const expectedToDAIBalanceDiff = BigNumber.from(amount).mul(nPayment); + const expectedDAIFeeBalanceDiff = + // fee added by the batch + expectedToDAIBalanceDiff + .mul(BATCH_FEE) + .div(BATCH_DENOMINATOR) + // fee within the invoice: .1% of the amount, + .add(BigNumber.from(fee).mul(nPayment)); + fee; + const expectedFromDAIBalanceDiff = expectedToDAIBalanceDiff + .add(expectedDAIFeeBalanceDiff) + .mul(-1); + return [expectedFromDAIBalanceDiff, expectedToDAIBalanceDiff, expectedDAIFeeBalanceDiff]; + }; + + const calculDiffAndCheckERC20Balances = async ( + token: 'DAI' | 'FAU', + fromOldBalance: BigNumber, + toOldBalance: BigNumber, + feeOldBalance: BigNumber, + expectedFromBalanceDiff: BigNumber, + expectedToBalanceDiff: BigNumber, + expectedFeeBalanceDiff: BigNumber, + ) => { + const testERC20 = token === 'FAU' ? fauERC20 : daiERC20; + // Get balances + const [fromBalance, toBalance, feeBalance, batchBalance] = await getERC20Balances(testERC20); + // Compare balance changes to expected values + const fromBalanceDiff = BigNumber.from(fromBalance).sub(fromOldBalance); + const toBalanceDiff = BigNumber.from(toBalance).sub(toOldBalance); + const feeBalanceDiff = BigNumber.from(feeBalance).sub(feeOldBalance); + + expect(toBalanceDiff).to.equals(expectedToBalanceDiff, `toBalanceDiff in ${token}`); + expect(feeBalanceDiff).to.equals(expectedFeeBalanceDiff, `feeBalanceDiff in ${token}`); + expect(fromBalanceDiff).to.equals(expectedFromBalanceDiff, `fromBalanceDiff in ${token}`); + expect(batchBalance).to.equals('0', `batchBalance in ${token}`); + }; + + const checkETHBalances = async ( + ethAmount: BigNumber, + ethFeeAmount: BigNumber, + feeApplied = BATCH_CONV_FEE, + beforeETHBalanceFrom: BigNumber, + beforeETHBalanceTo: BigNumber, + beforeETHBalanceFee: BigNumber, + ) => { + const receipt = await tx.wait(); + const gasUsed = receipt.gasUsed.mul(2 * 10 ** 10); + + const afterETHBalance = await provider.getBalance(await fromSigner.getAddress()); + const afterETHBalanceTo = await provider.getBalance(to); + const afterETHBalanceFee = await provider.getBalance(feeAddress); + const batchETHBalanceDiff = await provider.getBalance(batchConversionProxy.address); + + // Calculate the difference of the balance : now - before + const fromETHBalanceDiff = beforeETHBalanceFrom.sub(afterETHBalance); + const toETHBalanceDiff = afterETHBalanceTo.sub(beforeETHBalanceTo); + const feeETHBalanceDiff = afterETHBalanceFee.sub(beforeETHBalanceFee); + + const expectedToETHBalanceDiff = ethAmount; + const expectedFeeETHBalanceDiff = expectedToETHBalanceDiff + .add(ethFeeAmount) + .mul(feeApplied) + .div(BATCH_DENOMINATOR) + .add(ethFeeAmount); + const expectedFromETHBalanceDiff = gasUsed + .add(expectedToETHBalanceDiff) + .add(expectedFeeETHBalanceDiff); + + // Check balance changes + expect(fromETHBalanceDiff).to.equals(expectedFromETHBalanceDiff, 'DiffBalance'); + expect(toETHBalanceDiff).to.equals(expectedToETHBalanceDiff, 'toETHBalanceDiff'); + expect(feeETHBalanceDiff).to.equals(expectedFeeETHBalanceDiff, 'feeETHBalanceDiff'); + expect(batchETHBalanceDiff).to.equals('0', 'batchETHBalanceDiff'); + }; + + /** + * Pays 3 ERC20 conversions payments, with DAI and FAU tokens and it calculates the balances + * It also check the balances expected for FAU token. + */ + const manyPaymentsBatchConv = async (withBatchRouter = false) => { + const [fromOldDAIBalance, toOldDAIBalance, feeOldDAIBalance] = await getERC20Balances(daiERC20); + const [fromOldFAUBalance, toOldFAUBalance, feeOldFAUBalance] = await getERC20Balances(fauERC20); + + if (withBatchRouter) { + await batchConversionProxy.batchRouter( + [ + { + paymentNetworkId: '0', + conversionDetails: [fauConvDetail, daiConvDetail, daiConvDetail], + cryptoDetails: emptyCryptoDetails, + }, + ], + feeAddress, + ); + } else { + await batchConversionProxy + .connect(fromSigner) + .batchMultiERC20ConversionPayments( + [fauConvDetail, daiConvDetail, daiConvDetail], + feeAddress, + ); + } + + // check the balance daiERC20 token + const [expectedFromDAIBalanceDiff, expectedToDAIBalanceDiff, expectedDAIFeeBalanceDiff] = + getExpectedConvERC20Balances(100000, 100, 2, 'EUR_DAI'); + await calculDiffAndCheckERC20Balances( + 'DAI', + fromOldDAIBalance, + toOldDAIBalance, + feeOldDAIBalance, + expectedFromDAIBalanceDiff, + expectedToDAIBalanceDiff, + expectedDAIFeeBalanceDiff, ); - }); + + // check the balance fauERC20 token + const [expectedFromFAUBalanceDiff, expectedToFAUBalanceDiff, expectedFeeFAUBalanceDiff] = + getExpectedConvERC20Balances(100000, 100, 1, 'USD_FAU'); + await calculDiffAndCheckERC20Balances( + 'FAU', + fromOldFAUBalance, + toOldFAUBalance, + feeOldFAUBalance, + expectedFromFAUBalanceDiff, + expectedToFAUBalanceDiff, + expectedFeeFAUBalanceDiff, + ); + }; + describe('batchRouter', async () => { it(`make ERC20 payment with no conversion`, async function () { + const [fromOldFAUBalance, toOldFAUBalance, feeOldFAUBalance] = await getERC20Balances( + fauERC20, + ); await batchConversionProxy.batchRouter( [ { paymentNetworkId: 2, conversionDetails: [], cryptoDetails: { - tokenAddresses: [DAI_address], + tokenAddresses: [FAU_address], recipients: [to], - amounts: [amount], + amounts: ['100000'], paymentReferences: [referenceExample], - feeAmounts: [feeAmount], + feeAmounts: ['100'], }, }, ], feeAddress, ); - [fromDiffBalanceExpected1, toDiffBalanceExpected1, feeDiffBalanceExpected1] = - expectedERC20Balances( - [BigNumber.from(amount)], - [BigNumber.from(feeAmount)], - batchFee, - false, - ); + // check the balance fauERC20 token + const [expectedFromFAUBalanceDiff, expectedToFAUBalanceDiff, expectedFeeFAUBalanceDiff] = + getExpectedERC20Balances(100000, 100, 1); + + await calculDiffAndCheckERC20Balances( + 'FAU', + fromOldFAUBalance, + toOldFAUBalance, + feeOldFAUBalance, + expectedFromFAUBalanceDiff, + expectedToFAUBalanceDiff, + expectedFeeFAUBalanceDiff, + ); }); it('make 3 ERC20 payments with different tokens and conversion lengths', async () => { - await manyPaymentsBatchConv([EUR_hash, USD_hash, DAI_address], [USD_hash, FAU_address], true); + await manyPaymentsBatchConv(true); }); it('make ETH payment without conversion', async function () { - const cryptoDetails = { - tokenAddresses: [], - recipients: [to], - amounts: [amount], // in ETH - paymentReferences: [referenceExample], - feeAmounts: [feeAmount], // in ETH - }; + // get Eth balances + const beforeETHBalanceTo = await provider.getBalance(to); + const beforeETHBalanceFee = await provider.getBalance(feeAddress); + const beforeETHBalanceFrom = await provider.getBalance(await fromSigner.getAddress()); + tx = await batchConversionProxy.batchRouter( [ { paymentNetworkId: 3, conversionDetails: [], - cryptoDetails: cryptoDetails, + cryptoDetails: { + tokenAddresses: [], + recipients: [to], + amounts: ['1000'], + paymentReferences: [referenceExample], + feeAmounts: ['1'], + }, }, ], feeAddress, - { value: '1000000000' }, + { value: 1000 + 1 + 42 }, + ); + + await checkETHBalances( + BigNumber.from(1000), + BigNumber.from(1), + BATCH_FEE, + beforeETHBalanceFrom, + beforeETHBalanceTo, + beforeETHBalanceFee, ); - await checkEthBalances(amount, feeAmount, batchFee); }); it('make ETH payment with 1-step conversion', async function () { + // get Eth balances + const beforeETHBalanceTo = await provider.getBalance(to); + const beforeETHBalanceFee = await provider.getBalance(feeAddress); + const beforeETHBalanceFrom = await provider.getBalance(await fromSigner.getAddress()); tx = await batchConversionProxy.batchRouter( [ { @@ -464,31 +411,50 @@ describe('contract: BatchConversionPayments', async () => { ], feeAddress, { - value: ethConversionToPay.mul(2), + value: (1000 + 1 + 42) * 20000000, }, ); - await checkEthBalances(ethConversionToPay, ethConversionFee); + + await checkETHBalances( + BigNumber.from(1000 * 20000000), + BigNumber.from(1 * 20000000), + BATCH_CONV_FEE, + beforeETHBalanceFrom, + beforeETHBalanceTo, + beforeETHBalanceFee, + ); }); it('make n heterogeneous (ERC20 and ETH) payments with and without conversion', async () => { - // set convDetail: done within "beforeEach" + // get balances + const [fromOldFAUBalance, toOldFAUBalance, feeOldFAUBalance] = await getERC20Balances( + fauERC20, + ); + const beforeETHBalanceTo = await provider.getBalance(to); + const beforeETHBalanceFee = await provider.getBalance(feeAddress); + const beforeETHBalanceFrom = await provider.getBalance(await fromSigner.getAddress()); - // set ERC20 cryptoDetails + // set inputs: ERC20 cryptoDetails & ethCryptoDetails const cryptoDetails = { - tokenAddresses: [DAI_address], + tokenAddresses: [FAU_address], + recipients: [to], + amounts: ['100000'], + paymentReferences: [referenceExample], + feeAmounts: ['100'], + }; + const ethCryptoDetails = { + tokenAddresses: [], recipients: [to], - amounts: [amount], + amounts: ['1000'], paymentReferences: [referenceExample], - feeAmounts: [feeAmount], + feeAmounts: ['1'], }; - const ethCryptoDetails = Utils.deepCopy(cryptoDetails); - ethCryptoDetails.tokenAddresses = []; - await batchConversionProxy.batchRouter( + tx = await batchConversionProxy.batchRouter( [ { paymentNetworkId: 0, - conversionDetails: [convDetail], + conversionDetails: [fauConvDetail], cryptoDetails: emptyCryptoDetails, }, { @@ -508,35 +474,62 @@ describe('contract: BatchConversionPayments', async () => { }, ], feeAddress, - { value: ethConversionToPay.mul(2).add(amount) }, + { value: (1000 + 1 + 42) * 20000000 + (1000 + 1 + 42) }, // +42 in excess ); - const [ - conversionFromDiffBalanceExpected1, - conversionToDiffBalanceExpected1, - conversionFeeDiffBalanceExpected1, - ] = expectedERC20Balances([conversionToPay], [conversionFees], batchConvFee); + // Chech FAU Balances // + const [expectedFromFAUBalanceDiff, expectedToFAUBalanceDiff, expectedFeeFAUBalanceDiff] = + getExpectedConvERC20Balances(100000, 100, 1, 'USD_FAU'); const [ - noConversionFromDiffBalanceExpected1, - noConversionToDiffBalanceExpected1, - noConversionFeeDiffBalanceExpected1, - ] = expectedERC20Balances( - [BigNumber.from(amount)], - [BigNumber.from(feeAmount)], - batchFee, - false, + noConvExpectedFromFAUBalanceDiff, + noConvExpectedToFAUBalanceDiff, + noConvExpectedFeeFAUBalanceDiff, + ] = getExpectedERC20Balances(100000, 100, 1); + + await calculDiffAndCheckERC20Balances( + 'FAU', + fromOldFAUBalance, + toOldFAUBalance, + feeOldFAUBalance, + expectedFromFAUBalanceDiff.add(noConvExpectedFromFAUBalanceDiff), + expectedToFAUBalanceDiff.add(noConvExpectedToFAUBalanceDiff), + expectedFeeFAUBalanceDiff.add(noConvExpectedFeeFAUBalanceDiff), ); - fromDiffBalanceExpected1 = conversionFromDiffBalanceExpected1.add( - noConversionFromDiffBalanceExpected1, - ); - toDiffBalanceExpected1 = conversionToDiffBalanceExpected1.add( - noConversionToDiffBalanceExpected1, - ); - feeDiffBalanceExpected1 = conversionFeeDiffBalanceExpected1.add( - noConversionFeeDiffBalanceExpected1, + // Check ETH balances // + const receipt = await tx.wait(); + const gasUsed = receipt.gasUsed.mul(2 * 10 ** 10); + + const afterETHBalance = await provider.getBalance(await fromSigner.getAddress()); + const afterETHBalanceTo = await provider.getBalance(to); + const afterETHBalanceFee = await provider.getBalance(feeAddress); + const batchETHBalanceDiff = await provider.getBalance(batchConversionProxy.address); + + // Calculate the difference of the balance : now - before + const fromETHBalanceDiff = beforeETHBalanceFrom.sub(afterETHBalance); + const toETHBalanceDiff = afterETHBalanceTo.sub(beforeETHBalanceTo); + const feeETHBalanceDiff = afterETHBalanceFee.sub(beforeETHBalanceFee); + + // expectedFeeETHBalanceDiff includes batch conversion fees now + const expectedFeeETHBalanceDiff = BigNumber.from(1000 * 20000000) + .add(1 * 20000000) + .mul(BATCH_CONV_FEE) + .div(BATCH_DENOMINATOR) + .add(1 * 20000000) + .add(BigNumber.from(1000).add(1).mul(BATCH_FEE).div(BATCH_DENOMINATOR).add(1)); + + const expectedFromETHBalanceDiff = gasUsed + .add(1000 * 20000000 + 1000) + .add(expectedFeeETHBalanceDiff); + // Check balance changes + expect(fromETHBalanceDiff).to.equals(expectedFromETHBalanceDiff, 'DiffBalance'); + expect(toETHBalanceDiff).to.equals( + BigNumber.from(1000 * 20000000 + 1000), + 'toETHBalanceDiff', ); + expect(feeETHBalanceDiff).to.equals(expectedFeeETHBalanceDiff, 'feeETHBalanceDiff'); + expect(batchETHBalanceDiff).to.equals('0', 'batchETHBalanceDiff'); }); }); @@ -596,39 +589,58 @@ describe('contract: BatchConversionPayments', async () => { }); }); describe('batchMultiERC20ConversionPayments', async () => { - it('make 1 payment with 1-step conversion', async () => { - await setConvToPayAndConvDetail( - to, - [USD_hash, DAI_address], - amount.toString(), - feeAmount.toString(), - 0, - chainlinkPath, + it('make 1 payment with 1-step conversion in FAU', async () => { + const [fromOldFAUBalance, toOldFAUBalance, feeOldFAUBalance] = await getERC20Balances( + fauERC20, + ); + + await batchConversionProxy + .connect(fromSigner) + .batchMultiERC20ConversionPayments([fauConvDetail], feeAddress); + + const [expectedFromFAUBalanceDiff, expectedToFAUBalanceDiff, expectedFeeFAUBalanceDiff] = + getExpectedConvERC20Balances(100000, 100, 1, 'USD_FAU'); + + await calculDiffAndCheckERC20Balances( + 'FAU', + fromOldFAUBalance, + toOldFAUBalance, + feeOldFAUBalance, + expectedFromFAUBalanceDiff, + expectedToFAUBalanceDiff, + expectedFeeFAUBalanceDiff, ); - await batchConversionProxy.batchMultiERC20ConversionPayments([convDetail], feeAddress); - [fromDiffBalanceExpected1, toDiffBalanceExpected1, feeDiffBalanceExpected1] = - expectedERC20Balances([conversionToPay], [conversionFees], batchConvFee); }); - it('make 1 payment with 2-steps conversion', async () => { - await setConvToPayAndConvDetail( - to, - [EUR_hash, USD_hash, DAI_address], - amount.toString(), - feeAmount.toString(), - 0, - chainlinkPath, + it('make 1 payment with 2-steps conversion in DAI', async () => { + const [fromOldDAIBalance, toOldDAIBalance, feeOldDAIBalance] = await getERC20Balances( + daiERC20, + ); + + await batchConversionProxy + .connect(fromSigner) + .batchMultiERC20ConversionPayments([daiConvDetail], feeAddress); + + const [expectedFromDAIBalanceDiff, expectedToDAIBalanceDiff, expectedDAIFeeBalanceDiff] = + getExpectedConvERC20Balances(100000, 100, 1, 'EUR_DAI'); + + await calculDiffAndCheckERC20Balances( + 'DAI', + fromOldDAIBalance, + toOldDAIBalance, + feeOldDAIBalance, + expectedFromDAIBalanceDiff, + expectedToDAIBalanceDiff, + expectedDAIFeeBalanceDiff, ); - await batchConversionProxy.batchMultiERC20ConversionPayments([convDetail], feeAddress); - [fromDiffBalanceExpected1, toDiffBalanceExpected1, feeDiffBalanceExpected1] = - expectedERC20Balances([conversionToPay], [conversionFees], batchConvFee); }); it('make 3 payment with different tokens and conversion length', async () => { - await manyPaymentsBatchConv([EUR_hash, USD_hash, DAI_address], [USD_hash, FAU_address]); + await manyPaymentsBatchConv(); }); }); describe('batchMultiERC20ConversionPayments errors', async () => { it('cannot transfer with invalid path', async function () { + const convDetail = Utils.deepCopy(fauConvDetail); convDetail.path = [EUR_hash, ETH_hash, DAI_address]; await expect( batchConversionProxy.batchMultiERC20ConversionPayments([convDetail], feeAddress), @@ -636,22 +648,25 @@ describe('contract: BatchConversionPayments', async () => { }); it('cannot transfer if max to spend too low', async function () { - convDetail.maxToSpend = conversionToPay.add(conversionFees).sub(1).toString(); + const convDetail = Utils.deepCopy(fauConvDetail); + convDetail.maxToSpend = '1000000'; // not enough await expect( batchConversionProxy.batchMultiERC20ConversionPayments([convDetail], feeAddress), ).to.be.revertedWith('Amount to pay is over the user limit'); }); it('cannot transfer if rate is too old', async function () { - convDetail.maxRateTimespan = 10; + const convDetail = Utils.deepCopy(fauConvDetail); + convDetail.maxRateTimespan = '10'; await expect( batchConversionProxy.batchMultiERC20ConversionPayments([convDetail], feeAddress), ).to.be.revertedWith('aggregator rate is outdated'); }); it('Not enough allowance', async function () { - // reduce signer1 allowance - await daiERC20.approve( + const convDetail = Utils.deepCopy(fauConvDetail); + // reduce fromSigner± allowance + await fauERC20.approve( batchConversionProxy.address, BigNumber.from(convDetail.maxToSpend).sub(2), { @@ -664,12 +679,15 @@ describe('contract: BatchConversionPayments', async () => { }); it('Not enough funds even if partially enough funds', async function () { - // signer1 transfer enough token to pay just 1 invoice to signer4 - await daiERC20 - .connect(signer1) + const convDetail = Utils.deepCopy(fauConvDetail); + // fromSigner transfer enough token to pay just 1 invoice to signer4 + await fauERC20 + .connect(fromSigner) .transfer(await signer4.getAddress(), BigNumber.from(convDetail.maxToSpend)); // increase signer4 allowance - await daiERC20.connect(signer4).approve(batchConversionProxy.address, thousandWith18Decimal); + await fauERC20 + .connect(signer4) + .approve(batchConversionProxy.address, thousandWith18Decimal + fiatDecimals); // 3 invoices to pay await expect( @@ -678,33 +696,39 @@ describe('contract: BatchConversionPayments', async () => { .batchMultiERC20ConversionPayments([convDetail, convDetail, convDetail], feeAddress), ).to.be.revertedWith('not enough funds, including fees'); - // signer4 transfer token to signer1 - await daiERC20 + // signer4 transfer token to fromSigner + await fauERC20 .connect(signer4) - .transfer(from, await daiERC20.balanceOf(await signer4.getAddress())); + .transfer(from, await fauERC20.balanceOf(await signer4.getAddress())); }); }); describe(`batchEthConversionPayments`, () => { it('make 1 payment with 1-step conversion', async function () { + // get Eth balances + const beforeETHBalanceTo = await provider.getBalance(to); + const beforeETHBalanceFee = await provider.getBalance(feeAddress); + const beforeETHBalanceFrom = await provider.getBalance(await fromSigner.getAddress()); tx = await batchConversionProxy.batchEthConversionPayments([ethConvDetail], feeAddress, { - value: BigNumber.from('100000000000000000'), + value: (1000 + 1 + 42) * 20000000, // +42 in excess }); - await checkEthBalances(ethConversionToPay, ethConversionFee); + await checkETHBalances( + BigNumber.from(1000 * 20000000), + BigNumber.from(1 * 20000000), + BATCH_CONV_FEE, + beforeETHBalanceFrom, + beforeETHBalanceTo, + beforeETHBalanceFee, + ); }); it('make 3 payments with different conversion lengths', async function () { + // get Eth balances + const beforeETHBalanceTo = await provider.getBalance(to); + const beforeETHBalanceFee = await provider.getBalance(feeAddress); + const beforeETHBalanceFrom = await provider.getBalance(await fromSigner.getAddress()); const EurConvDetail = Utils.deepCopy(ethConvDetail); EurConvDetail.path = [EUR_hash, USD_hash, ETH_hash]; - const eurConversionToPay = await chainlinkPath.getConversion( - EurConvDetail.requestAmount, - EurConvDetail.path, - ); - const eurFeesToPay = await chainlinkPath.getConversion( - EurConvDetail.feeAmount, - EurConvDetail.path, - ); - tx = await batchConversionProxy.batchEthConversionPayments( [ethConvDetail, EurConvDetail, ethConvDetail], feeAddress, @@ -712,9 +736,15 @@ describe('contract: BatchConversionPayments', async () => { value: BigNumber.from('100000000000000000'), }, ); - await checkEthBalances( - eurConversionToPay.result.add(ethConversionToPay.mul(2)), - eurFeesToPay.result.add(ethConversionFee.mul(2)), + await checkETHBalances( + BigNumber.from(1000 * 20000000) + .mul(2) + .add(1000 * 24000000), + BigNumber.from(20000000).mul(2).add(24000000), + BATCH_CONV_FEE, + beforeETHBalanceFrom, + beforeETHBalanceTo, + beforeETHBalanceFee, ); }); }); @@ -724,7 +754,7 @@ describe('contract: BatchConversionPayments', async () => { wrongConvDetail.path = [USD_hash, EUR_hash, ETH_hash]; await expect( batchConversionProxy.batchEthConversionPayments([wrongConvDetail], feeAddress, { - value: ethConversionToPay.mul(2), + value: (1000 + 1 + 42) * 20000000, }), ).to.be.revertedWith('No aggregator found'); }); @@ -734,7 +764,7 @@ describe('contract: BatchConversionPayments', async () => { [ethConvDetail, ethConvDetail], feeAddress, { - value: ethConversionToPay.mul(2), // no enough to pay the amount AND the fees + value: (2000 + 1) * 20000000, // no enough to pay the amount AND the fees }, ), ).to.be.revertedWith('paymentProxy transferExactEthWithReferenceAndFee failed'); @@ -745,67 +775,86 @@ describe('contract: BatchConversionPayments', async () => { wrongConvDetail.maxRateTimespan = BigNumber.from('1'); await expect( batchConversionProxy.batchEthConversionPayments([wrongConvDetail], feeAddress, { - value: ethConversionToPay.mul(2), + value: 1000 + 1 + 42, }), ).to.be.revertedWith('aggregator rate is outdated'); }); }); describe('Functions herited from contract BatchErc20Payments ', () => { - it(`batchERC20Payments 1 payment`, async function () { + it(`batchERC20Payments make ERC20 payment without conversion`, async function () { + const [fromOldFAUBalance, toOldFAUBalance, feeOldFAUBalance] = await getERC20Balances( + fauERC20, + ); await batchConversionProxy.batchERC20Payments( - DAI_address, + FAU_address, [to], - [amount], + ['100000'], [referenceExample], - [feeAmount], + ['100'], feeAddress, ); - [fromDiffBalanceExpected1, toDiffBalanceExpected1, feeDiffBalanceExpected1] = - expectedERC20Balances( - [BigNumber.from(amount)], - [BigNumber.from(feeAmount)], - batchFee, - false, - ); + const [expectedFromFAUBalanceDiff, expectedToFAUBalanceDiff, expectedFeeFAUBalanceDiff] = + getExpectedERC20Balances(100000, 100, 1); + + await calculDiffAndCheckERC20Balances( + 'FAU', + fromOldFAUBalance, + toOldFAUBalance, + feeOldFAUBalance, + expectedFromFAUBalanceDiff, + expectedToFAUBalanceDiff, + expectedFeeFAUBalanceDiff, + ); }); - it(`batchMultiERC20Payments 1 payment`, async function () { + it(`batchMultiERC20Payments make ERC20 payment without conversion`, async function () { + const [fromOldFAUBalance, toOldFAUBalance, feeOldFAUBalance] = await getERC20Balances( + fauERC20, + ); await batchConversionProxy.batchMultiERC20Payments( - [DAI_address], + [FAU_address], [to], - [amount], + ['100000'], [referenceExample], - [feeAmount], + ['100'], feeAddress, ); - [fromDiffBalanceExpected1, toDiffBalanceExpected1, feeDiffBalanceExpected1] = - expectedERC20Balances( - [BigNumber.from(amount)], - [BigNumber.from(feeAmount)], - batchFee, - false, - ); + const [expectedFromFAUBalanceDiff, expectedToFAUBalanceDiff, expectedFeeFAUBalanceDiff] = + getExpectedERC20Balances(100000, 100, 1); + await calculDiffAndCheckERC20Balances( + 'FAU', + fromOldFAUBalance, + toOldFAUBalance, + feeOldFAUBalance, + expectedFromFAUBalanceDiff, + expectedToFAUBalanceDiff, + expectedFeeFAUBalanceDiff, + ); }); - it('make 1 payment without conversion', async function () { - const cryptoDetails = { - tokenAddresses: [], - recipients: [to], - amounts: [amount], // in ETH - paymentReferences: [referenceExample], - feeAmounts: [feeAmount], // in ETH - }; + it('batchEthPayments 1 payment without conversion', async function () { + // get Eth balances + const beforeETHBalanceTo = await provider.getBalance(to); + const beforeETHBalanceFee = await provider.getBalance(feeAddress); + const beforeETHBalanceFrom = await provider.getBalance(await fromSigner.getAddress()); tx = await batchConversionProxy.batchEthPayments( - cryptoDetails.recipients, - cryptoDetails.amounts, - cryptoDetails.paymentReferences, - cryptoDetails.feeAmounts, + [to], + ['1000'], + [referenceExample], + ['1'], feeAddress, - { value: 1000000000 }, + { value: 1000 + 1 + 42 }, + ); + await checkETHBalances( + BigNumber.from(1000), + BigNumber.from(1), + BATCH_FEE, + beforeETHBalanceFrom, + beforeETHBalanceTo, + beforeETHBalanceFee, ); - await checkEthBalances(amount, feeAmount, batchFee); }); }); }); From 4dbf265f2c8824b149f7efe2bc93527945ddd948 Mon Sep 17 00:00:00 2001 From: olivier7delf <55892112+olivier7delf@users.noreply.github.com> Date: Wed, 7 Sep 2022 09:49:26 +0200 Subject: [PATCH 088/105] tests: cleaning --- .../contracts/BatchConversionPayments.test.ts | 77 ++++++++++--------- 1 file changed, 41 insertions(+), 36 deletions(-) diff --git a/packages/smart-contracts/test/contracts/BatchConversionPayments.test.ts b/packages/smart-contracts/test/contracts/BatchConversionPayments.test.ts index c5cacecf3..30a5ee9fb 100644 --- a/packages/smart-contracts/test/contracts/BatchConversionPayments.test.ts +++ b/packages/smart-contracts/test/contracts/BatchConversionPayments.test.ts @@ -33,7 +33,7 @@ describe('contract: BatchConversionPayments', async () => { let tx: ContractTransaction; // constants used to set up batch conversion proxy, and also requests payment - const BATCH_FEE = 50; + const BATCH_FEE = 50; // .5% const BATCH_CONV_FEE = 100; // 1% const BATCH_DENOMINATOR = 10000; const daiDecimals = '1000000000000000000'; @@ -42,7 +42,7 @@ describe('contract: BatchConversionPayments', async () => { const thousandWith18Decimal = '1000000000000000000000'; const referenceExample = '0xaaaa'; - // constants and variables to set up proxies and paths + // constants related to chainlink and conversion rate const currencyManager = CurrencyManager.getDefault(); const ETH_hash = currencyManager.fromSymbol('ETH')!.hash; @@ -50,15 +50,15 @@ describe('contract: BatchConversionPayments', async () => { const EUR_hash = currencyManager.fromSymbol('EUR')!.hash; const DAI_address = localERC20AlphaArtifact.getAddress(network.name); const FAU_address = secondLocalERC20AlphaArtifact.getAddress(network.name); + const USD_ETH_RATE = 20000000; - const EUR_USD = 120; // const eurDaiRate = 1.2 / 1.01; - const DAI_USD = 101; - + // proxies and tokens let batchConversionProxy: BatchConversionPayments; let daiERC20: TestERC20; let fauERC20: TestERC20; let chainlinkPath: ChainlinkConversionPath; + // constants inputs for batch functions, both conversion and no-conversion const emptyCryptoDetails = { tokenAddresses: [], recipients: [], @@ -167,7 +167,7 @@ describe('contract: BatchConversionPayments', async () => { // to get the exact result, we use millionDai const conversionRate = path === 'EUR_DAI' - ? BigNumber.from(millionDai).mul(EUR_USD).div(DAI_USD) + ? BigNumber.from(millionDai).mul(120).div(101) // EUR_USD: 120, and DAI_USD: 101 : BigNumber.from(millionDai).mul(100).div(FAU_USD_RATE); const expectedToDAIBalanceDiff = BigNumber.from(amount).mul(conversionRate).mul(nPayment); const expectedDAIFeeBalanceDiff = @@ -206,6 +206,7 @@ describe('contract: BatchConversionPayments', async () => { return [expectedFromDAIBalanceDiff, expectedToDAIBalanceDiff, expectedDAIFeeBalanceDiff]; }; + /** Both conversion and no-conversion payment */ const calculDiffAndCheckERC20Balances = async ( token: 'DAI' | 'FAU', fromOldBalance: BigNumber, @@ -229,6 +230,7 @@ describe('contract: BatchConversionPayments', async () => { expect(batchBalance).to.equals('0', `batchBalance in ${token}`); }; + /** Both conversion and no-conversion payment */ const checkETHBalances = async ( ethAmount: BigNumber, ethFeeAmount: BigNumber, @@ -323,7 +325,7 @@ describe('contract: BatchConversionPayments', async () => { }; describe('batchRouter', async () => { - it(`make ERC20 payment with no conversion`, async function () { + it(`make 1 ERC20 payment with no conversion`, async function () { const [fromOldFAUBalance, toOldFAUBalance, feeOldFAUBalance] = await getERC20Balances( fauERC20, ); @@ -362,7 +364,7 @@ describe('contract: BatchConversionPayments', async () => { await manyPaymentsBatchConv(true); }); - it('make ETH payment without conversion', async function () { + it('make 1 ETH payment without conversion', async function () { // get Eth balances const beforeETHBalanceTo = await provider.getBalance(to); const beforeETHBalanceFee = await provider.getBalance(feeAddress); @@ -396,7 +398,7 @@ describe('contract: BatchConversionPayments', async () => { ); }); - it('make ETH payment with 1-step conversion', async function () { + it('make 1 ETH payment with 1-step conversion', async function () { // get Eth balances const beforeETHBalanceTo = await provider.getBalance(to); const beforeETHBalanceFee = await provider.getBalance(feeAddress); @@ -411,13 +413,13 @@ describe('contract: BatchConversionPayments', async () => { ], feeAddress, { - value: (1000 + 1 + 42) * 20000000, + value: (1000 + 1 + 42) * USD_ETH_RATE, }, ); await checkETHBalances( - BigNumber.from(1000 * 20000000), - BigNumber.from(1 * 20000000), + BigNumber.from(1000 * USD_ETH_RATE), + BigNumber.from(1 * USD_ETH_RATE), BATCH_CONV_FEE, beforeETHBalanceFrom, beforeETHBalanceTo, @@ -474,7 +476,7 @@ describe('contract: BatchConversionPayments', async () => { }, ], feeAddress, - { value: (1000 + 1 + 42) * 20000000 + (1000 + 1 + 42) }, // +42 in excess + { value: (1000 + 1 + 42) * USD_ETH_RATE + (1000 + 1 + 42) }, // +42 in excess ); // Chech FAU Balances // @@ -512,20 +514,23 @@ describe('contract: BatchConversionPayments', async () => { const feeETHBalanceDiff = afterETHBalanceFee.sub(beforeETHBalanceFee); // expectedFeeETHBalanceDiff includes batch conversion fees now - const expectedFeeETHBalanceDiff = BigNumber.from(1000 * 20000000) - .add(1 * 20000000) - .mul(BATCH_CONV_FEE) - .div(BATCH_DENOMINATOR) - .add(1 * 20000000) - .add(BigNumber.from(1000).add(1).mul(BATCH_FEE).div(BATCH_DENOMINATOR).add(1)); + const expectedFeeETHBalanceDiff = + // Batch conversion + BigNumber.from(1000 * USD_ETH_RATE) + .add(1 * USD_ETH_RATE) + .mul(BATCH_CONV_FEE) + .div(BATCH_DENOMINATOR) + // Batch no-conversion + .add(1 * USD_ETH_RATE) + .add(BigNumber.from(1000).add(1).mul(BATCH_FEE).div(BATCH_DENOMINATOR).add(1)); const expectedFromETHBalanceDiff = gasUsed - .add(1000 * 20000000 + 1000) + .add(1000 * USD_ETH_RATE + 1000) .add(expectedFeeETHBalanceDiff); // Check balance changes expect(fromETHBalanceDiff).to.equals(expectedFromETHBalanceDiff, 'DiffBalance'); expect(toETHBalanceDiff).to.equals( - BigNumber.from(1000 * 20000000 + 1000), + BigNumber.from(1000 * USD_ETH_RATE + 1000), 'toETHBalanceDiff', ); expect(feeETHBalanceDiff).to.equals(expectedFeeETHBalanceDiff, 'feeETHBalanceDiff'); @@ -534,7 +539,7 @@ describe('contract: BatchConversionPayments', async () => { }); describe('batchRouter errors', async () => { - it(`Too many elements within batchRouter metaDetails input`, async function () { + it(`too many elements within batchRouter metaDetails input`, async function () { await expect( batchConversionProxy.batchRouter( [ @@ -573,7 +578,7 @@ describe('contract: BatchConversionPayments', async () => { ), ).to.be.revertedWith('more than 5 metaDetails'); }); - it(`Too many elements within batchRouter metaDetails input`, async function () { + it(`wrong paymentNetworkId set in metaDetails input`, async function () { await expect( batchConversionProxy.batchRouter( [ @@ -589,7 +594,7 @@ describe('contract: BatchConversionPayments', async () => { }); }); describe('batchMultiERC20ConversionPayments', async () => { - it('make 1 payment with 1-step conversion in FAU', async () => { + it('make 1 payment with 1-step conversion', async () => { const [fromOldFAUBalance, toOldFAUBalance, feeOldFAUBalance] = await getERC20Balances( fauERC20, ); @@ -633,7 +638,7 @@ describe('contract: BatchConversionPayments', async () => { expectedDAIFeeBalanceDiff, ); }); - it('make 3 payment with different tokens and conversion length', async () => { + it('make 3 payments with different tokens and conversion length', async () => { await manyPaymentsBatchConv(); }); }); @@ -709,11 +714,11 @@ describe('contract: BatchConversionPayments', async () => { const beforeETHBalanceFee = await provider.getBalance(feeAddress); const beforeETHBalanceFrom = await provider.getBalance(await fromSigner.getAddress()); tx = await batchConversionProxy.batchEthConversionPayments([ethConvDetail], feeAddress, { - value: (1000 + 1 + 42) * 20000000, // +42 in excess + value: (1000 + 1 + 42) * USD_ETH_RATE, // +42 in excess }); await checkETHBalances( - BigNumber.from(1000 * 20000000), - BigNumber.from(1 * 20000000), + BigNumber.from(1000 * USD_ETH_RATE), + BigNumber.from(1 * USD_ETH_RATE), BATCH_CONV_FEE, beforeETHBalanceFrom, beforeETHBalanceTo, @@ -737,10 +742,10 @@ describe('contract: BatchConversionPayments', async () => { }, ); await checkETHBalances( - BigNumber.from(1000 * 20000000) + BigNumber.from(1000 * USD_ETH_RATE) .mul(2) - .add(1000 * 24000000), - BigNumber.from(20000000).mul(2).add(24000000), + .add(1000 * 24000000), // 24000000 is EUR_ETH_RATE + BigNumber.from(USD_ETH_RATE).mul(2).add(24000000), BATCH_CONV_FEE, beforeETHBalanceFrom, beforeETHBalanceTo, @@ -754,7 +759,7 @@ describe('contract: BatchConversionPayments', async () => { wrongConvDetail.path = [USD_hash, EUR_hash, ETH_hash]; await expect( batchConversionProxy.batchEthConversionPayments([wrongConvDetail], feeAddress, { - value: (1000 + 1 + 42) * 20000000, + value: (1000 + 1 + 42) * USD_ETH_RATE, }), ).to.be.revertedWith('No aggregator found'); }); @@ -764,7 +769,7 @@ describe('contract: BatchConversionPayments', async () => { [ethConvDetail, ethConvDetail], feeAddress, { - value: (2000 + 1) * 20000000, // no enough to pay the amount AND the fees + value: (2000 + 1) * USD_ETH_RATE, // no enough to pay the amount AND the fees }, ), ).to.be.revertedWith('paymentProxy transferExactEthWithReferenceAndFee failed'); @@ -781,7 +786,7 @@ describe('contract: BatchConversionPayments', async () => { }); }); describe('Functions herited from contract BatchErc20Payments ', () => { - it(`batchERC20Payments make ERC20 payment without conversion`, async function () { + it(`batchERC20Payments make 1 ERC20 payment without conversion`, async function () { const [fromOldFAUBalance, toOldFAUBalance, feeOldFAUBalance] = await getERC20Balances( fauERC20, ); @@ -808,7 +813,7 @@ describe('contract: BatchConversionPayments', async () => { ); }); - it(`batchMultiERC20Payments make ERC20 payment without conversion`, async function () { + it(`batchMultiERC20Payments make 1 ERC20 payment without conversion`, async function () { const [fromOldFAUBalance, toOldFAUBalance, feeOldFAUBalance] = await getERC20Balances( fauERC20, ); @@ -834,7 +839,7 @@ describe('contract: BatchConversionPayments', async () => { ); }); - it('batchEthPayments 1 payment without conversion', async function () { + it('batchEthPayments make 1 ETH payment without conversion', async function () { // get Eth balances const beforeETHBalanceTo = await provider.getBalance(to); const beforeETHBalanceFee = await provider.getBalance(feeAddress); From d385a3182e820269285a888e5895645c398ee122 Mon Sep 17 00:00:00 2001 From: olivier7delf <55892112+olivier7delf@users.noreply.github.com> Date: Wed, 7 Sep 2022 12:18:08 +0200 Subject: [PATCH 089/105] update batchConversionPayments proxies on rinkeby, goerli, and matic --- .../lib/artifacts/BatchConversionPayments/index.ts | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/packages/smart-contracts/src/lib/artifacts/BatchConversionPayments/index.ts b/packages/smart-contracts/src/lib/artifacts/BatchConversionPayments/index.ts index 9b2356b17..3c06e5206 100644 --- a/packages/smart-contracts/src/lib/artifacts/BatchConversionPayments/index.ts +++ b/packages/smart-contracts/src/lib/artifacts/BatchConversionPayments/index.ts @@ -14,12 +14,16 @@ export const batchConversionPaymentsArtifact = new ContractArtifact Date: Wed, 7 Sep 2022 16:31:38 +0200 Subject: [PATCH 090/105] add type for batch payment inputs --- .../src/payment/batch-conversion-proxy.ts | 409 ++++++++++++++++++ .../contracts/BatchConversionPayments.test.ts | 19 +- packages/types/src/payment-types.ts | 29 ++ 3 files changed, 448 insertions(+), 9 deletions(-) create mode 100644 packages/payment-processor/src/payment/batch-conversion-proxy.ts diff --git a/packages/payment-processor/src/payment/batch-conversion-proxy.ts b/packages/payment-processor/src/payment/batch-conversion-proxy.ts new file mode 100644 index 000000000..83f45c960 --- /dev/null +++ b/packages/payment-processor/src/payment/batch-conversion-proxy.ts @@ -0,0 +1,409 @@ +import { ContractTransaction, Signer, providers, BigNumber, BigNumberish } from 'ethers'; +import { batchConversionPaymentsArtifact } from '@requestnetwork/smart-contracts'; +import { BatchConversionPayments__factory } from '@requestnetwork/smart-contracts/types'; +import { ClientTypes, ExtensionTypes, RequestLogicTypes } from '@requestnetwork/types'; +import { ITransactionOverrides } from './transaction-overrides'; +import { comparePnTypeAndVersion, getProvider, getRequestPaymentValues, getSigner } from './utils'; +import { + padAmountForChainlink, + getPaymentNetworkExtension, +} from '@requestnetwork/payment-detection'; +import { IPreparedTransaction } from './prepared-transaction'; +import { EnrichedRequest, IConversionPaymentSettings } from './index'; +import { checkRequestAndGetPathAndCurrency } from './any-to-erc20-proxy'; +import { getBatchArgs } from './batch-proxy'; +import { checkErc20Allowance, encodeApproveAnyErc20 } from './erc20'; + +// Types used by batch conversion smart contract +type ConversionDetail = { + recipient: string; + requestAmount: BigNumberish; + path: string[]; + paymentReference: string; + feeAmount: BigNumberish; + maxToSpend: BigNumberish; + maxRateTimespan: BigNumberish; +}; + +type CryptoDetails = { + tokenAddresses: Array; + recipients: Array; + amounts: Array; + paymentReferences: Array; + feeAmounts: Array; +}; + +type MetaDetail = { + paymentNetworkId: number; + conversionDetails: ConversionDetail[]; + cryptoDetails: CryptoDetails; +}; + +/** + * Processes a transaction to pay a batch of requests with an ERC20 currency + * that is different from the request currency (eg. fiat) + * The payment is made through ERC20 or ERC20Conversion proxies + * It can be used with a Multisig contract + * @param enrichedRequests List of EnrichedRequest to pay + * @param version Version of the batch conversion proxy + * @param signerOrProvider The Web3 provider, or signer. Defaults to window.ethereum. + * @param overrides Optionally, override default transaction values, like gas. + * @dev We only implement batchRouter using the ERC20 functions: + * batchERC20ConversionPaymentsMultiTokens, and batchERC20PaymentsMultiTokensWithReference. + * It implies that paymentNetworkId take only theses values: 0 or 2 + * Next steps: + * - Enable ETH payment within batchRouter function: with/out conversion + * - Enable gas optimizaton: implement the others batch functions + */ +export async function payBatchConversionProxyRequest( + enrichedRequests: EnrichedRequest[], + version: string, + signerOrProvider: providers.Provider | Signer = getProvider(), + overrides?: ITransactionOverrides, +): Promise { + const { data, to, value } = prepareBatchConversionPaymentTransaction(enrichedRequests, version); + const signer = getSigner(signerOrProvider); + return signer.sendTransaction({ data, to, value, ...overrides }); +} + +/** + * Prepare the transaction to pay a batch of requests through the batch conversion proxy contract, + * it can be used with a Multisig contract. + * @param enrichedRequests List of EnrichedRequest to pay + * @param version Version of the batch conversion proxy + */ +export function prepareBatchConversionPaymentTransaction( + enrichedRequests: EnrichedRequest[], + version: string, +): IPreparedTransaction { + const encodedTx = encodePayBatchConversionRequest(enrichedRequests); + const proxyAddress = getBatchConversionProxyAddress( + enrichedRequests[0].request, + version, + enrichedRequests[0].paymentSettings, + ); + return { + data: encodedTx, + to: proxyAddress, + value: 0, + }; +} + +/** + * Encodes the call to pay a batch conversion of requests through ERC20 or ERC20Conversion proxies + * It can be used with a Multisig contract. + * @param enrichedRequests list of ECR20 requests to pay + */ +export function encodePayBatchConversionRequest(enrichedRequests: EnrichedRequest[]): string { + // Get fee address + const extension = getPaymentNetworkExtension(enrichedRequests[0].request); + if (!extension) { + throw new Error('no payment network found'); + } + const { feeAddress } = extension.values; + + //**** Create and fill batchRouter function argument: metaDetails ****// + + const metaDetails: MetaDetail[] = []; + + // Variable and constants to get info about each payment network (pn) + let pn0FirstRequest: ClientTypes.IRequestData | undefined; + const pn2requests: ClientTypes.IRequestData[] = []; + // Constant storing conversion info + const conversionDetails: ConversionDetail[] = []; + + // Iterate throught each enrichedRequests to do checking and retrieve info + for (let i = 0; i < enrichedRequests.length; i++) { + const iExtension = getPaymentNetworkExtension(enrichedRequests[i].request); + if (!iExtension) { + throw new Error('no payment network found'); + } + if (enrichedRequests[i].paymentNetworkId === 0) { + // set pn0FirstRequest only if it is undefined + pn0FirstRequest = pn0FirstRequest ?? enrichedRequests[i].request; + if (!(iExtension.id === ExtensionTypes.ID.PAYMENT_NETWORK_ANY_TO_ERC20_PROXY)) + throw new Error( + 'request cannot be processed, or is not an pn-erc20-fee-proxy-contract request', + ); + + if ( + !comparePnTypeAndVersion( + getPaymentNetworkExtension(pn0FirstRequest), + enrichedRequests[i].request, + ) + ) + throw new Error(`Every payment network type and version must be identical`); + if ( + ![RequestLogicTypes.CURRENCY.ISO4217, RequestLogicTypes.CURRENCY.ERC20].includes( + enrichedRequests[i].request.currencyInfo.type, + ) + ) + throw new Error(`wrong request currencyInfo type`); + conversionDetails.push(getInputConversionDetail(enrichedRequests[i])); + } else if (enrichedRequests[i].paymentNetworkId === 2) { + pn2requests.push(enrichedRequests[i].request); + if ( + !comparePnTypeAndVersion( + getPaymentNetworkExtension(pn2requests[0]), + enrichedRequests[i].request, + ) + ) { + throw new Error(`Every payment network type and version must be identical`); + } + } + } + + // Add conversionDetails to metaDetails + if (pn0FirstRequest) { + metaDetails.push({ + paymentNetworkId: 0, + conversionDetails: conversionDetails, + cryptoDetails: { + tokenAddresses: [], + recipients: [], + amounts: [], + paymentReferences: [], + feeAmounts: [], + }, // cryptoDetails is not used with paymentNetworkId 0 + }); + } + + // Get values and add cryptpoDetails to metaDetails + if (pn2requests.length > 0) { + const { tokenAddresses, paymentAddresses, amountsToPay, paymentReferences, feesToPay } = + getBatchArgs(pn2requests, 'ERC20'); + + metaDetails.push({ + paymentNetworkId: 2, + conversionDetails: [], + cryptoDetails: { + tokenAddresses: tokenAddresses, + recipients: paymentAddresses, + amounts: amountsToPay, + paymentReferences: paymentReferences, + feeAmounts: feesToPay, + }, + }); + } + const proxyContract = BatchConversionPayments__factory.createInterface(); + return proxyContract.encodeFunctionData('batchRouter', [metaDetails, feeAddress]); +} + +/** + * Get the conversion detail values from one enriched request + * @param enrichedRequest enrichedRequest to pay + */ +function getInputConversionDetail(enrichedRequest: EnrichedRequest): ConversionDetail { + const paymentSettings = enrichedRequest.paymentSettings; + if (!paymentSettings) throw Error('the enrichedRequest has no paymentSettings'); + + const { path, requestCurrency } = checkRequestAndGetPathAndCurrency( + enrichedRequest.request, + paymentSettings, + ); + + const { paymentReference, paymentAddress, feeAmount, maxRateTimespan } = getRequestPaymentValues( + enrichedRequest.request, + ); + + const requestAmount = BigNumber.from(enrichedRequest.request.expectedAmount).sub( + enrichedRequest.request.balance?.balance || 0, + ); + + const padRequestAmount = padAmountForChainlink(requestAmount, requestCurrency); + const padFeeAmount = padAmountForChainlink(feeAmount || 0, requestCurrency); + return { + recipient: paymentAddress, + requestAmount: padRequestAmount, + path: path, + paymentReference: `0x${paymentReference}`, + feeAmount: BigNumber.from(padFeeAmount), + maxToSpend: BigNumber.from(paymentSettings.maxToSpend), + maxRateTimespan: BigNumber.from(maxRateTimespan || 0), + }; +} + +/** + * Gets batch conversion contract Address + * @param request request for an ERC20 payment with/out conversion + * @param version of the batch conversion proxy + * @param paymentSettings paymentSettings is necessary for conversion payment + */ +export function getBatchConversionProxyAddress( + request: ClientTypes.IRequestData, + version: string, + paymentSettings?: IConversionPaymentSettings, +): string { + // Get the network + let network = request.currencyInfo.network; + if (paymentSettings?.currency?.network) { + network = paymentSettings.currency.network; + } + if (!network) throw new Error('Cannot pay with a currency missing a network'); + + // Get the proxy address + const proxyAddress = batchConversionPaymentsArtifact.getAddress(network, version); + if (!proxyAddress) + throw new Error( + `No deployment found on the network ${network}, associated with the version ${version}`, + ); + return proxyAddress; +} + +/** + * ERC20 Batch conversion proxy approvals methods + */ + +/** + * Processes the approval transaction of the targeted ERC20 with batch conversion proxy. + * @param request request for an ERC20 payment with/out conversion + * @param account account that will be used to pay the request + * @param version version of the batch conversion proxy, which can be different from request pn version + * @param signerOrProvider the Web3 provider, or signer. Defaults to window.ethereum. + * @param paymentSettings paymentSettings is necessary for conversion payment approval + * @param overrides optionally, override default transaction values, like gas. + */ +export async function approveErc20BatchConversionIfNeeded( + request: ClientTypes.IRequestData, + account: string, + version: string, + signerOrProvider: providers.Provider | Signer = getProvider(), + paymentSettings?: IConversionPaymentSettings, + overrides?: ITransactionOverrides, +): Promise { + if ( + !(await hasErc20BatchConversionApproval( + request, + account, + version, + signerOrProvider, + paymentSettings, + )) + ) { + return approveErc20BatchConversion( + request, + version, + getSigner(signerOrProvider), + paymentSettings, + overrides, + ); + } +} + +/** + * Checks if the batch conversion proxy has the necessary allowance from a given account + * to pay a given request with ERC20 batch conversion proxy + * @param request request for an ERC20 payment with/out conversion + * @param account account that will be used to pay the request + * @param version version of the batch conversion proxy + * @param signerOrProvider the Web3 provider, or signer. Defaults to window.ethereum. + * @param paymentSettings paymentSettings is necessary for conversion payment approval + */ +export async function hasErc20BatchConversionApproval( + request: ClientTypes.IRequestData, + account: string, + version: string, + signerOrProvider: providers.Provider | Signer = getProvider(), + paymentSettings?: IConversionPaymentSettings, +): Promise { + return checkErc20Allowance( + account, + getBatchConversionProxyAddress(request, version, paymentSettings), + signerOrProvider, + getTokenAddress(request, paymentSettings), + request.expectedAmount, + ); +} + +/** + * Processes the transaction to approve the batch conversion proxy to spend signer's tokens to pay + * the request in its payment currency. Can be used with a Multisig contract. + * @param request request for an ERC20 payment with/out conversion + * @param version version of the batch conversion proxy, which can be different from request pn version + * @param signerOrProvider the Web3 provider, or signer. Defaults to window.ethereum. + * @param paymentSettings paymentSettings is necessary for conversion payment approval + * @param overrides optionally, override default transaction values, like gas. + */ +export async function approveErc20BatchConversion( + request: ClientTypes.IRequestData, + version: string, + signerOrProvider: providers.Provider | Signer = getProvider(), + paymentSettings?: IConversionPaymentSettings, + overrides?: ITransactionOverrides, +): Promise { + const preparedTx = prepareApproveErc20BatchConversion( + request, + version, + signerOrProvider, + paymentSettings, + overrides, + ); + const signer = getSigner(signerOrProvider); + const tx = await signer.sendTransaction(preparedTx); + return tx; +} + +/** + * Prepare the transaction to approve the proxy to spend signer's tokens to pay + * the request in its payment currency. Can be used with a Multisig contract. + * @param request request for an ERC20 payment with/out conversion + * @param version version of the batch conversion proxy + * @param signerOrProvider the Web3 provider, or signer. Defaults to window.ethereum. + * @param paymentSettings paymentSettings is necessary for conversion payment approval + * @param overrides optionally, override default transaction values, like gas. + */ +export function prepareApproveErc20BatchConversion( + request: ClientTypes.IRequestData, + version: string, + signerOrProvider: providers.Provider | Signer = getProvider(), + paymentSettings?: IConversionPaymentSettings, + overrides?: ITransactionOverrides, +): IPreparedTransaction { + const encodedTx = encodeApproveErc20BatchConversion( + request, + version, + signerOrProvider, + paymentSettings, + ); + return { + data: encodedTx, + to: getTokenAddress(request, paymentSettings), + value: 0, + ...overrides, + }; +} + +/** + * Encodes the transaction to approve the batch conversion proxy to spend signer's tokens to pay + * the request in its payment currency. Can be used with a Multisig contract. + * @param request request for an ERC20 payment with/out conversion + * @param version version of the batch conversion proxy + * @param signerOrProvider the Web3 provider, or signer. Defaults to window.ethereum. + * @param paymentSettings paymentSettings is necessary for conversion payment approval + */ +export function encodeApproveErc20BatchConversion( + request: ClientTypes.IRequestData, + version: string, + signerOrProvider: providers.Provider | Signer = getProvider(), + paymentSettings?: IConversionPaymentSettings, +): string { + const proxyAddress = getBatchConversionProxyAddress(request, version, paymentSettings); + return encodeApproveAnyErc20( + getTokenAddress(request, paymentSettings), + proxyAddress, + getSigner(signerOrProvider), + ); +} + +/** + * Get the address of the token to interact with, + * if it is a conversion payment, the info is inside paymentSettings + * @param request request for an ERC20 payment with/out conversion + * @param paymentSettings paymentSettings is necessary for conversion payment + * */ +function getTokenAddress( + request: ClientTypes.IRequestData, + paymentSettings?: IConversionPaymentSettings, +): string { + return paymentSettings ? paymentSettings.currency!.value : request.currencyInfo.value; +} diff --git a/packages/smart-contracts/test/contracts/BatchConversionPayments.test.ts b/packages/smart-contracts/test/contracts/BatchConversionPayments.test.ts index 30a5ee9fb..821715760 100644 --- a/packages/smart-contracts/test/contracts/BatchConversionPayments.test.ts +++ b/packages/smart-contracts/test/contracts/BatchConversionPayments.test.ts @@ -10,6 +10,7 @@ import { BatchConversionPayments__factory, BatchConversionPayments, } from '../../src/types'; +import { PaymentTypes } from '@requestnetwork/types'; import { BigNumber, ContractTransaction, Signer } from 'ethers'; import { expect } from 'chai'; import { CurrencyManager } from '@requestnetwork/currency'; @@ -59,7 +60,7 @@ describe('contract: BatchConversionPayments', async () => { let chainlinkPath: ChainlinkConversionPath; // constants inputs for batch functions, both conversion and no-conversion - const emptyCryptoDetails = { + const emptyCryptoDetails: PaymentTypes.CryptoDetails = { tokenAddresses: [], recipients: [], amounts: [], @@ -67,7 +68,7 @@ describe('contract: BatchConversionPayments', async () => { feeAmounts: [], }; - const fauConvDetail = { + const fauConvDetail: PaymentTypes.ConversionDetail = { recipient: '', requestAmount: '100000' + fiatDecimals, path: [USD_hash, FAU_address], @@ -77,7 +78,7 @@ describe('contract: BatchConversionPayments', async () => { maxRateTimespan: '0', }; - const daiConvDetail = { + const daiConvDetail: PaymentTypes.ConversionDetail = { recipient: '', requestAmount: '100000' + fiatDecimals, path: [EUR_hash, USD_hash, DAI_address], @@ -87,14 +88,14 @@ describe('contract: BatchConversionPayments', async () => { maxRateTimespan: '0', }; - const ethConvDetail = { + const ethConvDetail: PaymentTypes.ConversionDetail = { recipient: '', requestAmount: '1000', path: [USD_hash, ETH_hash], paymentReference: referenceExample, feeAmount: '1', - maxToSpend: BigNumber.from(0), - maxRateTimespan: BigNumber.from(0), + maxToSpend: '0', + maxRateTimespan: '0', }; before(async () => { @@ -437,14 +438,14 @@ describe('contract: BatchConversionPayments', async () => { const beforeETHBalanceFrom = await provider.getBalance(await fromSigner.getAddress()); // set inputs: ERC20 cryptoDetails & ethCryptoDetails - const cryptoDetails = { + const cryptoDetails: PaymentTypes.CryptoDetails = { tokenAddresses: [FAU_address], recipients: [to], amounts: ['100000'], paymentReferences: [referenceExample], feeAmounts: ['100'], }; - const ethCryptoDetails = { + const ethCryptoDetails: PaymentTypes.CryptoDetails = { tokenAddresses: [], recipients: [to], amounts: ['1000'], @@ -777,7 +778,7 @@ describe('contract: BatchConversionPayments', async () => { it('cannot transfer if rate is too old', async function () { const wrongConvDetail = Utils.deepCopy(ethConvDetail); - wrongConvDetail.maxRateTimespan = BigNumber.from('1'); + wrongConvDetail.maxRateTimespan = '1'; await expect( batchConversionProxy.batchEthConversionPayments([wrongConvDetail], feeAddress, { value: 1000 + 1 + 42, diff --git a/packages/types/src/payment-types.ts b/packages/types/src/payment-types.ts index a0496013a..0d46da27a 100644 --- a/packages/types/src/payment-types.ts +++ b/packages/types/src/payment-types.ts @@ -320,3 +320,32 @@ export type AllNetworkRetrieverEvents = { paymentEvents: TPaymentNetworkEventType[]; escrowEvents?: EscrowNetworkEvent[]; }; + +// Types used by batch conversion smart contract +/** Input type used by batch conversion proxy to make an ERC20/ETH conversion payment */ +export interface ConversionDetail { + recipient: string; + requestAmount: string; + path: string[]; + paymentReference: string; + feeAmount: string; + maxToSpend: string; + maxRateTimespan: string; +} + +/** Input type used by batch conversion proxy to make an ERC20/ETH no-conversion payment */ +export interface CryptoDetails { + tokenAddresses: Array; + recipients: Array; + amounts: Array; + paymentReferences: Array; + feeAmounts: Array; +} + +/** Input type used by batch conversion proxy to make an ERC20 & ETH, + * and conversion & no-conversion payment through batchRouter */ +export interface MetaDetail { + paymentNetworkId: number; + conversionDetails: ConversionDetail[]; + cryptoDetails: CryptoDetails; +} From 7a9831305c0178c2c45fc747231f2641d08b06de Mon Sep 17 00:00:00 2001 From: olivier7delf <55892112+olivier7delf@users.noreply.github.com> Date: Thu, 8 Sep 2022 09:17:42 +0200 Subject: [PATCH 091/105] update processor with type --- .../src/payment/batch-conversion-proxy.ts | 64 ++++++------------- .../payment/any-to-erc20-batch-proxy.test.ts | 23 +++++++ 2 files changed, 41 insertions(+), 46 deletions(-) diff --git a/packages/payment-processor/src/payment/batch-conversion-proxy.ts b/packages/payment-processor/src/payment/batch-conversion-proxy.ts index 83f45c960..bfe3d08a0 100644 --- a/packages/payment-processor/src/payment/batch-conversion-proxy.ts +++ b/packages/payment-processor/src/payment/batch-conversion-proxy.ts @@ -1,7 +1,7 @@ -import { ContractTransaction, Signer, providers, BigNumber, BigNumberish } from 'ethers'; +import { ContractTransaction, Signer, providers, BigNumber } from 'ethers'; import { batchConversionPaymentsArtifact } from '@requestnetwork/smart-contracts'; import { BatchConversionPayments__factory } from '@requestnetwork/smart-contracts/types'; -import { ClientTypes, ExtensionTypes, RequestLogicTypes } from '@requestnetwork/types'; +import { ClientTypes, RequestLogicTypes, PaymentTypes } from '@requestnetwork/types'; import { ITransactionOverrides } from './transaction-overrides'; import { comparePnTypeAndVersion, getProvider, getRequestPaymentValues, getSigner } from './utils'; import { @@ -14,31 +14,6 @@ import { checkRequestAndGetPathAndCurrency } from './any-to-erc20-proxy'; import { getBatchArgs } from './batch-proxy'; import { checkErc20Allowance, encodeApproveAnyErc20 } from './erc20'; -// Types used by batch conversion smart contract -type ConversionDetail = { - recipient: string; - requestAmount: BigNumberish; - path: string[]; - paymentReference: string; - feeAmount: BigNumberish; - maxToSpend: BigNumberish; - maxRateTimespan: BigNumberish; -}; - -type CryptoDetails = { - tokenAddresses: Array; - recipients: Array; - amounts: Array; - paymentReferences: Array; - feeAmounts: Array; -}; - -type MetaDetail = { - paymentNetworkId: number; - conversionDetails: ConversionDetail[]; - cryptoDetails: CryptoDetails; -}; - /** * Processes a transaction to pay a batch of requests with an ERC20 currency * that is different from the request currency (eg. fiat) @@ -104,13 +79,13 @@ export function encodePayBatchConversionRequest(enrichedRequests: EnrichedReques //**** Create and fill batchRouter function argument: metaDetails ****// - const metaDetails: MetaDetail[] = []; + const metaDetails: PaymentTypes.MetaDetail[] = []; // Variable and constants to get info about each payment network (pn) - let pn0FirstRequest: ClientTypes.IRequestData | undefined; + let firstPn0Request: ClientTypes.IRequestData | undefined; const pn2requests: ClientTypes.IRequestData[] = []; // Constant storing conversion info - const conversionDetails: ConversionDetail[] = []; + const conversionDetails: PaymentTypes.ConversionDetail[] = []; // Iterate throught each enrichedRequests to do checking and retrieve info for (let i = 0; i < enrichedRequests.length; i++) { @@ -119,16 +94,12 @@ export function encodePayBatchConversionRequest(enrichedRequests: EnrichedReques throw new Error('no payment network found'); } if (enrichedRequests[i].paymentNetworkId === 0) { - // set pn0FirstRequest only if it is undefined - pn0FirstRequest = pn0FirstRequest ?? enrichedRequests[i].request; - if (!(iExtension.id === ExtensionTypes.ID.PAYMENT_NETWORK_ANY_TO_ERC20_PROXY)) - throw new Error( - 'request cannot be processed, or is not an pn-erc20-fee-proxy-contract request', - ); + // set firstPn0Request only if it is undefined + firstPn0Request = firstPn0Request ?? enrichedRequests[i].request; if ( !comparePnTypeAndVersion( - getPaymentNetworkExtension(pn0FirstRequest), + getPaymentNetworkExtension(firstPn0Request), enrichedRequests[i].request, ) ) @@ -153,8 +124,8 @@ export function encodePayBatchConversionRequest(enrichedRequests: EnrichedReques } } - // Add conversionDetails to metaDetails - if (pn0FirstRequest) { + // Add ERC20 conversion payments + if (conversionDetails.length > 0) { metaDetails.push({ paymentNetworkId: 0, conversionDetails: conversionDetails, @@ -173,15 +144,16 @@ export function encodePayBatchConversionRequest(enrichedRequests: EnrichedReques const { tokenAddresses, paymentAddresses, amountsToPay, paymentReferences, feesToPay } = getBatchArgs(pn2requests, 'ERC20'); + // add ERC20 no-conversion payments metaDetails.push({ paymentNetworkId: 2, conversionDetails: [], cryptoDetails: { tokenAddresses: tokenAddresses, recipients: paymentAddresses, - amounts: amountsToPay, + amounts: amountsToPay.map((x) => x.toString()), paymentReferences: paymentReferences, - feeAmounts: feesToPay, + feeAmounts: feesToPay.map((x) => x.toString()), }, }); } @@ -193,7 +165,7 @@ export function encodePayBatchConversionRequest(enrichedRequests: EnrichedReques * Get the conversion detail values from one enriched request * @param enrichedRequest enrichedRequest to pay */ -function getInputConversionDetail(enrichedRequest: EnrichedRequest): ConversionDetail { +function getInputConversionDetail(enrichedRequest: EnrichedRequest): PaymentTypes.ConversionDetail { const paymentSettings = enrichedRequest.paymentSettings; if (!paymentSettings) throw Error('the enrichedRequest has no paymentSettings'); @@ -214,12 +186,12 @@ function getInputConversionDetail(enrichedRequest: EnrichedRequest): ConversionD const padFeeAmount = padAmountForChainlink(feeAmount || 0, requestCurrency); return { recipient: paymentAddress, - requestAmount: padRequestAmount, + requestAmount: padRequestAmount.toString(), path: path, paymentReference: `0x${paymentReference}`, - feeAmount: BigNumber.from(padFeeAmount), - maxToSpend: BigNumber.from(paymentSettings.maxToSpend), - maxRateTimespan: BigNumber.from(maxRateTimespan || 0), + feeAmount: padFeeAmount.toString(), + maxToSpend: paymentSettings.maxToSpend.toString(), + maxRateTimespan: maxRateTimespan || '0', }; } diff --git a/packages/payment-processor/test/payment/any-to-erc20-batch-proxy.test.ts b/packages/payment-processor/test/payment/any-to-erc20-batch-proxy.test.ts index 506a28a56..370cfd419 100644 --- a/packages/payment-processor/test/payment/any-to-erc20-batch-proxy.test.ts +++ b/packages/payment-processor/test/payment/any-to-erc20-batch-proxy.test.ts @@ -270,6 +270,29 @@ describe(`[Conversion]: erc20-batch-conversion-proxy`, () => { payBatchConversionProxyRequest(enrichedRequests, batchConvVersion, wallet), ).rejects.toThrowError(`wrong request currencyInfo type`); }); + it('should throw an error if the request has a wrong payment network id', async () => { + request2.extensions = { + // ERC20_FEE_PROXY_CONTRACT instead of ANY_TO_ERC20_PROXY + [PaymentTypes.PAYMENT_NETWORK_ID.ERC20_FEE_PROXY_CONTRACT]: { + events: [], + id: ExtensionTypes.ID.PAYMENT_NETWORK_ERC20_FEE_PROXY_CONTRACT, + type: ExtensionTypes.TYPE.PAYMENT_NETWORK, + values: { + feeAddress, + feeAmount: feeAmount, + paymentAddress: paymentAddress, + salt: 'salt', + }, + version: '0.1.0', + }, + }; + + await expect( + payBatchConversionProxyRequest(enrichedRequests, batchConvVersion, wallet), + ).rejects.toThrowError( + 'request cannot be processed, or is not an pn-any-to-erc20-proxy request', + ); + }); it("should throw an error if one request's currencyInfo has no value", async () => { request2.currencyInfo.value = ''; await expect( From 8fd262f2d094ee259ca1c709598fd77ab66fcd45 Mon Sep 17 00:00:00 2001 From: olivier7delf <55892112+olivier7delf@users.noreply.github.com> Date: Fri, 9 Sep 2022 12:16:04 +0200 Subject: [PATCH 092/105] refacto feeAddress --- .../src/payment/batch-conversion-proxy.ts | 24 +++++++------------ 1 file changed, 9 insertions(+), 15 deletions(-) diff --git a/packages/payment-processor/src/payment/batch-conversion-proxy.ts b/packages/payment-processor/src/payment/batch-conversion-proxy.ts index bfe3d08a0..26ea65ee8 100644 --- a/packages/payment-processor/src/payment/batch-conversion-proxy.ts +++ b/packages/payment-processor/src/payment/batch-conversion-proxy.ts @@ -1,4 +1,4 @@ -import { ContractTransaction, Signer, providers, BigNumber } from 'ethers'; +import { ContractTransaction, Signer, providers, BigNumber, constants } from 'ethers'; import { batchConversionPaymentsArtifact } from '@requestnetwork/smart-contracts'; import { BatchConversionPayments__factory } from '@requestnetwork/smart-contracts/types'; import { ClientTypes, RequestLogicTypes, PaymentTypes } from '@requestnetwork/types'; @@ -23,12 +23,8 @@ import { checkErc20Allowance, encodeApproveAnyErc20 } from './erc20'; * @param version Version of the batch conversion proxy * @param signerOrProvider The Web3 provider, or signer. Defaults to window.ethereum. * @param overrides Optionally, override default transaction values, like gas. - * @dev We only implement batchRouter using the ERC20 functions: - * batchERC20ConversionPaymentsMultiTokens, and batchERC20PaymentsMultiTokensWithReference. - * It implies that paymentNetworkId take only theses values: 0 or 2 - * Next steps: - * - Enable ETH payment within batchRouter function: with/out conversion - * - Enable gas optimizaton: implement the others batch functions + * @dev We only implement batchRouter using two ERC20 functions: + * batchMultiERC20ConversionPayments, and batchMultiERC20Payments. */ export async function payBatchConversionProxyRequest( enrichedRequests: EnrichedRequest[], @@ -42,7 +38,7 @@ export async function payBatchConversionProxyRequest( } /** - * Prepare the transaction to pay a batch of requests through the batch conversion proxy contract, + * Prepare the transaction to pay a batch of ERC20 requests through the batch conversion proxy contract, * it can be used with a Multisig contract. * @param enrichedRequests List of EnrichedRequest to pay * @param version Version of the batch conversion proxy @@ -70,12 +66,7 @@ export function prepareBatchConversionPaymentTransaction( * @param enrichedRequests list of ECR20 requests to pay */ export function encodePayBatchConversionRequest(enrichedRequests: EnrichedRequest[]): string { - // Get fee address - const extension = getPaymentNetworkExtension(enrichedRequests[0].request); - if (!extension) { - throw new Error('no payment network found'); - } - const { feeAddress } = extension.values; + const { feeAddress } = getRequestPaymentValues(enrichedRequests[0].request); //**** Create and fill batchRouter function argument: metaDetails ****// @@ -158,7 +149,10 @@ export function encodePayBatchConversionRequest(enrichedRequests: EnrichedReques }); } const proxyContract = BatchConversionPayments__factory.createInterface(); - return proxyContract.encodeFunctionData('batchRouter', [metaDetails, feeAddress]); + return proxyContract.encodeFunctionData('batchRouter', [ + metaDetails, + feeAddress || constants.AddressZero, + ]); } /** From ea5bc98ed4cc4660bdfe89a8422da20a1abcc6f0 Mon Sep 17 00:00:00 2001 From: olivier7delf <55892112+olivier7delf@users.noreply.github.com> Date: Tue, 13 Sep 2022 12:01:21 +0200 Subject: [PATCH 093/105] refacto batch payment --- .../src/payment/batch-conversion-proxy.ts | 33 ++++++++----------- .../src/payment/batch-proxy.ts | 4 +-- .../payment-processor/src/payment/utils.ts | 16 +++++---- 3 files changed, 25 insertions(+), 28 deletions(-) diff --git a/packages/payment-processor/src/payment/batch-conversion-proxy.ts b/packages/payment-processor/src/payment/batch-conversion-proxy.ts index 26ea65ee8..7e63f4716 100644 --- a/packages/payment-processor/src/payment/batch-conversion-proxy.ts +++ b/packages/payment-processor/src/payment/batch-conversion-proxy.ts @@ -38,7 +38,8 @@ export async function payBatchConversionProxyRequest( } /** - * Prepare the transaction to pay a batch of ERC20 requests through the batch conversion proxy contract, + * Prepares a transaction to pay a batch of requests with an ERC20 currency + * that is different from the request currency (eg. fiat) * it can be used with a Multisig contract. * @param enrichedRequests List of EnrichedRequest to pay * @param version Version of the batch conversion proxy @@ -61,7 +62,8 @@ export function prepareBatchConversionPaymentTransaction( } /** - * Encodes the call to pay a batch conversion of requests through ERC20 or ERC20Conversion proxies + * Encodes a transaction to pay a batch of requests with an ERC20 currency + * that is different from the request currency (eg. fiat) * It can be used with a Multisig contract. * @param enrichedRequests list of ECR20 requests to pay */ @@ -70,8 +72,6 @@ export function encodePayBatchConversionRequest(enrichedRequests: EnrichedReques //**** Create and fill batchRouter function argument: metaDetails ****// - const metaDetails: PaymentTypes.MetaDetail[] = []; - // Variable and constants to get info about each payment network (pn) let firstPn0Request: ClientTypes.IRequestData | undefined; const pn2requests: ClientTypes.IRequestData[] = []; @@ -88,14 +88,12 @@ export function encodePayBatchConversionRequest(enrichedRequests: EnrichedReques // set firstPn0Request only if it is undefined firstPn0Request = firstPn0Request ?? enrichedRequests[i].request; + comparePnTypeAndVersion( + getPaymentNetworkExtension(firstPn0Request!), + enrichedRequests[i].request, + ); if ( - !comparePnTypeAndVersion( - getPaymentNetworkExtension(firstPn0Request), - enrichedRequests[i].request, - ) - ) - throw new Error(`Every payment network type and version must be identical`); - if ( + // the type used must a fiat or an ERC20 ![RequestLogicTypes.CURRENCY.ISO4217, RequestLogicTypes.CURRENCY.ERC20].includes( enrichedRequests[i].request.currencyInfo.type, ) @@ -104,17 +102,14 @@ export function encodePayBatchConversionRequest(enrichedRequests: EnrichedReques conversionDetails.push(getInputConversionDetail(enrichedRequests[i])); } else if (enrichedRequests[i].paymentNetworkId === 2) { pn2requests.push(enrichedRequests[i].request); - if ( - !comparePnTypeAndVersion( - getPaymentNetworkExtension(pn2requests[0]), - enrichedRequests[i].request, - ) - ) { - throw new Error(`Every payment network type and version must be identical`); - } + comparePnTypeAndVersion( + getPaymentNetworkExtension(pn2requests[0]), + enrichedRequests[i].request, + ); } } + const metaDetails: PaymentTypes.MetaDetail[] = []; // Add ERC20 conversion payments if (conversionDetails.length > 0) { metaDetails.push({ diff --git a/packages/payment-processor/src/payment/batch-proxy.ts b/packages/payment-processor/src/payment/batch-proxy.ts index 850261d86..32e6a222d 100644 --- a/packages/payment-processor/src/payment/batch-proxy.ts +++ b/packages/payment-processor/src/payment/batch-proxy.ts @@ -116,9 +116,7 @@ export function encodePayBatchRequest(requests: ClientTypes.IRequestData[]): str const pn = getPaymentNetworkExtension(requests[0]); for (let i = 0; i < requests.length; i++) { validateErc20FeeProxyRequest(requests[i]); - if (!comparePnTypeAndVersion(pn, requests[i])) { - throw new Error(`Every payment network type and version must be identical`); - } + comparePnTypeAndVersion(pn, requests[i]); } if (isMultiTokens) { diff --git a/packages/payment-processor/src/payment/utils.ts b/packages/payment-processor/src/payment/utils.ts index 7d870a5ff..0415d11c8 100644 --- a/packages/payment-processor/src/payment/utils.ts +++ b/packages/payment-processor/src/payment/utils.ts @@ -315,18 +315,22 @@ export function getAmountToPay( /** * Compare 2 payment networks type and version in request's extension + * and throw an exception if they are different * @param pn payment network * @param request - * @returns true if type and version are identique else false */ export function comparePnTypeAndVersion( pn: ExtensionTypes.IState | undefined, request: ClientTypes.IRequestData, -): boolean { - return ( - pn?.type === getPaymentNetworkExtension(request)?.type && - pn?.version === getPaymentNetworkExtension(request)?.version - ); +): void { + if ( + !( + pn?.type === getPaymentNetworkExtension(request)?.type && + pn?.version === getPaymentNetworkExtension(request)?.version + ) + ) { + throw new Error(`Every payment network type and version must be identical`); + } } /** From f31725d373eb1b65c4a787573669d7bf8a631ed3 Mon Sep 17 00:00:00 2001 From: olivier7delf <55892112+olivier7delf@users.noreply.github.com> Date: Wed, 14 Sep 2022 09:49:32 +0200 Subject: [PATCH 094/105] clean history --- .../scripts-create2/compute-one-address.ts | 1 - .../scripts-create2/constructor-args.ts | 14 ---- .../contract-setup/adminTasks.ts | 80 +------------------ .../setupBatchConversionPayments.ts | 79 ------------------ .../contract-setup/setupBatchPayments.ts | 2 +- .../scripts-create2/contract-setup/setups.ts | 5 -- .../smart-contracts/scripts-create2/deploy.ts | 8 -- .../smart-contracts/scripts-create2/utils.ts | 19 ++--- .../smart-contracts/scripts-create2/verify.ts | 7 -- 9 files changed, 13 insertions(+), 202 deletions(-) delete mode 100644 packages/smart-contracts/scripts-create2/contract-setup/setupBatchConversionPayments.ts diff --git a/packages/smart-contracts/scripts-create2/compute-one-address.ts b/packages/smart-contracts/scripts-create2/compute-one-address.ts index 0e558437b..f2803616e 100644 --- a/packages/smart-contracts/scripts-create2/compute-one-address.ts +++ b/packages/smart-contracts/scripts-create2/compute-one-address.ts @@ -57,7 +57,6 @@ export const computeCreate2DeploymentAddressesFromList = async ( case 'Erc20ConversionProxy': case 'ERC20EscrowToPay': case 'BatchPayments': - case 'BatchConversionPayments': case 'ERC20SwapToConversion': { try { const constructorArgs = getConstructorArgs(contract, hre.network.name); diff --git a/packages/smart-contracts/scripts-create2/constructor-args.ts b/packages/smart-contracts/scripts-create2/constructor-args.ts index cac996383..db9d4952e 100644 --- a/packages/smart-contracts/scripts-create2/constructor-args.ts +++ b/packages/smart-contracts/scripts-create2/constructor-args.ts @@ -49,20 +49,6 @@ export const getConstructorArgs = (contract: string, network?: string): string[] getAdminWalletAddress(contract), ]; } - case 'BatchConversionPayments': { - if (!network) { - throw new Error( - 'Batch conversion contract requires network parameter to get correct address of erc20FeeProxy, erc20ConversionFeeProxy, ethereumFeeProxy, and ethereumConversionFeeProxy', - ); - } - return [ - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - getAdminWalletAddress(contract), - ]; - } default: return []; } diff --git a/packages/smart-contracts/scripts-create2/contract-setup/adminTasks.ts b/packages/smart-contracts/scripts-create2/contract-setup/adminTasks.ts index f689dbd63..a2c8d6ffc 100644 --- a/packages/smart-contracts/scripts-create2/contract-setup/adminTasks.ts +++ b/packages/smart-contracts/scripts-create2/contract-setup/adminTasks.ts @@ -6,14 +6,7 @@ import { BigNumber } from 'ethers'; // Fees: 0.5% export const REQUEST_SWAP_FEES = 5; // Batch Fees: .3% -/** - * BATCH_FEE_DEPRECATED is only used with batchProxy (NOT with batchConversionProxy) - */ -export const BATCH_FEE_DEPRECATED = 3; -export const BATCH_FEE = 30; - -// Batch Fees: .3% -export const BATCH_CONVERSION_FEE = 30; +export const BATCH_FEE = 3; export const updateChainlinkConversionPath = async ( contract: any, @@ -57,41 +50,16 @@ export const updateRequestSwapFees = async ( } }; -/** - * Updates batch and batchConversion batchFee dependant of the proxy selected - * @param batchConversionProxy batchConversionProxy must be specified because - * it impact the calcul of the batch fees - */ export const updateBatchPaymentFees = async ( contract: any, nonce: number, gasPrice: BigNumber, - batchConversionProxy = true, ): Promise => { - const batchFee = batchConversionProxy ? BATCH_FEE : BATCH_FEE_DEPRECATED; const currentFees = await contract.batchFee(); - if (currentFees !== batchFee) { + if (currentFees !== BATCH_FEE) { // Log is useful to have a direct view on was is being updated - console.log(`BatchFees, currentFees: ${currentFees.toString()}, new fees: ${batchFee}`); - await contract.setBatchFee(batchFee, { nonce: nonce, gasPrice: gasPrice }); - } -}; - -export const updateBatchConversionPaymentFees = async ( - contract: any, - nonce: number, - gasPrice: BigNumber, -): Promise => { - const currentFees = await contract.batchConversionFee(); - if (currentFees !== BATCH_CONVERSION_FEE) { - // Log is useful to have a direct view on was is being updated - console.log( - `BatchConversionFees, currentFees: ${currentFees.toString()}, new fees: ${BATCH_CONVERSION_FEE}`, - ); - await contract.setBatchConversionFee(BATCH_CONVERSION_FEE, { - nonce: nonce, - gasPrice: gasPrice, - }); + console.log(`currentFees: ${currentFees.toString()}, new fees: ${BATCH_FEE}`); + await contract.setBatchFee(BATCH_FEE, { nonce: nonce, gasPrice: gasPrice }); } }; @@ -128,43 +96,3 @@ export const updatePaymentEthFeeProxy = async ( }); } }; - -/** - * Update the address of a payment proxy used by batch conversion contract - */ -export const updateBatchConversionPaymentProxy = async ( - contract: any, - network: string, - nonce: number, - gasPrice: BigNumber, - proxyName: 'eth' | 'ethConversion' | 'erc20' | 'erc20Conversion', -): Promise => { - let proxyAddress: string; - let batchSetProxy: any; - let currentAddress: string; - if (proxyName === 'eth') { - proxyAddress = artifacts.ethereumFeeProxyArtifact.getAddress(network); - batchSetProxy = contract.setPaymentEthProxy; - currentAddress = await contract.paymentEthProxy(); - } else if (proxyName === 'ethConversion') { - proxyAddress = artifacts.ethConversionArtifact.getAddress(network); - batchSetProxy = contract.setPaymentEthConversionProxy; - currentAddress = await contract.paymentEthConversionProxy(); - } else if (proxyName === 'erc20') { - proxyAddress = artifacts.erc20FeeProxyArtifact.getAddress(network); - batchSetProxy = contract.setPaymentErc20Proxy; - currentAddress = await contract.paymentErc20Proxy(); - } else { - // "erc20Conversion" - proxyAddress = artifacts.erc20ConversionProxy.getAddress(network); - batchSetProxy = contract.setPaymentErc20ConversionProxy; - currentAddress = await contract.paymentErc20ConversionProxy(); - } - - if (currentAddress !== proxyAddress) { - await batchSetProxy(proxyAddress, { - nonce: nonce, - gasPrice: gasPrice, - }); - } -}; diff --git a/packages/smart-contracts/scripts-create2/contract-setup/setupBatchConversionPayments.ts b/packages/smart-contracts/scripts-create2/contract-setup/setupBatchConversionPayments.ts deleted file mode 100644 index e1530772d..000000000 --- a/packages/smart-contracts/scripts-create2/contract-setup/setupBatchConversionPayments.ts +++ /dev/null @@ -1,79 +0,0 @@ -import { batchConversionPaymentsArtifact } from '../../src/lib'; -import { HardhatRuntimeEnvironmentExtended } from '../types'; -import utils from '@requestnetwork/utils'; -import { - updateBatchPaymentFees, - updateBatchConversionPaymentFees, - updateBatchConversionPaymentProxy, -} from './adminTasks'; - -/** - * Updates the values of the batch fees of the BatchConversionPayments contract, if needed - * @param contractAddress address of the BatchConversionPayments Proxy - * @param hre Hardhat runtime environment - */ -export const setupBatchConversionPayments = async ( - contractAddress: string, - hre: HardhatRuntimeEnvironmentExtended, -): Promise => { - // Setup contract parameters - const batchConversionPaymentContract = new hre.ethers.Contract( - contractAddress, - batchConversionPaymentsArtifact.getContractAbi(), - ); - await Promise.all( - hre.config.xdeploy.networks.map(async (network) => { - let provider; - if (network === 'celo') { - provider = utils.getCeloProvider(); - } else { - provider = utils.getDefaultProvider(network); - } - const wallet = new hre.ethers.Wallet(hre.config.xdeploy.signer, provider); - const signer = wallet.connect(provider); - const batchConversionPaymentConnected = await batchConversionPaymentContract.connect(signer); - const adminNonce = await signer.getTransactionCount(); - const gasPrice = await provider.getGasPrice(); - - // start from the adminNonce, increase gasPrice if needed - const gasCoef = 2; - await Promise.all([ - updateBatchPaymentFees(batchConversionPaymentConnected, adminNonce, gasPrice.mul(gasCoef)), - updateBatchConversionPaymentFees( - batchConversionPaymentConnected, - adminNonce + 1, - gasPrice.mul(gasCoef), - ), - updateBatchConversionPaymentProxy( - batchConversionPaymentConnected, - network, - adminNonce + 2, - gasPrice.mul(gasCoef), - 'erc20', - ), - updateBatchConversionPaymentProxy( - batchConversionPaymentConnected, - network, - adminNonce + 3, - gasPrice.mul(gasCoef), - 'eth', - ), - updateBatchConversionPaymentProxy( - batchConversionPaymentConnected, - network, - adminNonce + 4, - gasPrice.mul(gasCoef), - 'erc20Conversion', - ), - updateBatchConversionPaymentProxy( - batchConversionPaymentConnected, - network, - adminNonce + 5, - gasPrice.mul(gasCoef), - 'ethConversion', - ), - ]); - }), - ); - console.log('Setup for setupBatchConversionPayment successfull'); -}; diff --git a/packages/smart-contracts/scripts-create2/contract-setup/setupBatchPayments.ts b/packages/smart-contracts/scripts-create2/contract-setup/setupBatchPayments.ts index e48818107..78fa718e0 100644 --- a/packages/smart-contracts/scripts-create2/contract-setup/setupBatchPayments.ts +++ b/packages/smart-contracts/scripts-create2/contract-setup/setupBatchPayments.ts @@ -37,7 +37,7 @@ export const setupBatchPayments = async ( // start from the adminNonce, increase gasPrice if needed await Promise.all([ - updateBatchPaymentFees(batchPaymentConnected, adminNonce, gasPrice.mul(2), false), + updateBatchPaymentFees(batchPaymentConnected, adminNonce, gasPrice.mul(2)), updatePaymentErc20FeeProxy(batchPaymentConnected, network, adminNonce + 1, gasPrice.mul(2)), updatePaymentEthFeeProxy(batchPaymentConnected, network, adminNonce + 2, gasPrice.mul(2)), ]); diff --git a/packages/smart-contracts/scripts-create2/contract-setup/setups.ts b/packages/smart-contracts/scripts-create2/contract-setup/setups.ts index 79cc90b1b..cda45affa 100644 --- a/packages/smart-contracts/scripts-create2/contract-setup/setups.ts +++ b/packages/smart-contracts/scripts-create2/contract-setup/setups.ts @@ -1,7 +1,6 @@ import { HardhatRuntimeEnvironmentExtended } from '../types'; import { setupETHConversionProxy } from './setupETHConversionProxy'; import { setupBatchPayments } from './setupBatchPayments'; -import { setupBatchConversionPayments } from './setupBatchConversionPayments'; import { setupERC20SwapToConversion } from './setupERC20SwapToConversion'; /** @@ -28,10 +27,6 @@ export const setupContract = async ( await setupBatchPayments(contractAddress, hre); break; } - case 'BatchConversionPayments': { - await setupBatchConversionPayments(contractAddress, hre); - break; - } default: { console.log('Contract name not found'); break; diff --git a/packages/smart-contracts/scripts-create2/deploy.ts b/packages/smart-contracts/scripts-create2/deploy.ts index 737314bf3..23dbfc931 100644 --- a/packages/smart-contracts/scripts-create2/deploy.ts +++ b/packages/smart-contracts/scripts-create2/deploy.ts @@ -5,7 +5,6 @@ import { xdeploy } from './xdeployer'; import { getConstructorArgs } from './constructor-args'; import { setupERC20SwapToConversion } from './contract-setup'; import { setupBatchPayments } from './contract-setup/setupBatchPayments'; -import { setupBatchConversionPayments } from './contract-setup/setupBatchConversionPayments'; // Deploys, set up the contracts and returns the address export const deployOneWithCreate2 = async ( @@ -80,13 +79,6 @@ export const deployWithCreate2FromList = async ( await setupBatchPayments(address, hre); break; } - case 'BatchConversionPayments': { - const network = hre.config.xdeploy.networks[0]; - const constructorArgs = getConstructorArgs(contract, network); - const address = await deployOneWithCreate2({ contract, constructorArgs }, hre); - await setupBatchConversionPayments(address, hre); - break; - } // Other cases to add when necessary default: throw new Error(`The contract ${contract} is not to be deployed using the CREATE2 scheme`); diff --git a/packages/smart-contracts/scripts-create2/utils.ts b/packages/smart-contracts/scripts-create2/utils.ts index f4fe002fa..6956b562a 100644 --- a/packages/smart-contracts/scripts-create2/utils.ts +++ b/packages/smart-contracts/scripts-create2/utils.ts @@ -7,15 +7,14 @@ import * as artifacts from '../src/lib'; * If you want to skip deploying one or more, then comment them out in the list bellow. */ export const create2ContractDeploymentList = [ - // 'EthereumProxy', - // 'EthereumFeeProxy', - // 'EthConversionProxy', - // 'ERC20FeeProxy', - // 'Erc20ConversionProxy', - // 'ERC20SwapToConversion', - // 'ERC20EscrowToPay', - // 'BatchPayments', - 'BatchConversionPayments', + 'EthereumProxy', + 'EthereumFeeProxy', + 'EthConversionProxy', + 'ERC20FeeProxy', + 'Erc20ConversionProxy', + 'ERC20SwapToConversion', + 'ERC20EscrowToPay', + 'BatchPayments', ]; /** @@ -50,8 +49,6 @@ export const getArtifact = (contract: string): artifacts.ContractArtifact Date: Wed, 14 Sep 2022 12:26:07 +0200 Subject: [PATCH 095/105] refacto batch-conversion-proxy --- .../src/payment/batch-conversion-proxy.ts | 75 ++++++++----------- .../payment-processor/src/payment/utils.ts | 21 +++--- .../payment/any-to-erc20-batch-proxy.test.ts | 73 ++++++++++-------- 3 files changed, 88 insertions(+), 81 deletions(-) diff --git a/packages/payment-processor/src/payment/batch-conversion-proxy.ts b/packages/payment-processor/src/payment/batch-conversion-proxy.ts index 7e63f4716..f1ba5efd0 100644 --- a/packages/payment-processor/src/payment/batch-conversion-proxy.ts +++ b/packages/payment-processor/src/payment/batch-conversion-proxy.ts @@ -3,7 +3,13 @@ import { batchConversionPaymentsArtifact } from '@requestnetwork/smart-contracts import { BatchConversionPayments__factory } from '@requestnetwork/smart-contracts/types'; import { ClientTypes, RequestLogicTypes, PaymentTypes } from '@requestnetwork/types'; import { ITransactionOverrides } from './transaction-overrides'; -import { comparePnTypeAndVersion, getProvider, getRequestPaymentValues, getSigner } from './utils'; +import { + comparePnTypeAndVersion, + getProvider, + getProxyAddress, + getRequestPaymentValues, + getSigner, +} from './utils'; import { padAmountForChainlink, getPaymentNetworkExtension, @@ -13,6 +19,8 @@ import { EnrichedRequest, IConversionPaymentSettings } from './index'; import { checkRequestAndGetPathAndCurrency } from './any-to-erc20-proxy'; import { getBatchArgs } from './batch-proxy'; import { checkErc20Allowance, encodeApproveAnyErc20 } from './erc20'; +import { BATCH_PAYMENT_NETWORK_ID } from '@requestnetwork/types/dist/payment-types'; +import { IState } from 'types/dist/extension-types'; /** * Processes a transaction to pay a batch of requests with an ERC20 currency @@ -49,18 +57,13 @@ export function prepareBatchConversionPaymentTransaction( version: string, ): IPreparedTransaction { const encodedTx = encodePayBatchConversionRequest(enrichedRequests); - const proxyAddress = getBatchConversionProxyAddress( - enrichedRequests[0].request, - version, - enrichedRequests[0].paymentSettings, - ); + const proxyAddress = getBatchConversionProxyAddress(enrichedRequests[0].request, version); return { data: encodedTx, to: proxyAddress, value: 0, }; } - /** * Encodes a transaction to pay a batch of requests with an ERC20 currency * that is different from the request currency (eg. fiat) @@ -72,26 +75,20 @@ export function encodePayBatchConversionRequest(enrichedRequests: EnrichedReques //**** Create and fill batchRouter function argument: metaDetails ****// - // Variable and constants to get info about each payment network (pn) - let firstPn0Request: ClientTypes.IRequestData | undefined; + let firstPn0Extension: IState | undefined; const pn2requests: ClientTypes.IRequestData[] = []; - // Constant storing conversion info const conversionDetails: PaymentTypes.ConversionDetail[] = []; // Iterate throught each enrichedRequests to do checking and retrieve info for (let i = 0; i < enrichedRequests.length; i++) { - const iExtension = getPaymentNetworkExtension(enrichedRequests[i].request); - if (!iExtension) { - throw new Error('no payment network found'); - } - if (enrichedRequests[i].paymentNetworkId === 0) { - // set firstPn0Request only if it is undefined - firstPn0Request = firstPn0Request ?? enrichedRequests[i].request; + if ( + enrichedRequests[i].paymentNetworkId === + BATCH_PAYMENT_NETWORK_ID.BATCH_MULTI_ERC20_CONVERSION_PAYMENTS + ) { + firstPn0Extension = + firstPn0Extension ?? getPaymentNetworkExtension(enrichedRequests[i].request); - comparePnTypeAndVersion( - getPaymentNetworkExtension(firstPn0Request!), - enrichedRequests[i].request, - ); + comparePnTypeAndVersion(firstPn0Extension, enrichedRequests[i].request); if ( // the type used must a fiat or an ERC20 ![RequestLogicTypes.CURRENCY.ISO4217, RequestLogicTypes.CURRENCY.ERC20].includes( @@ -100,7 +97,9 @@ export function encodePayBatchConversionRequest(enrichedRequests: EnrichedReques ) throw new Error(`wrong request currencyInfo type`); conversionDetails.push(getInputConversionDetail(enrichedRequests[i])); - } else if (enrichedRequests[i].paymentNetworkId === 2) { + } else if ( + enrichedRequests[i].paymentNetworkId === BATCH_PAYMENT_NETWORK_ID.BATCH_MULTI_ERC20_PAYMENTS + ) { pn2requests.push(enrichedRequests[i].request); comparePnTypeAndVersion( getPaymentNetworkExtension(pn2requests[0]), @@ -113,7 +112,7 @@ export function encodePayBatchConversionRequest(enrichedRequests: EnrichedReques // Add ERC20 conversion payments if (conversionDetails.length > 0) { metaDetails.push({ - paymentNetworkId: 0, + paymentNetworkId: BATCH_PAYMENT_NETWORK_ID.BATCH_MULTI_ERC20_CONVERSION_PAYMENTS, conversionDetails: conversionDetails, cryptoDetails: { tokenAddresses: [], @@ -132,7 +131,7 @@ export function encodePayBatchConversionRequest(enrichedRequests: EnrichedReques // add ERC20 no-conversion payments metaDetails.push({ - paymentNetworkId: 2, + paymentNetworkId: BATCH_PAYMENT_NETWORK_ID.BATCH_MULTI_ERC20_PAYMENTS, conversionDetails: [], cryptoDetails: { tokenAddresses: tokenAddresses, @@ -184,31 +183,23 @@ function getInputConversionDetail(enrichedRequest: EnrichedRequest): PaymentType }; } +function getBatchDeploymentInformation( + network: string, + version: string, +): { address: string } | null { + return { address: batchConversionPaymentsArtifact.getAddress(network, version) }; +} + /** * Gets batch conversion contract Address * @param request request for an ERC20 payment with/out conversion * @param version of the batch conversion proxy - * @param paymentSettings paymentSettings is necessary for conversion payment */ export function getBatchConversionProxyAddress( request: ClientTypes.IRequestData, version: string, - paymentSettings?: IConversionPaymentSettings, ): string { - // Get the network - let network = request.currencyInfo.network; - if (paymentSettings?.currency?.network) { - network = paymentSettings.currency.network; - } - if (!network) throw new Error('Cannot pay with a currency missing a network'); - - // Get the proxy address - const proxyAddress = batchConversionPaymentsArtifact.getAddress(network, version); - if (!proxyAddress) - throw new Error( - `No deployment found on the network ${network}, associated with the version ${version}`, - ); - return proxyAddress; + return getProxyAddress(request, getBatchDeploymentInformation, version); } /** @@ -269,7 +260,7 @@ export async function hasErc20BatchConversionApproval( ): Promise { return checkErc20Allowance( account, - getBatchConversionProxyAddress(request, version, paymentSettings), + getBatchConversionProxyAddress(request, version), signerOrProvider, getTokenAddress(request, paymentSettings), request.expectedAmount, @@ -348,7 +339,7 @@ export function encodeApproveErc20BatchConversion( signerOrProvider: providers.Provider | Signer = getProvider(), paymentSettings?: IConversionPaymentSettings, ): string { - const proxyAddress = getBatchConversionProxyAddress(request, version, paymentSettings); + const proxyAddress = getBatchConversionProxyAddress(request, version); return encodeApproveAnyErc20( getTokenAddress(request, paymentSettings), proxyAddress, diff --git a/packages/payment-processor/src/payment/utils.ts b/packages/payment-processor/src/payment/utils.ts index 0415d11c8..81c85c00b 100644 --- a/packages/payment-processor/src/payment/utils.ts +++ b/packages/payment-processor/src/payment/utils.ts @@ -119,7 +119,7 @@ export function getPaymentExtensionVersion(request: ClientTypes.IRequestData): s return extension.version; } -const getProxyNetwork = ( +export const getProxyNetwork = ( pn: ExtensionTypes.IState, currency: RequestLogicTypes.ICurrency, ): string => { @@ -132,18 +132,22 @@ const getProxyNetwork = ( throw new Error('Payment currency must have a network'); }; +/** + * @param version version has to be set to get batch conversion proxy + */ export const getProxyAddress = ( request: ClientTypes.IRequestData, getDeploymentInformation: (network: string, version: string) => { address: string } | null, + version?: string, ): string => { const pn = getPaymentNetworkExtension(request); if (!pn) { throw new Error('PaymentNetwork not found'); } const network = getProxyNetwork(pn, request.currencyInfo); - const deploymentInfo = getDeploymentInformation(network, pn.version); + const deploymentInfo = getDeploymentInformation(network, version || pn.version); if (!deploymentInfo) { - throw new Error(`No deployment found for network ${network}, version ${pn.version}`); + throw new Error(`No deployment found for network ${network}, version ${version || pn.version}`); } return deploymentInfo.address; }; @@ -323,12 +327,11 @@ export function comparePnTypeAndVersion( pn: ExtensionTypes.IState | undefined, request: ClientTypes.IRequestData, ): void { - if ( - !( - pn?.type === getPaymentNetworkExtension(request)?.type && - pn?.version === getPaymentNetworkExtension(request)?.version - ) - ) { + const extension = getPaymentNetworkExtension(request); + if (!extension) { + throw new Error('no payment network found'); + } + if (!(pn?.type === extension.type && pn?.version === extension.version)) { throw new Error(`Every payment network type and version must be identical`); } } diff --git a/packages/payment-processor/test/payment/any-to-erc20-batch-proxy.test.ts b/packages/payment-processor/test/payment/any-to-erc20-batch-proxy.test.ts index 370cfd419..02bed9dc7 100644 --- a/packages/payment-processor/test/payment/any-to-erc20-batch-proxy.test.ts +++ b/packages/payment-processor/test/payment/any-to-erc20-batch-proxy.test.ts @@ -22,6 +22,7 @@ import { sameCurrencyValue } from './shared'; import { batchConversionPaymentsArtifact } from '@requestnetwork/smart-contracts'; import { UnsupportedCurrencyError } from '@requestnetwork/currency'; import { ERC20__factory } from '@requestnetwork/smart-contracts/types'; +import { BATCH_PAYMENT_NETWORK_ID } from '@requestnetwork/types/dist/payment-types'; /* eslint-disable no-magic-numbers */ /* eslint-disable @typescript-eslint/no-unused-expressions */ @@ -34,7 +35,7 @@ const alphaPaymentSettings: IConversionPaymentSettings = { value: erc20ContractAddress, network: 'private', }, - maxToSpend: BigNumber.from(2).pow(250).sub(1), // is updated later + maxToSpend: '10000000000000000000000000000', currencyManager, }; @@ -183,35 +184,31 @@ const expectedEncoding = ( describe(`[Conversion]: erc20-batch-conversion-proxy`, () => { beforeAll(async () => { - // revoke DAI approval + // revoke DAI and FAU approvals await revokeErc20Approval( - getBatchConversionProxyAddress(validEuroRequest, batchConvVersion, alphaPaymentSettings), + getBatchConversionProxyAddress(validEuroRequest, batchConvVersion), erc20ContractAddress, wallet, ); - // revoke FAU approval await revokeErc20Approval( getBatchConversionProxyAddress(fauValidRequest, batchConvVersion), erc20ContractAddress, wallet, ); - // maxToSpend should be around the amountToPay * 1.03, it depends of the front end - // we do a simplification for the purpose of the test with: requestedAmount < maxToSpend < payeerBalance - alphaPaymentSettings.maxToSpend = '10000000000000000000000000000'; }); beforeEach(() => { jest.restoreAllMocks(); - request1 = Utils.deepCopy(validEuroRequest) as ClientTypes.IRequestData; - request2 = Utils.deepCopy(validEuroRequest) as ClientTypes.IRequestData; + request1 = Utils.deepCopy(validEuroRequest); + request2 = Utils.deepCopy(validEuroRequest); enrichedRequests = [ { - paymentNetworkId: 0, + paymentNetworkId: BATCH_PAYMENT_NETWORK_ID.BATCH_MULTI_ERC20_CONVERSION_PAYMENTS, request: request1, paymentSettings: alphaPaymentSettings, }, { - paymentNetworkId: 0, + paymentNetworkId: BATCH_PAYMENT_NETWORK_ID.BATCH_MULTI_ERC20_CONVERSION_PAYMENTS, request: request2, paymentSettings: alphaPaymentSettings, }, @@ -224,7 +221,7 @@ describe(`[Conversion]: erc20-batch-conversion-proxy`, () => { payBatchConversionProxyRequest( [ { - paymentNetworkId: 0, + paymentNetworkId: BATCH_PAYMENT_NETWORK_ID.BATCH_MULTI_ERC20_CONVERSION_PAYMENTS, request: request1, paymentSettings: { ...alphaPaymentSettings, @@ -245,7 +242,6 @@ describe(`[Conversion]: erc20-batch-conversion-proxy`, () => { }), ); }); - it('should throw an error if request has no extension', async () => { const request = Utils.deepCopy(validEuroRequest); request.extensions = [] as any; @@ -254,7 +250,7 @@ describe(`[Conversion]: erc20-batch-conversion-proxy`, () => { payBatchConversionProxyRequest( [ { - paymentNetworkId: 0, + paymentNetworkId: BATCH_PAYMENT_NETWORK_ID.BATCH_MULTI_ERC20_CONVERSION_PAYMENTS, request: request, paymentSettings: alphaPaymentSettings, }, @@ -264,6 +260,21 @@ describe(`[Conversion]: erc20-batch-conversion-proxy`, () => { ), ).rejects.toThrowError('no payment network found'); }); + it('should throw an error if request has no paymentSettings', async () => { + await expect( + payBatchConversionProxyRequest( + [ + { + paymentNetworkId: BATCH_PAYMENT_NETWORK_ID.BATCH_MULTI_ERC20_CONVERSION_PAYMENTS, + request: validEuroRequest, + paymentSettings: undefined, + }, + ], + batchConvVersion, + wallet, + ), + ).rejects.toThrowError('the enrichedRequest has no paymentSettings'); + }); it('should throw an error if the request is ETH', async () => { request2.currencyInfo.type = RequestLogicTypes.CURRENCY.ETH; await expect( @@ -326,7 +337,7 @@ describe(`[Conversion]: erc20-batch-conversion-proxy`, () => { await payBatchConversionProxyRequest( [ { - paymentNetworkId: 0, + paymentNetworkId: BATCH_PAYMENT_NETWORK_ID.BATCH_MULTI_ERC20_CONVERSION_PAYMENTS, request: request1, paymentSettings: alphaPaymentSettings, }, @@ -338,7 +349,7 @@ describe(`[Conversion]: erc20-batch-conversion-proxy`, () => { expect(spy).toHaveBeenCalledWith({ data: '0xf0fa379f0000000000000000000000000000000000000000000000000000000000000040000000000000000000000000c5fdf4076b8f3a5357c5e395ab970b5b54098fef0000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000024000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000020000000000000000000000000f17f52151ebef6c7334fad080c5704d77216b7320000000000000000000000000000000000000000000000000000000005f5e10000000000000000000000000000000000000000000000000000000000000000e0000000000000000000000000000000000000000000000000000000000000016000000000000000000000000000000000000000000000000000000000001e84800000000000000000000000000000000000000000204fce5e3e250261100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000300000000000000000000000017b4158805772ced11225e77339f90beb5aae968000000000000000000000000775eb53d00dd0acd3ec1696472105d579b9b386b00000000000000000000000038cf23c52bb4b13f051aec09580a2de845a7fa35000000000000000000000000000000000000000000000000000000000000000886dfbccad783599a00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000c000000000000000000000000000000000000000000000000000000000000000e00000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000012000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000', gasPrice: '20000000000', - to: getBatchConversionProxyAddress(request1, '0.1.0', alphaPaymentSettings), + to: getBatchConversionProxyAddress(request1, '0.1.0'), value: 0, }); wallet.sendTransaction = originalSendTransaction; @@ -368,7 +379,7 @@ describe(`[Conversion]: erc20-batch-conversion-proxy`, () => { const tx = await payBatchConversionProxyRequest( [ { - paymentNetworkId: 0, + paymentNetworkId: BATCH_PAYMENT_NETWORK_ID.BATCH_MULTI_ERC20_CONVERSION_PAYMENTS, request: validEuroRequest, paymentSettings: alphaPaymentSettings, }, @@ -395,7 +406,7 @@ describe(`[Conversion]: erc20-batch-conversion-proxy`, () => { expect(BigNumber.from(balanceEthBefore).sub(balanceEthAfter).toNumber()).toBeGreaterThan(0); expect( BigNumber.from(balanceTokenBefore).sub(BigNumber.from(balanceTokenAfter)), - // Here is simplified approximation of the calcul + // Calculation // expectedAmount: 1.00 // feeAmount: + .02 // = 1.02 @@ -417,12 +428,12 @@ describe(`[Conversion]: erc20-batch-conversion-proxy`, () => { const tx = await payBatchConversionProxyRequest( [ { - paymentNetworkId: 0, + paymentNetworkId: BATCH_PAYMENT_NETWORK_ID.BATCH_MULTI_ERC20_CONVERSION_PAYMENTS, request: validEuroRequest, paymentSettings: alphaPaymentSettings, }, { - paymentNetworkId: 0, + paymentNetworkId: BATCH_PAYMENT_NETWORK_ID.BATCH_MULTI_ERC20_CONVERSION_PAYMENTS, request: validEuroRequest, paymentSettings: alphaPaymentSettings, }, @@ -451,7 +462,7 @@ describe(`[Conversion]: erc20-batch-conversion-proxy`, () => { expectedAmout, ); }); - it('should convert and pay two requests in EUR with ERC20 and one ERC20 payment', async () => { + it('should pay 3 heterogeneous ERC20 payments with and without conversion', async () => { // Get the balances to compare after payment const balanceEthBefore = await wallet.getBalance(); const balanceTokenBefore = await ERC20__factory.connect( @@ -463,17 +474,17 @@ describe(`[Conversion]: erc20-batch-conversion-proxy`, () => { const tx = await payBatchConversionProxyRequest( [ { - paymentNetworkId: 0, + paymentNetworkId: BATCH_PAYMENT_NETWORK_ID.BATCH_MULTI_ERC20_CONVERSION_PAYMENTS, request: validEuroRequest, paymentSettings: alphaPaymentSettings, }, { - paymentNetworkId: 0, + paymentNetworkId: BATCH_PAYMENT_NETWORK_ID.BATCH_MULTI_ERC20_CONVERSION_PAYMENTS, request: validEuroRequest, paymentSettings: alphaPaymentSettings, }, { - paymentNetworkId: 2, + paymentNetworkId: BATCH_PAYMENT_NETWORK_ID.BATCH_MULTI_ERC20_PAYMENTS, request: validRequest, }, ], @@ -524,7 +535,7 @@ const testERC20Batch = ( _request1: ClientTypes.IRequestData, _request2: ClientTypes.IRequestData, ) => { - describe(`[No conversion]: erc20-batch-conversion-proxy ${testDescription}`, () => { + describe(`${testDescription}`, () => { beforeAll(async () => { // revoke DAI approval await revokeErc20Approval( @@ -545,11 +556,11 @@ const testERC20Batch = ( request2 = Utils.deepCopy(_request2); enrichedRequests = [ { - paymentNetworkId: 2, + paymentNetworkId: BATCH_PAYMENT_NETWORK_ID.BATCH_MULTI_ERC20_PAYMENTS, request: request1, }, { - paymentNetworkId: 2, + paymentNetworkId: BATCH_PAYMENT_NETWORK_ID.BATCH_MULTI_ERC20_PAYMENTS, request: request2, }, ]; @@ -701,7 +712,7 @@ const testERC20Batch = ( prepareBatchConversionPaymentTransaction( [ { - paymentNetworkId: 2, + paymentNetworkId: BATCH_PAYMENT_NETWORK_ID.BATCH_MULTI_ERC20_PAYMENTS, request: { ...request1, extensions: { @@ -736,5 +747,7 @@ const testERC20Batch = ( }); }; -testERC20Batch('Same tokens', validRequest, validRequest); -testERC20Batch('Different tokens', validRequest, fauValidRequest); +describe('[No conversion]: erc20-batch-conversion-proxy', () => { + testERC20Batch('Same tokens', validRequest, validRequest); + testERC20Batch('Different tokens', validRequest, fauValidRequest); +}); From 4fca56c7c519f5fcbd79f71fc083eeed6700bf21 Mon Sep 17 00:00:00 2001 From: olivier7delf <55892112+olivier7delf@users.noreply.github.com> Date: Wed, 14 Sep 2022 15:18:40 +0200 Subject: [PATCH 096/105] refacto tests --- .../payment/any-to-erc20-batch-proxy.test.ts | 836 ++++++++---------- 1 file changed, 390 insertions(+), 446 deletions(-) diff --git a/packages/payment-processor/test/payment/any-to-erc20-batch-proxy.test.ts b/packages/payment-processor/test/payment/any-to-erc20-batch-proxy.test.ts index 02bed9dc7..208841090 100644 --- a/packages/payment-processor/test/payment/any-to-erc20-batch-proxy.test.ts +++ b/packages/payment-processor/test/payment/any-to-erc20-batch-proxy.test.ts @@ -18,7 +18,6 @@ import { payBatchConversionProxyRequest, prepareBatchConversionPaymentTransaction, } from '../../src/payment/batch-conversion-proxy'; -import { sameCurrencyValue } from './shared'; import { batchConversionPaymentsArtifact } from '@requestnetwork/smart-contracts'; import { UnsupportedCurrencyError } from '@requestnetwork/currency'; import { ERC20__factory } from '@requestnetwork/smart-contracts/types'; @@ -27,34 +26,37 @@ import { BATCH_PAYMENT_NETWORK_ID } from '@requestnetwork/types/dist/payment-typ /* eslint-disable no-magic-numbers */ /* eslint-disable @typescript-eslint/no-unused-expressions */ +/** Used to to calculate batch fees */ +const BATCH_DENOMINATOR = 10000; +const BATCH_FEE = 30; +const BATCH_CONV_FEE = 30; +const batchConvVersion = '0.1.0'; +const DAITokenAddress = '0x38cF23C52Bb4B13F051Aec09580a2dE845a7FA35'; +const FAUTokenAddress = '0x9FBDa871d559710256a2502A2517b794B482Db40'; +const mnemonic = 'candy maple cake sugar pudding cream honey rich smooth crumble sweet treat'; +const paymentAddress = '0xf17f52151EbEF6C7334FAD080c5704D77216b732'; +const feeAddress = '0xC5fdf4076b8F3A5357c5E395ab970B5B54098Fef'; +const provider = new providers.JsonRpcProvider('http://localhost:8545'); +const wallet = Wallet.fromMnemonic(mnemonic).connect(provider); + // Cf. ERC20Alpha in TestERC20.sol -const erc20ContractAddress = '0x38cF23C52Bb4B13F051Aec09580a2dE845a7FA35'; const alphaPaymentSettings: IConversionPaymentSettings = { currency: { type: RequestLogicTypes.CURRENCY.ERC20, - value: erc20ContractAddress, + value: DAITokenAddress, network: 'private', }, maxToSpend: '10000000000000000000000000000', currencyManager, }; -/** Used to to calculate batch fees */ -const tenThousand = 10000; -const batchFee = 30; -const batchConvFee = 30; -const batchConvVersion = '0.1.0'; -const DAITokenAddress = '0x38cF23C52Bb4B13F051Aec09580a2dE845a7FA35'; -const FAUTokenAddress = '0x9FBDa871d559710256a2502A2517b794B482Db40'; // TestERC20 address -const mnemonic = 'candy maple cake sugar pudding cream honey rich smooth crumble sweet treat'; -const paymentAddress = '0xf17f52151EbEF6C7334FAD080c5704D77216b732'; -const feeAddress = '0xC5fdf4076b8F3A5357c5E395ab970B5B54098Fef'; -const provider = new providers.JsonRpcProvider('http://localhost:8545'); -const wallet = Wallet.fromMnemonic(mnemonic).connect(provider); - const EURExpectedAmount = 100; const EURFeeAmount = 2; -const validEuroRequest: ClientTypes.IRequestData = { +// amounts used for DAI and FAU requests +const expectedAmount = 100000; +const feeAmount = 100; + +const validEURRequest: ClientTypes.IRequestData = { balance: { balance: '0', events: [], @@ -69,7 +71,6 @@ const validEuroRequest: ClientTypes.IRequestData = { type: RequestLogicTypes.CURRENCY.ISO4217, value: 'EUR', }, - events: [], expectedAmount: EURExpectedAmount, extensions: { @@ -83,7 +84,7 @@ const validEuroRequest: ClientTypes.IRequestData = { paymentAddress, salt: 'salt', network: 'private', - tokensAccepted: [erc20ContractAddress], + tokensAccepted: [DAITokenAddress], }, version: '0.1.0', }, @@ -99,9 +100,7 @@ const validEuroRequest: ClientTypes.IRequestData = { version: '1.0', }; -const expectedAmount = 100000; -const feeAmount = 100; -const validRequest: ClientTypes.IRequestData = { +const DAIValidRequest: ClientTypes.IRequestData = { balance: { balance: '0', events: [], @@ -144,16 +143,17 @@ const validRequest: ClientTypes.IRequestData = { version: '1.0', }; -const fauValidRequest = Utils.deepCopy(validRequest) as ClientTypes.IRequestData; -fauValidRequest.currencyInfo = { +const FAUValidRequest = Utils.deepCopy(DAIValidRequest) as ClientTypes.IRequestData; +FAUValidRequest.currencyInfo = { network: 'private', type: RequestLogicTypes.CURRENCY.ERC20 as any, value: FAUTokenAddress, }; -let request1: ClientTypes.IRequestData; -let request2: ClientTypes.IRequestData; let enrichedRequests: EnrichedRequest[] = []; +// EUR and FAU requests modified within tests to throw errors +let EURRequest: ClientTypes.IRequestData; +let FAURequest: ClientTypes.IRequestData; /** * Calcul the expected amount to pay for X euro into Y tokens @@ -167,408 +167,359 @@ const expectedConversionAmount = (amount: number): BigNumber => { return BigNumber.from(10).pow(18).mul(amount).div(100).mul(120).div(100).mul(100).div(101); }; -/** - * Gets the encoding depending of two ERC20 (no conversion) requests predefined: - * validRequest and fauValidRequest - */ -const expectedEncoding = ( - request1: ClientTypes.IRequestData, - request2: ClientTypes.IRequestData, -): string => { - if (sameCurrencyValue(request1, validRequest) && sameCurrencyValue(request2, validRequest)) { - return '0xf0fa379f0000000000000000000000000000000000000000000000000000000000000040000000000000000000000000c5fdf4076b8f3a5357c5e395ab970b5b54098fef00000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000016000000000000000000000000000000000000000000000000000000000000001c000000000000000000000000000000000000000000000000000000000000002a0000000000000000000000000000000000000000000000000000000000000000200000000000000000000000038cf23c52bb4b13f051aec09580a2de845a7fa3500000000000000000000000038cf23c52bb4b13f051aec09580a2de845a7fa350000000000000000000000000000000000000000000000000000000000000002000000000000000000000000f17f52151ebef6c7334fad080c5704d77216b732000000000000000000000000f17f52151ebef6c7334fad080c5704d77216b732000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000186a000000000000000000000000000000000000000000000000000000000000186a0000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000000886dfbccad783599a000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000886dfbccad783599a000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000640000000000000000000000000000000000000000000000000000000000000064'; - } else { - return '0xf0fa379f0000000000000000000000000000000000000000000000000000000000000040000000000000000000000000c5fdf4076b8f3a5357c5e395ab970b5b54098fef00000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000016000000000000000000000000000000000000000000000000000000000000001c000000000000000000000000000000000000000000000000000000000000002a0000000000000000000000000000000000000000000000000000000000000000200000000000000000000000038cf23c52bb4b13f051aec09580a2de845a7fa350000000000000000000000009fbda871d559710256a2502a2517b794b482db400000000000000000000000000000000000000000000000000000000000000002000000000000000000000000f17f52151ebef6c7334fad080c5704d77216b732000000000000000000000000f17f52151ebef6c7334fad080c5704d77216b732000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000186a000000000000000000000000000000000000000000000000000000000000186a0000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000000886dfbccad783599a000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000886dfbccad783599a000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000640000000000000000000000000000000000000000000000000000000000000064'; - } -}; - -describe(`[Conversion]: erc20-batch-conversion-proxy`, () => { +describe('erc20-batch-conversion-proxy', () => { beforeAll(async () => { // revoke DAI and FAU approvals await revokeErc20Approval( - getBatchConversionProxyAddress(validEuroRequest, batchConvVersion), - erc20ContractAddress, + getBatchConversionProxyAddress(validEURRequest, batchConvVersion), + DAITokenAddress, wallet, ); await revokeErc20Approval( - getBatchConversionProxyAddress(fauValidRequest, batchConvVersion), - erc20ContractAddress, + getBatchConversionProxyAddress(FAUValidRequest, batchConvVersion), + DAITokenAddress, wallet, ); }); + describe(`Conversion:`, () => { + beforeEach(() => { + jest.restoreAllMocks(); + EURRequest = Utils.deepCopy(validEURRequest); + enrichedRequests = [ + { + paymentNetworkId: BATCH_PAYMENT_NETWORK_ID.BATCH_MULTI_ERC20_CONVERSION_PAYMENTS, + request: validEURRequest, + paymentSettings: alphaPaymentSettings, + }, + { + paymentNetworkId: BATCH_PAYMENT_NETWORK_ID.BATCH_MULTI_ERC20_CONVERSION_PAYMENTS, + request: EURRequest, + paymentSettings: alphaPaymentSettings, + }, + ]; + }); - beforeEach(() => { - jest.restoreAllMocks(); - request1 = Utils.deepCopy(validEuroRequest); - request2 = Utils.deepCopy(validEuroRequest); - enrichedRequests = [ - { - paymentNetworkId: BATCH_PAYMENT_NETWORK_ID.BATCH_MULTI_ERC20_CONVERSION_PAYMENTS, - request: request1, - paymentSettings: alphaPaymentSettings, - }, - { - paymentNetworkId: BATCH_PAYMENT_NETWORK_ID.BATCH_MULTI_ERC20_CONVERSION_PAYMENTS, - request: request2, - paymentSettings: alphaPaymentSettings, - }, - ]; - }); + describe('Throw an error', () => { + it('should throw an error if the token is not accepted', async () => { + await expect( + payBatchConversionProxyRequest( + [ + { + paymentNetworkId: BATCH_PAYMENT_NETWORK_ID.BATCH_MULTI_ERC20_CONVERSION_PAYMENTS, + request: validEURRequest, + paymentSettings: { + ...alphaPaymentSettings, + currency: { + ...alphaPaymentSettings.currency, + value: '0x775eb53d00dd0acd3ec1696472105d579b9b386b', + }, + } as IConversionPaymentSettings, + }, + ], + batchConvVersion, + wallet, + ), + ).rejects.toThrowError( + new UnsupportedCurrencyError({ + value: '0x775eb53d00dd0acd3ec1696472105d579b9b386b', + network: 'private', + }), + ); + }); + it('should throw an error if request has no extension', async () => { + EURRequest.extensions = [] as any; + + await expect( + payBatchConversionProxyRequest( + [ + { + paymentNetworkId: BATCH_PAYMENT_NETWORK_ID.BATCH_MULTI_ERC20_CONVERSION_PAYMENTS, + request: EURRequest, + paymentSettings: alphaPaymentSettings, + }, + ], + batchConvVersion, + wallet, + ), + ).rejects.toThrowError('no payment network found'); + }); + it('should throw an error if request has no paymentSettings', async () => { + await expect( + payBatchConversionProxyRequest( + [ + { + paymentNetworkId: BATCH_PAYMENT_NETWORK_ID.BATCH_MULTI_ERC20_CONVERSION_PAYMENTS, + request: EURRequest, + paymentSettings: undefined, + }, + ], + batchConvVersion, + wallet, + ), + ).rejects.toThrowError('the enrichedRequest has no paymentSettings'); + }); + it('should throw an error if the request is ETH', async () => { + EURRequest.currencyInfo.type = RequestLogicTypes.CURRENCY.ETH; + await expect( + payBatchConversionProxyRequest(enrichedRequests, batchConvVersion, wallet), + ).rejects.toThrowError(`wrong request currencyInfo type`); + }); + it('should throw an error if the request has a wrong payment network id', async () => { + EURRequest.extensions = { + // ERC20_FEE_PROXY_CONTRACT instead of ANY_TO_ERC20_PROXY + [PaymentTypes.PAYMENT_NETWORK_ID.ERC20_FEE_PROXY_CONTRACT]: { + events: [], + id: ExtensionTypes.ID.PAYMENT_NETWORK_ERC20_FEE_PROXY_CONTRACT, + type: ExtensionTypes.TYPE.PAYMENT_NETWORK, + values: { + feeAddress, + feeAmount: feeAmount, + paymentAddress: paymentAddress, + salt: 'salt', + }, + version: '0.1.0', + }, + }; - describe('Throw an error', () => { - it('should throw an error if the token is not accepted', async () => { - await expect( - payBatchConversionProxyRequest( + await expect( + payBatchConversionProxyRequest(enrichedRequests, batchConvVersion, wallet), + ).rejects.toThrowError( + 'request cannot be processed, or is not an pn-any-to-erc20-proxy request', + ); + }); + it("should throw an error if one request's currencyInfo has no value", async () => { + EURRequest.currencyInfo.value = ''; + await expect( + payBatchConversionProxyRequest(enrichedRequests, batchConvVersion, wallet), + ).rejects.toThrowError("The currency '' is unknown or not supported"); + }); + it('should throw an error if request has no extension', async () => { + EURRequest.extensions = [] as any; + await expect( + payBatchConversionProxyRequest(enrichedRequests, batchConvVersion, wallet), + ).rejects.toThrowError('no payment network found'); + }); + it('should throw an error if there is a wrong version mapping', async () => { + EURRequest.extensions = { + [PaymentTypes.PAYMENT_NETWORK_ID.ANY_TO_ERC20_PROXY]: { + ...EURRequest.extensions[PaymentTypes.PAYMENT_NETWORK_ID.ANY_TO_ERC20_PROXY], + version: '0.3.0', + }, + }; + await expect( + payBatchConversionProxyRequest(enrichedRequests, batchConvVersion, wallet), + ).rejects.toThrowError('Every payment network type and version must be identical'); + }); + }); + + describe('payment', () => { + it('should consider override parameters', async () => { + const spy = jest.fn(); + const originalSendTransaction = wallet.sendTransaction.bind(wallet); + wallet.sendTransaction = spy; + await payBatchConversionProxyRequest( [ { paymentNetworkId: BATCH_PAYMENT_NETWORK_ID.BATCH_MULTI_ERC20_CONVERSION_PAYMENTS, - request: request1, - paymentSettings: { - ...alphaPaymentSettings, - currency: { - ...alphaPaymentSettings.currency, - value: '0x775eb53d00dd0acd3ec1696472105d579b9b386b', - }, - } as IConversionPaymentSettings, + request: validEURRequest, + paymentSettings: alphaPaymentSettings, }, ], batchConvVersion, wallet, - ), - ).rejects.toThrowError( - new UnsupportedCurrencyError({ - value: '0x775eb53d00dd0acd3ec1696472105d579b9b386b', - network: 'private', - }), - ); - }); - it('should throw an error if request has no extension', async () => { - const request = Utils.deepCopy(validEuroRequest); - request.extensions = [] as any; + { gasPrice: '20000000000' }, + ); + expect(spy).toHaveBeenCalledWith({ + data: '0xf0fa379f0000000000000000000000000000000000000000000000000000000000000040000000000000000000000000c5fdf4076b8f3a5357c5e395ab970b5b54098fef0000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000024000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000020000000000000000000000000f17f52151ebef6c7334fad080c5704d77216b7320000000000000000000000000000000000000000000000000000000005f5e10000000000000000000000000000000000000000000000000000000000000000e0000000000000000000000000000000000000000000000000000000000000016000000000000000000000000000000000000000000000000000000000001e84800000000000000000000000000000000000000000204fce5e3e250261100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000300000000000000000000000017b4158805772ced11225e77339f90beb5aae968000000000000000000000000775eb53d00dd0acd3ec1696472105d579b9b386b00000000000000000000000038cf23c52bb4b13f051aec09580a2de845a7fa35000000000000000000000000000000000000000000000000000000000000000886dfbccad783599a00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000c000000000000000000000000000000000000000000000000000000000000000e00000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000012000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000', + gasPrice: '20000000000', + to: getBatchConversionProxyAddress(validEURRequest, '0.1.0'), + value: 0, + }); + wallet.sendTransaction = originalSendTransaction; + }); + it('should convert and pay a request in EUR with ERC20', async () => { + // Approve the contract + const approvalTx = await approveErc20BatchConversionIfNeeded( + validEURRequest, + wallet.address, + batchConvVersion, + wallet.provider, + alphaPaymentSettings, + ); + expect(approvalTx).toBeDefined(); + if (approvalTx) { + await approvalTx.wait(1); + } + + // Get the balances to compare after payment + const initialETHFromBalance = await wallet.getBalance(); + const initialDAIFromBalance = await ERC20__factory.connect( + DAITokenAddress, + provider, + ).balanceOf(wallet.address); - await expect( - payBatchConversionProxyRequest( + // Convert and pay + const tx = await payBatchConversionProxyRequest( [ { paymentNetworkId: BATCH_PAYMENT_NETWORK_ID.BATCH_MULTI_ERC20_CONVERSION_PAYMENTS, - request: request, + request: validEURRequest, paymentSettings: alphaPaymentSettings, }, ], batchConvVersion, wallet, - ), - ).rejects.toThrowError('no payment network found'); - }); - it('should throw an error if request has no paymentSettings', async () => { - await expect( - payBatchConversionProxyRequest( + ); + const confirmedTx = await tx.wait(1); + expect(confirmedTx.status).toEqual(1); + expect(tx.hash).toBeDefined(); + // Get the new balances + const ETHFromBalance = await wallet.getBalance(); + const DAIFromBalance = await ERC20__factory.connect(DAITokenAddress, provider).balanceOf( + wallet.address, + ); + // Check each balance + const amountToPay = expectedConversionAmount(EURExpectedAmount); + const feeToPay = expectedConversionAmount(EURFeeAmount); + const expectedAmountToPay = amountToPay + .add(feeToPay) + .mul(BATCH_DENOMINATOR + BATCH_CONV_FEE) + .div(BATCH_DENOMINATOR); + expect( + BigNumber.from(initialETHFromBalance).sub(ETHFromBalance).toNumber(), + ).toBeGreaterThan(0); + expect( + BigNumber.from(initialDAIFromBalance).sub(BigNumber.from(DAIFromBalance)), + // Calculation of expectedAmountToPay + // expectedAmount: 1.00 + // feeAmount: + .02 + // = 1.02 + // AggEurUsd.sol x 1.20 + // AggDaiUsd.sol / 1.01 + // BATCH_CONV_FEE x 1.003 + // (exact result) = 1.215516831683168316 (over 18 decimals for this ERC20) + ).toEqual(expectedAmountToPay); + }); + it('should convert and pay two requests in EUR with ERC20', async () => { + // Get the balances to compare after payment + const initialETHFromBalance = await wallet.getBalance(); + const initialDAIFromBalance = await ERC20__factory.connect( + DAITokenAddress, + provider, + ).balanceOf(wallet.address); + + // Convert and pay + const tx = await payBatchConversionProxyRequest( + Array(2).fill({ + paymentNetworkId: BATCH_PAYMENT_NETWORK_ID.BATCH_MULTI_ERC20_CONVERSION_PAYMENTS, + request: validEURRequest, + paymentSettings: alphaPaymentSettings, + }), + batchConvVersion, + wallet, + ); + const confirmedTx = await tx.wait(1); + expect(confirmedTx.status).toEqual(1); + expect(tx.hash).toBeDefined(); + // Get the new balances + const ETHFromBalance = await wallet.getBalance(); + const DAIFromBalance = await ERC20__factory.connect(DAITokenAddress, provider).balanceOf( + wallet.address, + ); + // Check each balance + const amountToPay = expectedConversionAmount(EURExpectedAmount).mul(2); // multiply by the number of requests: 2 + const feeToPay = expectedConversionAmount(EURFeeAmount).mul(2); // multiply by the number of requests: 2 + const expectedAmoutToPay = amountToPay + .add(feeToPay) + .mul(BATCH_DENOMINATOR + BATCH_CONV_FEE) + .div(BATCH_DENOMINATOR); + expect( + BigNumber.from(initialETHFromBalance).sub(ETHFromBalance).toNumber(), + ).toBeGreaterThan(0); + expect(BigNumber.from(initialDAIFromBalance).sub(BigNumber.from(DAIFromBalance))).toEqual( + expectedAmoutToPay, + ); + }); + it('should pay 3 heterogeneous ERC20 payments with and without conversion', async () => { + // Get the balances to compare after payment + const initialETHFromBalance = await wallet.getBalance(); + const initialDAIFromBalance = await ERC20__factory.connect( + DAITokenAddress, + provider, + ).balanceOf(wallet.address); + + // Convert the two first requests and pay the three requests + const tx = await payBatchConversionProxyRequest( [ { paymentNetworkId: BATCH_PAYMENT_NETWORK_ID.BATCH_MULTI_ERC20_CONVERSION_PAYMENTS, - request: validEuroRequest, - paymentSettings: undefined, + request: validEURRequest, + paymentSettings: alphaPaymentSettings, + }, + { + paymentNetworkId: BATCH_PAYMENT_NETWORK_ID.BATCH_MULTI_ERC20_CONVERSION_PAYMENTS, + request: validEURRequest, + paymentSettings: alphaPaymentSettings, + }, + { + paymentNetworkId: BATCH_PAYMENT_NETWORK_ID.BATCH_MULTI_ERC20_PAYMENTS, + request: DAIValidRequest, }, ], batchConvVersion, wallet, - ), - ).rejects.toThrowError('the enrichedRequest has no paymentSettings'); - }); - it('should throw an error if the request is ETH', async () => { - request2.currencyInfo.type = RequestLogicTypes.CURRENCY.ETH; - await expect( - payBatchConversionProxyRequest(enrichedRequests, batchConvVersion, wallet), - ).rejects.toThrowError(`wrong request currencyInfo type`); - }); - it('should throw an error if the request has a wrong payment network id', async () => { - request2.extensions = { - // ERC20_FEE_PROXY_CONTRACT instead of ANY_TO_ERC20_PROXY - [PaymentTypes.PAYMENT_NETWORK_ID.ERC20_FEE_PROXY_CONTRACT]: { - events: [], - id: ExtensionTypes.ID.PAYMENT_NETWORK_ERC20_FEE_PROXY_CONTRACT, - type: ExtensionTypes.TYPE.PAYMENT_NETWORK, - values: { - feeAddress, - feeAmount: feeAmount, - paymentAddress: paymentAddress, - salt: 'salt', - }, - version: '0.1.0', - }, - }; + ); + const confirmedTx = await tx.wait(1); + expect(confirmedTx.status).toEqual(1); + expect(tx.hash).toBeDefined(); + // Get the new balances + const ETHFromBalance = await wallet.getBalance(); + const DAIFromBalance = await ERC20__factory.connect(DAITokenAddress, provider).balanceOf( + wallet.address, + ); - await expect( - payBatchConversionProxyRequest(enrichedRequests, batchConvVersion, wallet), - ).rejects.toThrowError( - 'request cannot be processed, or is not an pn-any-to-erc20-proxy request', - ); - }); - it("should throw an error if one request's currencyInfo has no value", async () => { - request2.currencyInfo.value = ''; - await expect( - payBatchConversionProxyRequest(enrichedRequests, batchConvVersion, wallet), - ).rejects.toThrowError("The currency '' is unknown or not supported"); - }); - it('should throw an error if request has no extension', async () => { - request2.extensions = [] as any; - await expect( - payBatchConversionProxyRequest(enrichedRequests, batchConvVersion, wallet), - ).rejects.toThrowError('no payment network found'); - }); - it('should throw an error if there is a wrong version mapping', async () => { - request2.extensions = { - [PaymentTypes.PAYMENT_NETWORK_ID.ANY_TO_ERC20_PROXY]: { - ...request2.extensions[PaymentTypes.PAYMENT_NETWORK_ID.ANY_TO_ERC20_PROXY], - version: '0.3.0', - }, - }; - await expect( - payBatchConversionProxyRequest(enrichedRequests, batchConvVersion, wallet), - ).rejects.toThrowError('Every payment network type and version must be identical'); - }); - }); + // Check each balance + let expectedConvAmountToPay = expectedConversionAmount(EURExpectedAmount).mul(2); // multiply by the number of conversion requests: 2 + const feeToPay = expectedConversionAmount(EURFeeAmount).mul(2); // multiply by the number of conversion requests: 2 + // expectedConvAmountToPay with fees and batch fees + expectedConvAmountToPay = expectedConvAmountToPay + .add(feeToPay) + .mul(BATCH_DENOMINATOR + BATCH_CONV_FEE) + .div(BATCH_DENOMINATOR); - describe('payment', () => { - it('should consider override parameters', async () => { - const spy = jest.fn(); - const originalSendTransaction = wallet.sendTransaction.bind(wallet); - wallet.sendTransaction = spy; - await payBatchConversionProxyRequest( - [ - { - paymentNetworkId: BATCH_PAYMENT_NETWORK_ID.BATCH_MULTI_ERC20_CONVERSION_PAYMENTS, - request: request1, - paymentSettings: alphaPaymentSettings, - }, - ], - batchConvVersion, - wallet, - { gasPrice: '20000000000' }, - ); - expect(spy).toHaveBeenCalledWith({ - data: '0xf0fa379f0000000000000000000000000000000000000000000000000000000000000040000000000000000000000000c5fdf4076b8f3a5357c5e395ab970b5b54098fef0000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000024000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000020000000000000000000000000f17f52151ebef6c7334fad080c5704d77216b7320000000000000000000000000000000000000000000000000000000005f5e10000000000000000000000000000000000000000000000000000000000000000e0000000000000000000000000000000000000000000000000000000000000016000000000000000000000000000000000000000000000000000000000001e84800000000000000000000000000000000000000000204fce5e3e250261100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000300000000000000000000000017b4158805772ced11225e77339f90beb5aae968000000000000000000000000775eb53d00dd0acd3ec1696472105d579b9b386b00000000000000000000000038cf23c52bb4b13f051aec09580a2de845a7fa35000000000000000000000000000000000000000000000000000000000000000886dfbccad783599a00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000c000000000000000000000000000000000000000000000000000000000000000e00000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000012000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000', - gasPrice: '20000000000', - to: getBatchConversionProxyAddress(request1, '0.1.0'), - value: 0, + const expectedNoConvAmountToPay = BigNumber.from(DAIValidRequest.expectedAmount) + .add(feeAmount) + .mul(BATCH_DENOMINATOR + BATCH_FEE) + .div(BATCH_DENOMINATOR); + + expect( + BigNumber.from(initialETHFromBalance).sub(ETHFromBalance).toNumber(), + ).toBeGreaterThan(0); + expect(BigNumber.from(initialDAIFromBalance).sub(BigNumber.from(DAIFromBalance))).toEqual( + expectedConvAmountToPay.add(expectedNoConvAmountToPay), + ); }); - wallet.sendTransaction = originalSendTransaction; - }); - it('should convert and pay a request in EUR with ERC20', async () => { - // Approve the contract - const approvalTx = await approveErc20BatchConversionIfNeeded( - validEuroRequest, - wallet.address, - batchConvVersion, - wallet.provider, - alphaPaymentSettings, - ); - expect(approvalTx).toBeDefined(); - if (approvalTx) { - await approvalTx.wait(1); - } - - // Get the balances to compare after payment - const balanceEthBefore = await wallet.getBalance(); - const balanceTokenBefore = await ERC20__factory.connect( - erc20ContractAddress, - provider, - ).balanceOf(wallet.address); - - // Convert and pay - const tx = await payBatchConversionProxyRequest( - [ - { - paymentNetworkId: BATCH_PAYMENT_NETWORK_ID.BATCH_MULTI_ERC20_CONVERSION_PAYMENTS, - request: validEuroRequest, - paymentSettings: alphaPaymentSettings, - }, - ], - batchConvVersion, - wallet, - ); - const confirmedTx = await tx.wait(1); - expect(confirmedTx.status).toEqual(1); - expect(tx.hash).toBeDefined(); - // Get the new balances - const balanceEthAfter = await wallet.getBalance(); - const balanceTokenAfter = await ERC20__factory.connect( - erc20ContractAddress, - provider, - ).balanceOf(wallet.address); - // Check each balance - const amountToPay = expectedConversionAmount(EURExpectedAmount); - const feeToPay = expectedConversionAmount(EURFeeAmount); - const expectedAmountToPay = amountToPay - .add(feeToPay) - .mul(tenThousand + batchConvFee) - .div(tenThousand); - expect(BigNumber.from(balanceEthBefore).sub(balanceEthAfter).toNumber()).toBeGreaterThan(0); - expect( - BigNumber.from(balanceTokenBefore).sub(BigNumber.from(balanceTokenAfter)), - // Calculation - // expectedAmount: 1.00 - // feeAmount: + .02 - // = 1.02 - // AggEurUsd.sol x 1.20 - // AggDaiUsd.sol / 1.01 - // batchConvFee x 1.003 - // (exact result) = 1.215516831683168316 (over 18 decimals for this ERC20) - ).toEqual(expectedAmountToPay); - }); - it('should convert and pay two requests in EUR with ERC20', async () => { - // Get the balances to compare after payment - const balanceEthBefore = await wallet.getBalance(); - const balanceTokenBefore = await ERC20__factory.connect( - erc20ContractAddress, - provider, - ).balanceOf(wallet.address); - - // Convert and pay - const tx = await payBatchConversionProxyRequest( - [ - { - paymentNetworkId: BATCH_PAYMENT_NETWORK_ID.BATCH_MULTI_ERC20_CONVERSION_PAYMENTS, - request: validEuroRequest, - paymentSettings: alphaPaymentSettings, - }, - { - paymentNetworkId: BATCH_PAYMENT_NETWORK_ID.BATCH_MULTI_ERC20_CONVERSION_PAYMENTS, - request: validEuroRequest, - paymentSettings: alphaPaymentSettings, - }, - ], - batchConvVersion, - wallet, - ); - const confirmedTx = await tx.wait(1); - expect(confirmedTx.status).toEqual(1); - expect(tx.hash).toBeDefined(); - // Get the new balances - const balanceEthAfter = await wallet.getBalance(); - const balanceTokenAfter = await ERC20__factory.connect( - erc20ContractAddress, - provider, - ).balanceOf(wallet.address); - // Check each balance - const amountToPay = expectedConversionAmount(EURExpectedAmount).mul(2); // multiply by the number of requests: 2 - const feeToPay = expectedConversionAmount(EURFeeAmount).mul(2); // multiply by the number of requests: 2 - const expectedAmout = amountToPay - .add(feeToPay) - .mul(tenThousand + batchConvFee) - .div(tenThousand); - expect(BigNumber.from(balanceEthBefore).sub(balanceEthAfter).toNumber()).toBeGreaterThan(0); - expect(BigNumber.from(balanceTokenBefore).sub(BigNumber.from(balanceTokenAfter))).toEqual( - expectedAmout, - ); - }); - it('should pay 3 heterogeneous ERC20 payments with and without conversion', async () => { - // Get the balances to compare after payment - const balanceEthBefore = await wallet.getBalance(); - const balanceTokenBefore = await ERC20__factory.connect( - erc20ContractAddress, - provider, - ).balanceOf(wallet.address); - - // Convert the two first requests and pay the three requests - const tx = await payBatchConversionProxyRequest( - [ - { - paymentNetworkId: BATCH_PAYMENT_NETWORK_ID.BATCH_MULTI_ERC20_CONVERSION_PAYMENTS, - request: validEuroRequest, - paymentSettings: alphaPaymentSettings, - }, - { - paymentNetworkId: BATCH_PAYMENT_NETWORK_ID.BATCH_MULTI_ERC20_CONVERSION_PAYMENTS, - request: validEuroRequest, - paymentSettings: alphaPaymentSettings, - }, - { - paymentNetworkId: BATCH_PAYMENT_NETWORK_ID.BATCH_MULTI_ERC20_PAYMENTS, - request: validRequest, - }, - ], - batchConvVersion, - wallet, - ); - const confirmedTx = await tx.wait(1); - expect(confirmedTx.status).toEqual(1); - expect(tx.hash).toBeDefined(); - // Get the new balances - const balanceEthAfter = await wallet.getBalance(); - const balanceTokenAfter = await ERC20__factory.connect( - erc20ContractAddress, - provider, - ).balanceOf(wallet.address); - - // Check each balance - // amountToPay without fees - let amountToPay = expectedConversionAmount(EURExpectedAmount).mul(2); // multiply by the number of conversion requests: 2 - const feeToPay = expectedConversionAmount(EURFeeAmount).mul(2); // multiply by the number of conversion requests: 2 - // amountToPay with fees - amountToPay = amountToPay - .add(feeToPay) - .mul(tenThousand + batchConvFee) - .div(tenThousand); - - const noConvExpectedAmount = BigNumber.from(validRequest.expectedAmount); - const noConvAmountToPay = noConvExpectedAmount - .add(feeAmount) - .mul(tenThousand + batchFee) - .div(tenThousand); - - expect(BigNumber.from(balanceEthBefore).sub(balanceEthAfter).toNumber()).toBeGreaterThan(0); - expect(BigNumber.from(balanceTokenBefore).sub(BigNumber.from(balanceTokenAfter))).toEqual( - amountToPay.add(noConvAmountToPay), - ); }); }); -}); - -/** - * Test only ERC20 requests. No Conversion - * @param _request1 ERC20 request to test/pay, no conversion - * @param _request2 ERC20 request to test/pay, no conversion - */ -const testERC20Batch = ( - testDescription: string, - _request1: ClientTypes.IRequestData, - _request2: ClientTypes.IRequestData, -) => { - describe(`${testDescription}`, () => { - beforeAll(async () => { - // revoke DAI approval - await revokeErc20Approval( - getBatchConversionProxyAddress(validRequest, batchConvVersion), - erc20ContractAddress, - wallet, - ); - // revoke FAU approval - await revokeErc20Approval( - getBatchConversionProxyAddress(fauValidRequest, batchConvVersion), - erc20ContractAddress, - wallet, - ); - }); + describe('No conversion:', () => { beforeEach(() => { - request1 = Utils.deepCopy(_request1); - request2 = Utils.deepCopy(_request2); + FAURequest = Utils.deepCopy(FAUValidRequest); enrichedRequests = [ { paymentNetworkId: BATCH_PAYMENT_NETWORK_ID.BATCH_MULTI_ERC20_PAYMENTS, - request: request1, + request: DAIValidRequest, }, { paymentNetworkId: BATCH_PAYMENT_NETWORK_ID.BATCH_MULTI_ERC20_PAYMENTS, - request: request2, + request: FAURequest, }, ]; }); describe('Throw an error', () => { it('should throw an error if the request is not erc20', async () => { - request2.currencyInfo.type = RequestLogicTypes.CURRENCY.ETH; + FAURequest.currencyInfo.type = RequestLogicTypes.CURRENCY.ETH; await expect( payBatchConversionProxyRequest(enrichedRequests, batchConvVersion, wallet), ).rejects.toThrowError( @@ -577,7 +528,7 @@ const testERC20Batch = ( }); it("should throw an error if one request's currencyInfo has no value", async () => { - request2.currencyInfo.value = ''; + FAURequest.currencyInfo.value = ''; await expect( payBatchConversionProxyRequest(enrichedRequests, batchConvVersion, wallet), ).rejects.toThrowError( @@ -586,7 +537,7 @@ const testERC20Batch = ( }); it("should throw an error if one request's currencyInfo has no network", async () => { - request2.currencyInfo.network = ''; + FAURequest.currencyInfo.network = ''; await expect( payBatchConversionProxyRequest(enrichedRequests, batchConvVersion, wallet), ).rejects.toThrowError( @@ -595,16 +546,16 @@ const testERC20Batch = ( }); it('should throw an error if request has no extension', async () => { - request2.extensions = [] as any; + FAURequest.extensions = [] as any; await expect( payBatchConversionProxyRequest(enrichedRequests, batchConvVersion, wallet), ).rejects.toThrowError('no payment network found'); }); it('should throw an error if there is a wrong version mapping', async () => { - request2.extensions = { + FAURequest.extensions = { [PaymentTypes.PAYMENT_NETWORK_ID.ERC20_FEE_PROXY_CONTRACT]: { - ...validRequest.extensions[PaymentTypes.PAYMENT_NETWORK_ID.ERC20_FEE_PROXY_CONTRACT], + ...DAIValidRequest.extensions[PaymentTypes.PAYMENT_NETWORK_ID.ERC20_FEE_PROXY_CONTRACT], version: '0.3.0', }, }; @@ -623,85 +574,83 @@ const testERC20Batch = ( gasPrice: '20000000000', }); expect(spy).toHaveBeenCalledWith({ - data: expectedEncoding(request1, request2), + data: '0xf0fa379f0000000000000000000000000000000000000000000000000000000000000040000000000000000000000000c5fdf4076b8f3a5357c5e395ab970b5b54098fef00000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000016000000000000000000000000000000000000000000000000000000000000001c000000000000000000000000000000000000000000000000000000000000002a0000000000000000000000000000000000000000000000000000000000000000200000000000000000000000038cf23c52bb4b13f051aec09580a2de845a7fa350000000000000000000000009fbda871d559710256a2502a2517b794b482db400000000000000000000000000000000000000000000000000000000000000002000000000000000000000000f17f52151ebef6c7334fad080c5704d77216b732000000000000000000000000f17f52151ebef6c7334fad080c5704d77216b732000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000186a000000000000000000000000000000000000000000000000000000000000186a0000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000000886dfbccad783599a000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000886dfbccad783599a000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000640000000000000000000000000000000000000000000000000000000000000064', gasPrice: '20000000000', - to: getBatchConversionProxyAddress(request1, '0.1.0'), + to: getBatchConversionProxyAddress(DAIValidRequest, '0.1.0'), value: 0, }); wallet.sendTransaction = originalSendTransaction; }); - it(`should pay 2 ERC20 requests with fees`, async () => { - // first approve the contract - const tmpRequest1 = Utils.deepCopy(request1); - const isMultiToken = !sameCurrencyValue(request1, request2); - let amount = BigNumber.from(request1.expectedAmount); - if (!isMultiToken) { - amount = amount.add(BigNumber.from(request2.expectedAmount)); - tmpRequest1.expectedAmount = amount.toString(); - } else { - const ApprovalTx2 = await approveErc20BatchConversionIfNeeded( - request2, - wallet.address, - batchConvVersion, - wallet, - ); - if (ApprovalTx2) { - await ApprovalTx2.wait(1); - } - } - const approvalTx1 = await approveErc20BatchConversionIfNeeded( - tmpRequest1, + it(`should pay 2 differents ERC20 requests with fees`, async () => { + // approve the contract for DAI and FAU tokens + const FAUApprovalTx = await approveErc20BatchConversionIfNeeded( + FAUValidRequest, wallet.address, batchConvVersion, wallet, ); + if (FAUApprovalTx) await FAUApprovalTx.wait(1); - if (approvalTx1) { - await approvalTx1.wait(1); - } + const DAIApprovalTx = await approveErc20BatchConversionIfNeeded( + DAIValidRequest, + wallet.address, + batchConvVersion, + wallet, + ); + if (DAIApprovalTx) await DAIApprovalTx.wait(1); // get the balance - const balanceEthBefore = await wallet.getBalance(); - const balanceErc20Before = await getErc20Balance(request1, wallet.address, provider); - const feeBalanceErc20Before = await getErc20Balance(request1, feeAddress, provider); + const initialETHFromBalance = await wallet.getBalance(); + const initialDAIFromBalance = await getErc20Balance( + DAIValidRequest, + wallet.address, + provider, + ); + const initialDAIFeeBalance = await getErc20Balance(DAIValidRequest, feeAddress, provider); - const balanceErc20Before2 = await getErc20Balance(request2, wallet.address, provider); - const feeBalanceErc20Before2 = await getErc20Balance(request2, feeAddress, provider); + const initialFAUFromBalance = await getErc20Balance( + FAUValidRequest, + wallet.address, + provider, + ); + const initialFAUFeeBalance = await getErc20Balance(FAUValidRequest, feeAddress, provider); // Batch payment const tx = await payBatchConversionProxyRequest(enrichedRequests, batchConvVersion, wallet); const confirmedTx = await tx.wait(1); - const balanceEthAfter = await wallet.getBalance(); - const balanceErc20After = await getErc20Balance(request1, wallet.address, provider); - const feeBalanceErc20After = await getErc20Balance(request1, feeAddress, provider); + const ETHFromBalance = await wallet.getBalance(); + const DAIFromBalance = await getErc20Balance(DAIValidRequest, wallet.address, provider); + const DAIFeeBalance = await getErc20Balance(DAIValidRequest, feeAddress, provider); + const FAUFromBalance = await getErc20Balance(FAUValidRequest, wallet.address, provider); + const FAUFeeBalance = await getErc20Balance(FAUValidRequest, feeAddress, provider); expect(confirmedTx.status).toBe(1); expect(tx.hash).not.toBeUndefined(); - expect(balanceEthAfter.lte(balanceEthBefore)).toBeTruthy(); // 'ETH balance should be lower' - - let feeAmountExpected = - feeAmount + - (expectedAmount * batchFee) / tenThousand + - (feeAmount + (expectedAmount * batchFee) / tenThousand); - if (isMultiToken) { - feeAmountExpected = feeAmount + (expectedAmount * batchFee) / tenThousand; // Will be sent on the 2nd token fee address - const balanceErc20After2 = await getErc20Balance(request2, wallet.address, provider); - const feeBalanceErc20After2 = await getErc20Balance(request2, feeAddress, provider); - // Compare request2 balances - expect(BigNumber.from(balanceErc20After2)).toEqual( - BigNumber.from(balanceErc20Before2).sub(expectedAmount + feeAmountExpected), - ); - expect(BigNumber.from(feeBalanceErc20After2)).toEqual( - BigNumber.from(feeBalanceErc20Before2).add(feeAmountExpected), - ); - } - // compare request 1 balances - expect(BigNumber.from(balanceErc20After)).toEqual( - BigNumber.from(balanceErc20Before).sub(amount.add(feeAmountExpected)), + expect(ETHFromBalance.lte(initialETHFromBalance)).toBeTruthy(); // 'ETH balance should be lower' + + const expectedDAIFeeAmountToPay = + feeAmount + ((DAIValidRequest.expectedAmount as number) * BATCH_FEE) / BATCH_DENOMINATOR; + const expectedFAUFeeAmountToPay = + feeAmount + ((FAUValidRequest.expectedAmount as number) * BATCH_FEE) / BATCH_DENOMINATOR; + + // Compare FAUValidRequest balances + expect(BigNumber.from(FAUFromBalance)).toEqual( + BigNumber.from(initialFAUFromBalance).sub( + (FAUValidRequest.expectedAmount as number) + expectedFAUFeeAmountToPay, + ), + ); + expect(BigNumber.from(FAUFeeBalance)).toEqual( + BigNumber.from(initialFAUFeeBalance).add(expectedFAUFeeAmountToPay), ); - expect(BigNumber.from(feeBalanceErc20After)).toEqual( - BigNumber.from(feeBalanceErc20Before).add(feeAmountExpected), + // compare DAIValidRequest balances + expect(BigNumber.from(DAIFromBalance)).toEqual( + BigNumber.from(initialDAIFromBalance) + .sub(DAIValidRequest.expectedAmount as number) + .sub(expectedDAIFeeAmountToPay), + ); + expect(BigNumber.from(DAIFeeBalance)).toEqual( + BigNumber.from(initialDAIFeeBalance).add(expectedDAIFeeAmountToPay), ); }); }); @@ -714,10 +663,10 @@ const testERC20Batch = ( { paymentNetworkId: BATCH_PAYMENT_NETWORK_ID.BATCH_MULTI_ERC20_PAYMENTS, request: { - ...request1, + ...DAIValidRequest, extensions: { [PaymentTypes.PAYMENT_NETWORK_ID.ERC20_FEE_PROXY_CONTRACT]: { - ...request1.extensions[ + ...DAIValidRequest.extensions[ PaymentTypes.PAYMENT_NETWORK_ID.ERC20_FEE_PROXY_CONTRACT ], version: '0.1.0', @@ -727,10 +676,10 @@ const testERC20Batch = ( } as EnrichedRequest, { request: { - ...request2, + ...FAUValidRequest, extensions: { [PaymentTypes.PAYMENT_NETWORK_ID.ERC20_FEE_PROXY_CONTRACT]: { - ...request2.extensions[ + ...FAUValidRequest.extensions[ PaymentTypes.PAYMENT_NETWORK_ID.ERC20_FEE_PROXY_CONTRACT ], version: '0.1.0', @@ -745,9 +694,4 @@ const testERC20Batch = ( }); }); }); -}; - -describe('[No conversion]: erc20-batch-conversion-proxy', () => { - testERC20Batch('Same tokens', validRequest, validRequest); - testERC20Batch('Different tokens', validRequest, fauValidRequest); }); From 487473ddbebb9408dfc49f7c7c758a87c4fa5f31 Mon Sep 17 00:00:00 2001 From: olivier7delf <55892112+olivier7delf@users.noreply.github.com> Date: Wed, 14 Sep 2022 15:26:12 +0200 Subject: [PATCH 097/105] cleaning --- .../src/payment/batch-conversion-proxy.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/packages/payment-processor/src/payment/batch-conversion-proxy.ts b/packages/payment-processor/src/payment/batch-conversion-proxy.ts index f1ba5efd0..6bb102c5f 100644 --- a/packages/payment-processor/src/payment/batch-conversion-proxy.ts +++ b/packages/payment-processor/src/payment/batch-conversion-proxy.ts @@ -73,13 +73,11 @@ export function prepareBatchConversionPaymentTransaction( export function encodePayBatchConversionRequest(enrichedRequests: EnrichedRequest[]): string { const { feeAddress } = getRequestPaymentValues(enrichedRequests[0].request); - //**** Create and fill batchRouter function argument: metaDetails ****// - let firstPn0Extension: IState | undefined; const pn2requests: ClientTypes.IRequestData[] = []; const conversionDetails: PaymentTypes.ConversionDetail[] = []; - // Iterate throught each enrichedRequests to do checking and retrieve info + // fill conversionDetails and pn2requests for (let i = 0; i < enrichedRequests.length; i++) { if ( enrichedRequests[i].paymentNetworkId === @@ -109,7 +107,7 @@ export function encodePayBatchConversionRequest(enrichedRequests: EnrichedReques } const metaDetails: PaymentTypes.MetaDetail[] = []; - // Add ERC20 conversion payments + // Add conversionDetails to metaDetails if (conversionDetails.length > 0) { metaDetails.push({ paymentNetworkId: BATCH_PAYMENT_NETWORK_ID.BATCH_MULTI_ERC20_CONVERSION_PAYMENTS, @@ -142,6 +140,7 @@ export function encodePayBatchConversionRequest(enrichedRequests: EnrichedReques }, }); } + const proxyContract = BatchConversionPayments__factory.createInterface(); return proxyContract.encodeFunctionData('batchRouter', [ metaDetails, From 40974f9b2d02ff77923053ddc14a863c1b9e65e9 Mon Sep 17 00:00:00 2001 From: olivier7delf <55892112+olivier7delf@users.noreply.github.com> Date: Wed, 14 Sep 2022 18:08:56 +0200 Subject: [PATCH 098/105] tests cleaning --- .../src/payment/batch-conversion-proxy.ts | 3 +- .../payment/any-to-erc20-batch-proxy.test.ts | 93 +++++++++++-------- 2 files changed, 54 insertions(+), 42 deletions(-) diff --git a/packages/payment-processor/src/payment/batch-conversion-proxy.ts b/packages/payment-processor/src/payment/batch-conversion-proxy.ts index 6bb102c5f..076f1a84c 100644 --- a/packages/payment-processor/src/payment/batch-conversion-proxy.ts +++ b/packages/payment-processor/src/payment/batch-conversion-proxy.ts @@ -64,6 +64,7 @@ export function prepareBatchConversionPaymentTransaction( value: 0, }; } + /** * Encodes a transaction to pay a batch of requests with an ERC20 currency * that is different from the request currency (eg. fiat) @@ -122,7 +123,7 @@ export function encodePayBatchConversionRequest(enrichedRequests: EnrichedReques }); } - // Get values and add cryptpoDetails to metaDetails + // Get values and add cryptoDetails to metaDetails if (pn2requests.length > 0) { const { tokenAddresses, paymentAddresses, amountsToPay, paymentReferences, feesToPay } = getBatchArgs(pn2requests, 'ERC20'); diff --git a/packages/payment-processor/test/payment/any-to-erc20-batch-proxy.test.ts b/packages/payment-processor/test/payment/any-to-erc20-batch-proxy.test.ts index 208841090..7fa6d7827 100644 --- a/packages/payment-processor/test/payment/any-to-erc20-batch-proxy.test.ts +++ b/packages/payment-processor/test/payment/any-to-erc20-batch-proxy.test.ts @@ -20,7 +20,6 @@ import { } from '../../src/payment/batch-conversion-proxy'; import { batchConversionPaymentsArtifact } from '@requestnetwork/smart-contracts'; import { UnsupportedCurrencyError } from '@requestnetwork/currency'; -import { ERC20__factory } from '@requestnetwork/smart-contracts/types'; import { BATCH_PAYMENT_NETWORK_ID } from '@requestnetwork/types/dist/payment-types'; /* eslint-disable no-magic-numbers */ @@ -30,6 +29,7 @@ import { BATCH_PAYMENT_NETWORK_ID } from '@requestnetwork/types/dist/payment-typ const BATCH_DENOMINATOR = 10000; const BATCH_FEE = 30; const BATCH_CONV_FEE = 30; + const batchConvVersion = '0.1.0'; const DAITokenAddress = '0x38cF23C52Bb4B13F051Aec09580a2dE845a7FA35'; const FAUTokenAddress = '0x9FBDa871d559710256a2502A2517b794B482Db40'; @@ -50,6 +50,8 @@ const alphaPaymentSettings: IConversionPaymentSettings = { currencyManager, }; +// requests setting + const EURExpectedAmount = 100; const EURFeeAmount = 2; // amounts used for DAI and FAU requests @@ -169,7 +171,7 @@ const expectedConversionAmount = (amount: number): BigNumber => { describe('erc20-batch-conversion-proxy', () => { beforeAll(async () => { - // revoke DAI and FAU approvals + // Revoke DAI and FAU approvals await revokeErc20Approval( getBatchConversionProxyAddress(validEURRequest, batchConvVersion), DAITokenAddress, @@ -181,6 +183,7 @@ describe('erc20-batch-conversion-proxy', () => { wallet, ); }); + describe(`Conversion:`, () => { beforeEach(() => { jest.restoreAllMocks(); @@ -353,10 +356,11 @@ describe('erc20-batch-conversion-proxy', () => { // Get the balances to compare after payment const initialETHFromBalance = await wallet.getBalance(); - const initialDAIFromBalance = await ERC20__factory.connect( - DAITokenAddress, + const initialDAIFromBalance = await getErc20Balance( + DAIValidRequest, + wallet.address, provider, - ).balanceOf(wallet.address); + ); // Convert and pay const tx = await payBatchConversionProxyRequest( @@ -373,11 +377,11 @@ describe('erc20-batch-conversion-proxy', () => { const confirmedTx = await tx.wait(1); expect(confirmedTx.status).toEqual(1); expect(tx.hash).toBeDefined(); + // Get the new balances const ETHFromBalance = await wallet.getBalance(); - const DAIFromBalance = await ERC20__factory.connect(DAITokenAddress, provider).balanceOf( - wallet.address, - ); + const DAIFromBalance = await getErc20Balance(DAIValidRequest, wallet.address, provider); + // Check each balance const amountToPay = expectedConversionAmount(EURExpectedAmount); const feeToPay = expectedConversionAmount(EURFeeAmount); @@ -401,12 +405,13 @@ describe('erc20-batch-conversion-proxy', () => { ).toEqual(expectedAmountToPay); }); it('should convert and pay two requests in EUR with ERC20', async () => { - // Get the balances to compare after payment + // Get initial balances const initialETHFromBalance = await wallet.getBalance(); - const initialDAIFromBalance = await ERC20__factory.connect( - DAITokenAddress, + const initialDAIFromBalance = await getErc20Balance( + DAIValidRequest, + wallet.address, provider, - ).balanceOf(wallet.address); + ); // Convert and pay const tx = await payBatchConversionProxyRequest( @@ -421,32 +426,35 @@ describe('erc20-batch-conversion-proxy', () => { const confirmedTx = await tx.wait(1); expect(confirmedTx.status).toEqual(1); expect(tx.hash).toBeDefined(); - // Get the new balances + + // Get balances const ETHFromBalance = await wallet.getBalance(); - const DAIFromBalance = await ERC20__factory.connect(DAITokenAddress, provider).balanceOf( - wallet.address, - ); - // Check each balance + const DAIFromBalance = await getErc20Balance(DAIValidRequest, wallet.address, provider); + + // Checks ETH balances + expect( + BigNumber.from(initialETHFromBalance).sub(ETHFromBalance).toNumber(), + ).toBeGreaterThan(0); + + // Checks DAI balances const amountToPay = expectedConversionAmount(EURExpectedAmount).mul(2); // multiply by the number of requests: 2 const feeToPay = expectedConversionAmount(EURFeeAmount).mul(2); // multiply by the number of requests: 2 const expectedAmoutToPay = amountToPay .add(feeToPay) .mul(BATCH_DENOMINATOR + BATCH_CONV_FEE) .div(BATCH_DENOMINATOR); - expect( - BigNumber.from(initialETHFromBalance).sub(ETHFromBalance).toNumber(), - ).toBeGreaterThan(0); expect(BigNumber.from(initialDAIFromBalance).sub(BigNumber.from(DAIFromBalance))).toEqual( expectedAmoutToPay, ); }); it('should pay 3 heterogeneous ERC20 payments with and without conversion', async () => { - // Get the balances to compare after payment + // Get initial balances const initialETHFromBalance = await wallet.getBalance(); - const initialDAIFromBalance = await ERC20__factory.connect( - DAITokenAddress, + const initialDAIFromBalance = await getErc20Balance( + DAIValidRequest, + wallet.address, provider, - ).balanceOf(wallet.address); + ); // Convert the two first requests and pay the three requests const tx = await payBatchConversionProxyRequest( @@ -472,13 +480,17 @@ describe('erc20-batch-conversion-proxy', () => { const confirmedTx = await tx.wait(1); expect(confirmedTx.status).toEqual(1); expect(tx.hash).toBeDefined(); - // Get the new balances + + // Get balances const ETHFromBalance = await wallet.getBalance(); - const DAIFromBalance = await ERC20__factory.connect(DAITokenAddress, provider).balanceOf( - wallet.address, - ); + const DAIFromBalance = await getErc20Balance(DAIValidRequest, wallet.address, provider); - // Check each balance + // Checks ETH balances + expect( + BigNumber.from(initialETHFromBalance).sub(ETHFromBalance).toNumber(), + ).toBeGreaterThan(0); + + // Checks DAI balances let expectedConvAmountToPay = expectedConversionAmount(EURExpectedAmount).mul(2); // multiply by the number of conversion requests: 2 const feeToPay = expectedConversionAmount(EURFeeAmount).mul(2); // multiply by the number of conversion requests: 2 // expectedConvAmountToPay with fees and batch fees @@ -486,15 +498,11 @@ describe('erc20-batch-conversion-proxy', () => { .add(feeToPay) .mul(BATCH_DENOMINATOR + BATCH_CONV_FEE) .div(BATCH_DENOMINATOR); - const expectedNoConvAmountToPay = BigNumber.from(DAIValidRequest.expectedAmount) .add(feeAmount) .mul(BATCH_DENOMINATOR + BATCH_FEE) .div(BATCH_DENOMINATOR); - expect( - BigNumber.from(initialETHFromBalance).sub(ETHFromBalance).toNumber(), - ).toBeGreaterThan(0); expect(BigNumber.from(initialDAIFromBalance).sub(BigNumber.from(DAIFromBalance))).toEqual( expectedConvAmountToPay.add(expectedNoConvAmountToPay), ); @@ -582,7 +590,7 @@ describe('erc20-batch-conversion-proxy', () => { wallet.sendTransaction = originalSendTransaction; }); it(`should pay 2 differents ERC20 requests with fees`, async () => { - // approve the contract for DAI and FAU tokens + // Approve the contract for DAI and FAU tokens const FAUApprovalTx = await approveErc20BatchConversionIfNeeded( FAUValidRequest, wallet.address, @@ -599,7 +607,7 @@ describe('erc20-batch-conversion-proxy', () => { ); if (DAIApprovalTx) await DAIApprovalTx.wait(1); - // get the balance + // Get initial balances const initialETHFromBalance = await wallet.getBalance(); const initialDAIFromBalance = await getErc20Balance( DAIValidRequest, @@ -618,23 +626,23 @@ describe('erc20-batch-conversion-proxy', () => { // Batch payment const tx = await payBatchConversionProxyRequest(enrichedRequests, batchConvVersion, wallet); const confirmedTx = await tx.wait(1); + expect(confirmedTx.status).toBe(1); + expect(tx.hash).not.toBeUndefined(); + // Get balances const ETHFromBalance = await wallet.getBalance(); const DAIFromBalance = await getErc20Balance(DAIValidRequest, wallet.address, provider); const DAIFeeBalance = await getErc20Balance(DAIValidRequest, feeAddress, provider); const FAUFromBalance = await getErc20Balance(FAUValidRequest, wallet.address, provider); const FAUFeeBalance = await getErc20Balance(FAUValidRequest, feeAddress, provider); - expect(confirmedTx.status).toBe(1); - expect(tx.hash).not.toBeUndefined(); + // Checks ETH balances expect(ETHFromBalance.lte(initialETHFromBalance)).toBeTruthy(); // 'ETH balance should be lower' - const expectedDAIFeeAmountToPay = - feeAmount + ((DAIValidRequest.expectedAmount as number) * BATCH_FEE) / BATCH_DENOMINATOR; + // Check FAU balances const expectedFAUFeeAmountToPay = feeAmount + ((FAUValidRequest.expectedAmount as number) * BATCH_FEE) / BATCH_DENOMINATOR; - // Compare FAUValidRequest balances expect(BigNumber.from(FAUFromBalance)).toEqual( BigNumber.from(initialFAUFromBalance).sub( (FAUValidRequest.expectedAmount as number) + expectedFAUFeeAmountToPay, @@ -643,7 +651,10 @@ describe('erc20-batch-conversion-proxy', () => { expect(BigNumber.from(FAUFeeBalance)).toEqual( BigNumber.from(initialFAUFeeBalance).add(expectedFAUFeeAmountToPay), ); - // compare DAIValidRequest balances + // Check DAI balances + const expectedDAIFeeAmountToPay = + feeAmount + ((DAIValidRequest.expectedAmount as number) * BATCH_FEE) / BATCH_DENOMINATOR; + expect(BigNumber.from(DAIFromBalance)).toEqual( BigNumber.from(initialDAIFromBalance) .sub(DAIValidRequest.expectedAmount as number) From 5cc575571b63cc289af4940452a101b7fafd8ab2 Mon Sep 17 00:00:00 2001 From: olivier7delf <55892112+olivier7delf@users.noreply.github.com> Date: Wed, 14 Sep 2022 18:28:39 +0200 Subject: [PATCH 099/105] refacto from PR comments --- .../src/payment/batch-conversion-proxy.ts | 42 +++++++++---------- .../payment/any-to-erc20-batch-proxy.test.ts | 17 -------- .../payment-processor/test/payment/shared.ts | 9 +--- 3 files changed, 22 insertions(+), 46 deletions(-) diff --git a/packages/payment-processor/src/payment/batch-conversion-proxy.ts b/packages/payment-processor/src/payment/batch-conversion-proxy.ts index 076f1a84c..bd45df730 100644 --- a/packages/payment-processor/src/payment/batch-conversion-proxy.ts +++ b/packages/payment-processor/src/payment/batch-conversion-proxy.ts @@ -74,35 +74,35 @@ export function prepareBatchConversionPaymentTransaction( export function encodePayBatchConversionRequest(enrichedRequests: EnrichedRequest[]): string { const { feeAddress } = getRequestPaymentValues(enrichedRequests[0].request); - let firstPn0Extension: IState | undefined; - const pn2requests: ClientTypes.IRequestData[] = []; + let firstConversionRequestExtension: IState | undefined; + const requestsWithoutConversion: ClientTypes.IRequestData[] = []; const conversionDetails: PaymentTypes.ConversionDetail[] = []; - // fill conversionDetails and pn2requests - for (let i = 0; i < enrichedRequests.length; i++) { + // fill conversionDetails and requestsWithoutConversion lists + for (const enrichedRequest of enrichedRequests) { if ( - enrichedRequests[i].paymentNetworkId === + enrichedRequest.paymentNetworkId === BATCH_PAYMENT_NETWORK_ID.BATCH_MULTI_ERC20_CONVERSION_PAYMENTS ) { - firstPn0Extension = - firstPn0Extension ?? getPaymentNetworkExtension(enrichedRequests[i].request); + firstConversionRequestExtension = + firstConversionRequestExtension ?? getPaymentNetworkExtension(enrichedRequest.request); - comparePnTypeAndVersion(firstPn0Extension, enrichedRequests[i].request); - if ( - // the type used must a fiat or an ERC20 - ![RequestLogicTypes.CURRENCY.ISO4217, RequestLogicTypes.CURRENCY.ERC20].includes( - enrichedRequests[i].request.currencyInfo.type, - ) - ) + comparePnTypeAndVersion(firstConversionRequestExtension, enrichedRequest.request); + const isErc20Currency = + enrichedRequest.request.currencyInfo.type === RequestLogicTypes.CURRENCY.ERC20; + const isISO4217Currency = + enrichedRequest.request.currencyInfo.type === RequestLogicTypes.CURRENCY.ISO4217; + // the type used must a fiat or an ERC20 + if (!(isErc20Currency || isISO4217Currency)) throw new Error(`wrong request currencyInfo type`); - conversionDetails.push(getInputConversionDetail(enrichedRequests[i])); + conversionDetails.push(getInputConversionDetail(enrichedRequest)); } else if ( - enrichedRequests[i].paymentNetworkId === BATCH_PAYMENT_NETWORK_ID.BATCH_MULTI_ERC20_PAYMENTS + enrichedRequest.paymentNetworkId === BATCH_PAYMENT_NETWORK_ID.BATCH_MULTI_ERC20_PAYMENTS ) { - pn2requests.push(enrichedRequests[i].request); + requestsWithoutConversion.push(enrichedRequest.request); comparePnTypeAndVersion( - getPaymentNetworkExtension(pn2requests[0]), - enrichedRequests[i].request, + getPaymentNetworkExtension(requestsWithoutConversion[0]), + enrichedRequest.request, ); } } @@ -124,9 +124,9 @@ export function encodePayBatchConversionRequest(enrichedRequests: EnrichedReques } // Get values and add cryptoDetails to metaDetails - if (pn2requests.length > 0) { + if (requestsWithoutConversion.length > 0) { const { tokenAddresses, paymentAddresses, amountsToPay, paymentReferences, feesToPay } = - getBatchArgs(pn2requests, 'ERC20'); + getBatchArgs(requestsWithoutConversion, 'ERC20'); // add ERC20 no-conversion payments metaDetails.push({ diff --git a/packages/payment-processor/test/payment/any-to-erc20-batch-proxy.test.ts b/packages/payment-processor/test/payment/any-to-erc20-batch-proxy.test.ts index 7fa6d7827..4bfcf77b3 100644 --- a/packages/payment-processor/test/payment/any-to-erc20-batch-proxy.test.ts +++ b/packages/payment-processor/test/payment/any-to-erc20-batch-proxy.test.ts @@ -229,23 +229,6 @@ describe('erc20-batch-conversion-proxy', () => { }), ); }); - it('should throw an error if request has no extension', async () => { - EURRequest.extensions = [] as any; - - await expect( - payBatchConversionProxyRequest( - [ - { - paymentNetworkId: BATCH_PAYMENT_NETWORK_ID.BATCH_MULTI_ERC20_CONVERSION_PAYMENTS, - request: EURRequest, - paymentSettings: alphaPaymentSettings, - }, - ], - batchConvVersion, - wallet, - ), - ).rejects.toThrowError('no payment network found'); - }); it('should throw an error if request has no paymentSettings', async () => { await expect( payBatchConversionProxyRequest( diff --git a/packages/payment-processor/test/payment/shared.ts b/packages/payment-processor/test/payment/shared.ts index 791b99883..734381a88 100644 --- a/packages/payment-processor/test/payment/shared.ts +++ b/packages/payment-processor/test/payment/shared.ts @@ -1,5 +1,5 @@ import { CurrencyManager, CurrencyDefinition } from '@requestnetwork/currency'; -import { RequestLogicTypes, ClientTypes } from '@requestnetwork/types'; +import { RequestLogicTypes } from '@requestnetwork/types'; export const currencyManager = new CurrencyManager([ ...CurrencyManager.getDefaultList(), @@ -24,10 +24,3 @@ export const currencyManager = new CurrencyManager([ type: RequestLogicTypes.CURRENCY.ERC20, })), ]); - -export const sameCurrencyValue = ( - requestA: ClientTypes.IRequestData, - requestB: ClientTypes.IRequestData, -): boolean => { - return requestA.currencyInfo.value === requestB.currencyInfo.value; -}; From 78d8c3b4292ad4d75147af9da0f157831fb4dc91 Mon Sep 17 00:00:00 2001 From: olivier7delf <55892112+olivier7delf@users.noreply.github.com> Date: Wed, 14 Sep 2022 19:30:23 +0200 Subject: [PATCH 100/105] use type guards for ERC20 and ISO4217 currencies --- .../src/payment/batch-conversion-proxy.ts | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/packages/payment-processor/src/payment/batch-conversion-proxy.ts b/packages/payment-processor/src/payment/batch-conversion-proxy.ts index bd45df730..7d64b12f2 100644 --- a/packages/payment-processor/src/payment/batch-conversion-proxy.ts +++ b/packages/payment-processor/src/payment/batch-conversion-proxy.ts @@ -1,7 +1,7 @@ import { ContractTransaction, Signer, providers, BigNumber, constants } from 'ethers'; import { batchConversionPaymentsArtifact } from '@requestnetwork/smart-contracts'; import { BatchConversionPayments__factory } from '@requestnetwork/smart-contracts/types'; -import { ClientTypes, RequestLogicTypes, PaymentTypes } from '@requestnetwork/types'; +import { ClientTypes, PaymentTypes } from '@requestnetwork/types'; import { ITransactionOverrides } from './transaction-overrides'; import { comparePnTypeAndVersion, @@ -21,6 +21,7 @@ import { getBatchArgs } from './batch-proxy'; import { checkErc20Allowance, encodeApproveAnyErc20 } from './erc20'; import { BATCH_PAYMENT_NETWORK_ID } from '@requestnetwork/types/dist/payment-types'; import { IState } from 'types/dist/extension-types'; +import { CurrencyInput, isERC20Currency, isISO4217Currency } from '@requestnetwork/currency/dist'; /** * Processes a transaction to pay a batch of requests with an ERC20 currency @@ -88,12 +89,12 @@ export function encodePayBatchConversionRequest(enrichedRequests: EnrichedReques firstConversionRequestExtension ?? getPaymentNetworkExtension(enrichedRequest.request); comparePnTypeAndVersion(firstConversionRequestExtension, enrichedRequest.request); - const isErc20Currency = - enrichedRequest.request.currencyInfo.type === RequestLogicTypes.CURRENCY.ERC20; - const isISO4217Currency = - enrichedRequest.request.currencyInfo.type === RequestLogicTypes.CURRENCY.ISO4217; - // the type used must a fiat or an ERC20 - if (!(isErc20Currency || isISO4217Currency)) + if ( + !( + isERC20Currency(enrichedRequest.request.currencyInfo as unknown as CurrencyInput) || + isISO4217Currency(enrichedRequest.request.currencyInfo as unknown as CurrencyInput) + ) + ) throw new Error(`wrong request currencyInfo type`); conversionDetails.push(getInputConversionDetail(enrichedRequest)); } else if ( From 20a8424749735b53a8cbfc333c81486e2dcd12cb Mon Sep 17 00:00:00 2001 From: olivier7delf <55892112+olivier7delf@users.noreply.github.com> Date: Thu, 15 Sep 2022 17:22:42 +0200 Subject: [PATCH 101/105] PR comments B --- .../src/payment/any-to-erc20-proxy.ts | 8 ++- .../src/payment/batch-conversion-proxy.ts | 62 ++++++++++--------- .../payment-processor/src/payment/index.ts | 7 ++- .../payment-processor/src/payment/utils.ts | 20 ++++-- .../payment/any-to-erc20-batch-proxy.test.ts | 52 +++++++++++----- 5 files changed, 94 insertions(+), 55 deletions(-) diff --git a/packages/payment-processor/src/payment/any-to-erc20-proxy.ts b/packages/payment-processor/src/payment/any-to-erc20-proxy.ts index cdc9de379..baceeb827 100644 --- a/packages/payment-processor/src/payment/any-to-erc20-proxy.ts +++ b/packages/payment-processor/src/payment/any-to-erc20-proxy.ts @@ -73,7 +73,7 @@ export function encodePayAnyToErc20ProxyRequest( maxRateTimespan, amountToPay, feeToPay, - } = prepAnyToErc20ProxyRequest(request, paymentSettings, amount, feeAmountOverride); + } = prepareAnyToErc20Arguments(request, paymentSettings, amount, feeAmountOverride); const proxyContract = Erc20ConversionProxy__factory.createInterface(); return proxyContract.encodeFunctionData('transferFromWithReferenceAndFee', [ paymentAddress, @@ -132,7 +132,11 @@ export function checkRequestAndGetPathAndCurrency( validateConversionFeeProxyRequest(request, path, amount, feeAmountOverride); return { path, requestCurrency }; } -export function prepAnyToErc20ProxyRequest( + +/** + * Prepares all necessaries arguments required to encode an any-to-erc20 request + */ +function prepareAnyToErc20Arguments( request: ClientTypes.IRequestData, paymentSettings: IConversionPaymentSettings, amount?: BigNumberish, diff --git a/packages/payment-processor/src/payment/batch-conversion-proxy.ts b/packages/payment-processor/src/payment/batch-conversion-proxy.ts index 7d64b12f2..e004aaf32 100644 --- a/packages/payment-processor/src/payment/batch-conversion-proxy.ts +++ b/packages/payment-processor/src/payment/batch-conversion-proxy.ts @@ -5,6 +5,7 @@ import { ClientTypes, PaymentTypes } from '@requestnetwork/types'; import { ITransactionOverrides } from './transaction-overrides'; import { comparePnTypeAndVersion, + getPnAndNetwork, getProvider, getProxyAddress, getRequestPaymentValues, @@ -29,7 +30,7 @@ import { CurrencyInput, isERC20Currency, isISO4217Currency } from '@requestnetwo * The payment is made through ERC20 or ERC20Conversion proxies * It can be used with a Multisig contract * @param enrichedRequests List of EnrichedRequest to pay - * @param version Version of the batch conversion proxy + * @param version The version of the batch conversion proxy * @param signerOrProvider The Web3 provider, or signer. Defaults to window.ethereum. * @param overrides Optionally, override default transaction values, like gas. * @dev We only implement batchRouter using two ERC20 functions: @@ -51,7 +52,7 @@ export async function payBatchConversionProxyRequest( * that is different from the request currency (eg. fiat) * it can be used with a Multisig contract. * @param enrichedRequests List of EnrichedRequest to pay - * @param version Version of the batch conversion proxy + * @param version The version of the batch conversion proxy */ export function prepareBatchConversionPaymentTransaction( enrichedRequests: EnrichedRequest[], @@ -75,6 +76,7 @@ export function prepareBatchConversionPaymentTransaction( export function encodePayBatchConversionRequest(enrichedRequests: EnrichedRequest[]): string { const { feeAddress } = getRequestPaymentValues(enrichedRequests[0].request); + const firstNetwork = getPnAndNetwork(enrichedRequests[0].request)[1]; let firstConversionRequestExtension: IState | undefined; const requestsWithoutConversion: ClientTypes.IRequestData[] = []; const conversionDetails: PaymentTypes.ConversionDetail[] = []; @@ -106,6 +108,8 @@ export function encodePayBatchConversionRequest(enrichedRequests: EnrichedReques enrichedRequest.request, ); } + if (firstNetwork !== getPnAndNetwork(enrichedRequest.request)[1]) + throw new Error('All the requests must have the same network'); } const metaDetails: PaymentTypes.MetaDetail[] = []; @@ -152,7 +156,7 @@ export function encodePayBatchConversionRequest(enrichedRequests: EnrichedReques /** * Get the conversion detail values from one enriched request - * @param enrichedRequest enrichedRequest to pay + * @param enrichedRequest The enrichedRequest to pay */ function getInputConversionDetail(enrichedRequest: EnrichedRequest): PaymentTypes.ConversionDetail { const paymentSettings = enrichedRequest.paymentSettings; @@ -193,8 +197,8 @@ function getBatchDeploymentInformation( /** * Gets batch conversion contract Address - * @param request request for an ERC20 payment with/out conversion - * @param version of the batch conversion proxy + * @param request The request for an ERC20 payment with/out conversion + * @param version The version of the batch conversion proxy */ export function getBatchConversionProxyAddress( request: ClientTypes.IRequestData, @@ -209,11 +213,11 @@ export function getBatchConversionProxyAddress( /** * Processes the approval transaction of the targeted ERC20 with batch conversion proxy. - * @param request request for an ERC20 payment with/out conversion - * @param account account that will be used to pay the request - * @param version version of the batch conversion proxy, which can be different from request pn version - * @param signerOrProvider the Web3 provider, or signer. Defaults to window.ethereum. - * @param paymentSettings paymentSettings is necessary for conversion payment approval + * @param request The request for an ERC20 payment with/out conversion + * @param account The account that will be used to pay the request + * @param version The version of the batch conversion proxy, which can be different from request pn version + * @param signerOrProvider The Web3 provider, or signer. Defaults to window.ethereum. + * @param paymentSettings The payment settings are necessary for conversion payment approval * @param overrides optionally, override default transaction values, like gas. */ export async function approveErc20BatchConversionIfNeeded( @@ -246,11 +250,11 @@ export async function approveErc20BatchConversionIfNeeded( /** * Checks if the batch conversion proxy has the necessary allowance from a given account * to pay a given request with ERC20 batch conversion proxy - * @param request request for an ERC20 payment with/out conversion - * @param account account that will be used to pay the request - * @param version version of the batch conversion proxy - * @param signerOrProvider the Web3 provider, or signer. Defaults to window.ethereum. - * @param paymentSettings paymentSettings is necessary for conversion payment approval + * @param request The request for an ERC20 payment with/out conversion + * @param account The account that will be used to pay the request + * @param version The version of the batch conversion proxy + * @param signerOrProvider The Web3 provider, or signer. Defaults to window.ethereum. + * @param paymentSettings The payment settings are necessary for conversion payment approval */ export async function hasErc20BatchConversionApproval( request: ClientTypes.IRequestData, @@ -271,10 +275,10 @@ export async function hasErc20BatchConversionApproval( /** * Processes the transaction to approve the batch conversion proxy to spend signer's tokens to pay * the request in its payment currency. Can be used with a Multisig contract. - * @param request request for an ERC20 payment with/out conversion - * @param version version of the batch conversion proxy, which can be different from request pn version - * @param signerOrProvider the Web3 provider, or signer. Defaults to window.ethereum. - * @param paymentSettings paymentSettings is necessary for conversion payment approval + * @param request The request for an ERC20 payment with/out conversion + * @param version The version of the batch conversion proxy, which can be different from request pn version + * @param signerOrProvider The Web3 provider, or signer. Defaults to window.ethereum. + * @param paymentSettings The payment settings are necessary for conversion payment approval * @param overrides optionally, override default transaction values, like gas. */ export async function approveErc20BatchConversion( @@ -299,10 +303,10 @@ export async function approveErc20BatchConversion( /** * Prepare the transaction to approve the proxy to spend signer's tokens to pay * the request in its payment currency. Can be used with a Multisig contract. - * @param request request for an ERC20 payment with/out conversion - * @param version version of the batch conversion proxy - * @param signerOrProvider the Web3 provider, or signer. Defaults to window.ethereum. - * @param paymentSettings paymentSettings is necessary for conversion payment approval + * @param request The request for an ERC20 payment with/out conversion + * @param version The version of the batch conversion proxy + * @param signerOrProvider The Web3 provider, or signer. Defaults to window.ethereum. + * @param paymentSettings The payment settings are necessary for conversion payment approval * @param overrides optionally, override default transaction values, like gas. */ export function prepareApproveErc20BatchConversion( @@ -329,10 +333,10 @@ export function prepareApproveErc20BatchConversion( /** * Encodes the transaction to approve the batch conversion proxy to spend signer's tokens to pay * the request in its payment currency. Can be used with a Multisig contract. - * @param request request for an ERC20 payment with/out conversion - * @param version version of the batch conversion proxy - * @param signerOrProvider the Web3 provider, or signer. Defaults to window.ethereum. - * @param paymentSettings paymentSettings is necessary for conversion payment approval + * @param request The request for an ERC20 payment with/out conversion + * @param version The version of the batch conversion proxy + * @param signerOrProvider The Web3 provider, or signer. Defaults to window.ethereum. + * @param paymentSettings The payment settings are necessary for conversion payment approval */ export function encodeApproveErc20BatchConversion( request: ClientTypes.IRequestData, @@ -351,8 +355,8 @@ export function encodeApproveErc20BatchConversion( /** * Get the address of the token to interact with, * if it is a conversion payment, the info is inside paymentSettings - * @param request request for an ERC20 payment with/out conversion - * @param paymentSettings paymentSettings is necessary for conversion payment + * @param request The request for an ERC20 payment with/out conversion + * @param paymentSettings The payment settings are necessary for conversion payment * */ function getTokenAddress( request: ClientTypes.IRequestData, diff --git a/packages/payment-processor/src/payment/index.ts b/packages/payment-processor/src/payment/index.ts index 81e677a4c..ee1f039db 100644 --- a/packages/payment-processor/src/payment/index.ts +++ b/packages/payment-processor/src/payment/index.ts @@ -1,6 +1,6 @@ import { ContractTransaction, Signer, BigNumber, BigNumberish, providers } from 'ethers'; -import { ClientTypes, ExtensionTypes } from '@requestnetwork/types'; +import { ClientTypes, ExtensionTypes, PaymentTypes } from '@requestnetwork/types'; import { getBtcPaymentUrl } from './btc-address-based'; import { _getErc20PaymentUrl, getAnyErc20Balance } from './erc20'; @@ -333,9 +333,12 @@ const throwIfNotWeb3 = (request: ClientTypes.IRequestData) => { * Currently, these requests must have the same PN, version, and batchFee * Also used in Invoicing repository. * @dev next step: paymentNetworkId could get more values options, see the "ref" + * in batchConversionPayment.sol */ export interface EnrichedRequest { - paymentNetworkId: 0 | 2; // ref in batchConversionPayment.sol + paymentNetworkId: + | PaymentTypes.BATCH_PAYMENT_NETWORK_ID.BATCH_MULTI_ERC20_CONVERSION_PAYMENTS + | PaymentTypes.BATCH_PAYMENT_NETWORK_ID.BATCH_MULTI_ERC20_PAYMENTS; request: ClientTypes.IRequestData; paymentSettings?: IConversionPaymentSettings; amount?: BigNumberish; diff --git a/packages/payment-processor/src/payment/utils.ts b/packages/payment-processor/src/payment/utils.ts index 81c85c00b..464c4e659 100644 --- a/packages/payment-processor/src/payment/utils.ts +++ b/packages/payment-processor/src/payment/utils.ts @@ -132,19 +132,27 @@ export const getProxyNetwork = ( throw new Error('Payment currency must have a network'); }; +export function getPnAndNetwork( + request: ClientTypes.IRequestData, +): [ExtensionTypes.IState, string] { + const pn = getPaymentNetworkExtension(request); + if (!pn) { + throw new Error('PaymentNetwork not found'); + } + return [pn, getProxyNetwork(pn, request.currencyInfo)]; +} + /** - * @param version version has to be set to get batch conversion proxy + * @param request the request + * @param getDeploymentInformation the function to get the proxy address + * @param version the version has to be set to get batch conversion proxy */ export const getProxyAddress = ( request: ClientTypes.IRequestData, getDeploymentInformation: (network: string, version: string) => { address: string } | null, version?: string, ): string => { - const pn = getPaymentNetworkExtension(request); - if (!pn) { - throw new Error('PaymentNetwork not found'); - } - const network = getProxyNetwork(pn, request.currencyInfo); + const [pn, network] = getPnAndNetwork(request); const deploymentInfo = getDeploymentInformation(network, version || pn.version); if (!deploymentInfo) { throw new Error(`No deployment found for network ${network}, version ${version || pn.version}`); diff --git a/packages/payment-processor/test/payment/any-to-erc20-batch-proxy.test.ts b/packages/payment-processor/test/payment/any-to-erc20-batch-proxy.test.ts index 4bfcf77b3..4b94551b6 100644 --- a/packages/payment-processor/test/payment/any-to-erc20-batch-proxy.test.ts +++ b/packages/payment-processor/test/payment/any-to-erc20-batch-proxy.test.ts @@ -58,7 +58,7 @@ const EURFeeAmount = 2; const expectedAmount = 100000; const feeAmount = 100; -const validEURRequest: ClientTypes.IRequestData = { +const EURValidRequest: ClientTypes.IRequestData = { balance: { balance: '0', events: [], @@ -173,13 +173,13 @@ describe('erc20-batch-conversion-proxy', () => { beforeAll(async () => { // Revoke DAI and FAU approvals await revokeErc20Approval( - getBatchConversionProxyAddress(validEURRequest, batchConvVersion), + getBatchConversionProxyAddress(DAIValidRequest, batchConvVersion), DAITokenAddress, wallet, ); await revokeErc20Approval( getBatchConversionProxyAddress(FAUValidRequest, batchConvVersion), - DAITokenAddress, + FAUTokenAddress, wallet, ); }); @@ -187,11 +187,11 @@ describe('erc20-batch-conversion-proxy', () => { describe(`Conversion:`, () => { beforeEach(() => { jest.restoreAllMocks(); - EURRequest = Utils.deepCopy(validEURRequest); + EURRequest = Utils.deepCopy(EURValidRequest); enrichedRequests = [ { paymentNetworkId: BATCH_PAYMENT_NETWORK_ID.BATCH_MULTI_ERC20_CONVERSION_PAYMENTS, - request: validEURRequest, + request: EURValidRequest, paymentSettings: alphaPaymentSettings, }, { @@ -209,7 +209,7 @@ describe('erc20-batch-conversion-proxy', () => { [ { paymentNetworkId: BATCH_PAYMENT_NETWORK_ID.BATCH_MULTI_ERC20_CONVERSION_PAYMENTS, - request: validEURRequest, + request: EURValidRequest, paymentSettings: { ...alphaPaymentSettings, currency: { @@ -250,6 +250,28 @@ describe('erc20-batch-conversion-proxy', () => { payBatchConversionProxyRequest(enrichedRequests, batchConvVersion, wallet), ).rejects.toThrowError(`wrong request currencyInfo type`); }); + it('should throw an error if the request has a wrong network', async () => { + EURRequest.extensions = { + // ERC20_FEE_PROXY_CONTRACT instead of ANY_TO_ERC20_PROXY + [PaymentTypes.PAYMENT_NETWORK_ID.ANY_TO_ERC20_PROXY]: { + events: [], + id: ExtensionTypes.ID.PAYMENT_NETWORK_ERC20_FEE_PROXY_CONTRACT, + type: ExtensionTypes.TYPE.PAYMENT_NETWORK, + values: { + feeAddress, + feeAmount: feeAmount, + paymentAddress: paymentAddress, + salt: 'salt', + network: 'fakePrivate', + }, + version: '0.1.0', + }, + }; + + await expect( + payBatchConversionProxyRequest(enrichedRequests, batchConvVersion, wallet), + ).rejects.toThrowError('All the requests must have the same network'); + }); it('should throw an error if the request has a wrong payment network id', async () => { EURRequest.extensions = { // ERC20_FEE_PROXY_CONTRACT instead of ANY_TO_ERC20_PROXY @@ -307,7 +329,7 @@ describe('erc20-batch-conversion-proxy', () => { [ { paymentNetworkId: BATCH_PAYMENT_NETWORK_ID.BATCH_MULTI_ERC20_CONVERSION_PAYMENTS, - request: validEURRequest, + request: EURValidRequest, paymentSettings: alphaPaymentSettings, }, ], @@ -318,7 +340,7 @@ describe('erc20-batch-conversion-proxy', () => { expect(spy).toHaveBeenCalledWith({ data: '0xf0fa379f0000000000000000000000000000000000000000000000000000000000000040000000000000000000000000c5fdf4076b8f3a5357c5e395ab970b5b54098fef0000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000024000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000020000000000000000000000000f17f52151ebef6c7334fad080c5704d77216b7320000000000000000000000000000000000000000000000000000000005f5e10000000000000000000000000000000000000000000000000000000000000000e0000000000000000000000000000000000000000000000000000000000000016000000000000000000000000000000000000000000000000000000000001e84800000000000000000000000000000000000000000204fce5e3e250261100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000300000000000000000000000017b4158805772ced11225e77339f90beb5aae968000000000000000000000000775eb53d00dd0acd3ec1696472105d579b9b386b00000000000000000000000038cf23c52bb4b13f051aec09580a2de845a7fa35000000000000000000000000000000000000000000000000000000000000000886dfbccad783599a00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000c000000000000000000000000000000000000000000000000000000000000000e00000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000012000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000', gasPrice: '20000000000', - to: getBatchConversionProxyAddress(validEURRequest, '0.1.0'), + to: getBatchConversionProxyAddress(EURValidRequest, '0.1.0'), value: 0, }); wallet.sendTransaction = originalSendTransaction; @@ -326,7 +348,7 @@ describe('erc20-batch-conversion-proxy', () => { it('should convert and pay a request in EUR with ERC20', async () => { // Approve the contract const approvalTx = await approveErc20BatchConversionIfNeeded( - validEURRequest, + EURValidRequest, wallet.address, batchConvVersion, wallet.provider, @@ -350,7 +372,7 @@ describe('erc20-batch-conversion-proxy', () => { [ { paymentNetworkId: BATCH_PAYMENT_NETWORK_ID.BATCH_MULTI_ERC20_CONVERSION_PAYMENTS, - request: validEURRequest, + request: EURValidRequest, paymentSettings: alphaPaymentSettings, }, ], @@ -400,7 +422,7 @@ describe('erc20-batch-conversion-proxy', () => { const tx = await payBatchConversionProxyRequest( Array(2).fill({ paymentNetworkId: BATCH_PAYMENT_NETWORK_ID.BATCH_MULTI_ERC20_CONVERSION_PAYMENTS, - request: validEURRequest, + request: EURValidRequest, paymentSettings: alphaPaymentSettings, }), batchConvVersion, @@ -444,12 +466,12 @@ describe('erc20-batch-conversion-proxy', () => { [ { paymentNetworkId: BATCH_PAYMENT_NETWORK_ID.BATCH_MULTI_ERC20_CONVERSION_PAYMENTS, - request: validEURRequest, + request: EURValidRequest, paymentSettings: alphaPaymentSettings, }, { paymentNetworkId: BATCH_PAYMENT_NETWORK_ID.BATCH_MULTI_ERC20_CONVERSION_PAYMENTS, - request: validEURRequest, + request: EURValidRequest, paymentSettings: alphaPaymentSettings, }, { @@ -531,9 +553,7 @@ describe('erc20-batch-conversion-proxy', () => { FAURequest.currencyInfo.network = ''; await expect( payBatchConversionProxyRequest(enrichedRequests, batchConvVersion, wallet), - ).rejects.toThrowError( - 'request cannot be processed, or is not an pn-erc20-fee-proxy-contract request', - ); + ).rejects.toThrowError('Payment currency must have a network'); }); it('should throw an error if request has no extension', async () => { From dba87f8fc88463f970a580fe4e8e0dae1dd83232 Mon Sep 17 00:00:00 2001 From: olivier7delf <55892112+olivier7delf@users.noreply.github.com> Date: Thu, 15 Sep 2022 18:23:38 +0200 Subject: [PATCH 102/105] add comments --- .../src/payment/batch-conversion-proxy.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/packages/payment-processor/src/payment/batch-conversion-proxy.ts b/packages/payment-processor/src/payment/batch-conversion-proxy.ts index e004aaf32..338d1cf82 100644 --- a/packages/payment-processor/src/payment/batch-conversion-proxy.ts +++ b/packages/payment-processor/src/payment/batch-conversion-proxy.ts @@ -78,6 +78,7 @@ export function encodePayBatchConversionRequest(enrichedRequests: EnrichedReques const firstNetwork = getPnAndNetwork(enrichedRequests[0].request)[1]; let firstConversionRequestExtension: IState | undefined; + let firstNoConversionRequestExtension: IState | undefined; const requestsWithoutConversion: ClientTypes.IRequestData[] = []; const conversionDetails: PaymentTypes.ConversionDetail[] = []; @@ -102,11 +103,12 @@ export function encodePayBatchConversionRequest(enrichedRequests: EnrichedReques } else if ( enrichedRequest.paymentNetworkId === BATCH_PAYMENT_NETWORK_ID.BATCH_MULTI_ERC20_PAYMENTS ) { + firstNoConversionRequestExtension = + firstNoConversionRequestExtension ?? getPaymentNetworkExtension(enrichedRequest.request); + + // isERC20Currency is checked within getBatchArgs function + comparePnTypeAndVersion(firstNoConversionRequestExtension, enrichedRequest.request); requestsWithoutConversion.push(enrichedRequest.request); - comparePnTypeAndVersion( - getPaymentNetworkExtension(requestsWithoutConversion[0]), - enrichedRequest.request, - ); } if (firstNetwork !== getPnAndNetwork(enrichedRequest.request)[1]) throw new Error('All the requests must have the same network'); From 7a9ecf30ed15dff7ac2363c9a5220c76059b9a13 Mon Sep 17 00:00:00 2001 From: olivier7delf <55892112+olivier7delf@users.noreply.github.com> Date: Thu, 15 Sep 2022 18:36:25 +0200 Subject: [PATCH 103/105] convention if --- .../payment-processor/src/payment/batch-conversion-proxy.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/payment-processor/src/payment/batch-conversion-proxy.ts b/packages/payment-processor/src/payment/batch-conversion-proxy.ts index 338d1cf82..5fe6ca56f 100644 --- a/packages/payment-processor/src/payment/batch-conversion-proxy.ts +++ b/packages/payment-processor/src/payment/batch-conversion-proxy.ts @@ -97,8 +97,9 @@ export function encodePayBatchConversionRequest(enrichedRequests: EnrichedReques isERC20Currency(enrichedRequest.request.currencyInfo as unknown as CurrencyInput) || isISO4217Currency(enrichedRequest.request.currencyInfo as unknown as CurrencyInput) ) - ) + ) { throw new Error(`wrong request currencyInfo type`); + } conversionDetails.push(getInputConversionDetail(enrichedRequest)); } else if ( enrichedRequest.paymentNetworkId === BATCH_PAYMENT_NETWORK_ID.BATCH_MULTI_ERC20_PAYMENTS From 5beb6ca9eebabe5b438d23f07b9f5a9026f114e8 Mon Sep 17 00:00:00 2001 From: olivier7delf <55892112+olivier7delf@users.noreply.github.com> Date: Fri, 16 Sep 2022 08:56:50 +0200 Subject: [PATCH 104/105] Add every params description to functions --- .../src/payment/any-to-erc20-proxy.ts | 29 +++++++++------- .../src/payment/batch-conversion-proxy.ts | 18 ++++++---- .../src/payment/batch-proxy.ts | 33 ++++++++++--------- .../payment-processor/src/payment/utils.ts | 18 +++++++--- 4 files changed, 60 insertions(+), 38 deletions(-) diff --git a/packages/payment-processor/src/payment/any-to-erc20-proxy.ts b/packages/payment-processor/src/payment/any-to-erc20-proxy.ts index baceeb827..8f8144538 100644 --- a/packages/payment-processor/src/payment/any-to-erc20-proxy.ts +++ b/packages/payment-processor/src/payment/any-to-erc20-proxy.ts @@ -25,12 +25,12 @@ import { IConversionPaymentSettings } from './index'; /** * Processes a transaction to pay a request with an ERC20 currency that is different from the request currency (eg. fiat). * The payment is made by the ERC20 Conversion fee proxy contract. - * @param request the request to pay - * @param signerOrProvider the Web3 provider, or signer. Defaults to window.ethereum. - * @param paymentSettings payment settings - * @param amount optionally, the amount to pay. Defaults to remaining amount of the request. - * @param feeAmount optionally, the fee amount to pay. Defaults to the fee amount. - * @param overrides optionally, override default transaction values, like gas. + * @param request The request to pay + * @param signerOrProvider The Web3 provider, or signer. Defaults to window.ethereum. + * @param paymentSettings The payment settings + * @param amount Optionally, the amount to pay. Defaults to remaining amount of the request. + * @param feeAmount Optionally, the fee amount to pay. Defaults to the fee amount. + * @param overrides Optionally, override default transaction values, like gas. */ export async function payAnyToErc20ProxyRequest( request: ClientTypes.IRequestData, @@ -53,11 +53,10 @@ export async function payAnyToErc20ProxyRequest( /** * Encodes the call to pay a request with an ERC20 currency that is different from the request currency (eg. fiat). * The payment is made by the ERC20 Conversion fee proxy contract. - * @param request request to pay - * @param signerOrProvider the Web3 provider, or signer. Defaults to window.ethereum. - * @param paymentSettings payment settings - * @param amount optionally, the amount to pay. Defaults to remaining amount of the request. - * @param feeAmountOverride optionally, the fee amount to pay. Defaults to the fee amount of the request. + * @param request The request to pay + * @param paymentSettings The payment settings + * @param amount Optionally, the amount to pay. Defaults to remaining amount of the request. + * @param feeAmountOverride Optionally, the fee amount to pay. Defaults to the fee amount of the request. */ export function encodePayAnyToErc20ProxyRequest( request: ClientTypes.IRequestData, @@ -89,6 +88,10 @@ export function encodePayAnyToErc20ProxyRequest( /** * It checks paymentSettings values, it get request's path and requestCurrency + * @param request The request to pay + * @param paymentSettings The payment settings + * @param amount Optionally, the amount to pay. Defaults to remaining amount of the request. + * @param feeAmountOverride Optionally, the fee amount to pay. Defaults to the fee amount of the request. */ export function checkRequestAndGetPathAndCurrency( request: ClientTypes.IRequestData, @@ -135,6 +138,10 @@ export function checkRequestAndGetPathAndCurrency( /** * Prepares all necessaries arguments required to encode an any-to-erc20 request + * @param request The request to pay + * @param paymentSettings The payment settings + * @param amount Optionally, the amount to pay. Defaults to remaining amount of the request. + * @param feeAmountOverride Optionally, the fee amount to pay. Defaults to the fee amount of the request. */ function prepareAnyToErc20Arguments( request: ClientTypes.IRequestData, diff --git a/packages/payment-processor/src/payment/batch-conversion-proxy.ts b/packages/payment-processor/src/payment/batch-conversion-proxy.ts index 5fe6ca56f..60715f635 100644 --- a/packages/payment-processor/src/payment/batch-conversion-proxy.ts +++ b/packages/payment-processor/src/payment/batch-conversion-proxy.ts @@ -29,7 +29,7 @@ import { CurrencyInput, isERC20Currency, isISO4217Currency } from '@requestnetwo * that is different from the request currency (eg. fiat) * The payment is made through ERC20 or ERC20Conversion proxies * It can be used with a Multisig contract - * @param enrichedRequests List of EnrichedRequest to pay + * @param enrichedRequests List of EnrichedRequests to pay * @param version The version of the batch conversion proxy * @param signerOrProvider The Web3 provider, or signer. Defaults to window.ethereum. * @param overrides Optionally, override default transaction values, like gas. @@ -51,7 +51,7 @@ export async function payBatchConversionProxyRequest( * Prepares a transaction to pay a batch of requests with an ERC20 currency * that is different from the request currency (eg. fiat) * it can be used with a Multisig contract. - * @param enrichedRequests List of EnrichedRequest to pay + * @param enrichedRequests List of EnrichedRequests to pay * @param version The version of the batch conversion proxy */ export function prepareBatchConversionPaymentTransaction( @@ -71,7 +71,7 @@ export function prepareBatchConversionPaymentTransaction( * Encodes a transaction to pay a batch of requests with an ERC20 currency * that is different from the request currency (eg. fiat) * It can be used with a Multisig contract. - * @param enrichedRequests list of ECR20 requests to pay + * @param enrichedRequests List of EnrichedRequests to pay */ export function encodePayBatchConversionRequest(enrichedRequests: EnrichedRequest[]): string { const { feeAddress } = getRequestPaymentValues(enrichedRequests[0].request); @@ -191,6 +191,12 @@ function getInputConversionDetail(enrichedRequest: EnrichedRequest): PaymentType }; } +/** + * + * @param network The network targeted + * @param version The version of the batch conversion proxy + * @returns + */ function getBatchDeploymentInformation( network: string, version: string, @@ -221,7 +227,7 @@ export function getBatchConversionProxyAddress( * @param version The version of the batch conversion proxy, which can be different from request pn version * @param signerOrProvider The Web3 provider, or signer. Defaults to window.ethereum. * @param paymentSettings The payment settings are necessary for conversion payment approval - * @param overrides optionally, override default transaction values, like gas. + * @param overrides Optionally, override default transaction values, like gas. */ export async function approveErc20BatchConversionIfNeeded( request: ClientTypes.IRequestData, @@ -282,7 +288,7 @@ export async function hasErc20BatchConversionApproval( * @param version The version of the batch conversion proxy, which can be different from request pn version * @param signerOrProvider The Web3 provider, or signer. Defaults to window.ethereum. * @param paymentSettings The payment settings are necessary for conversion payment approval - * @param overrides optionally, override default transaction values, like gas. + * @param overrides Optionally, override default transaction values, like gas. */ export async function approveErc20BatchConversion( request: ClientTypes.IRequestData, @@ -310,7 +316,7 @@ export async function approveErc20BatchConversion( * @param version The version of the batch conversion proxy * @param signerOrProvider The Web3 provider, or signer. Defaults to window.ethereum. * @param paymentSettings The payment settings are necessary for conversion payment approval - * @param overrides optionally, override default transaction values, like gas. + * @param overrides Optionally, override default transaction values, like gas. */ export function prepareApproveErc20BatchConversion( request: ClientTypes.IRequestData, diff --git a/packages/payment-processor/src/payment/batch-proxy.ts b/packages/payment-processor/src/payment/batch-proxy.ts index 32e6a222d..531daac2b 100644 --- a/packages/payment-processor/src/payment/batch-proxy.ts +++ b/packages/payment-processor/src/payment/batch-proxy.ts @@ -34,7 +34,7 @@ import { checkErc20Allowance, encodeApproveAnyErc20 } from './erc20'; * Processes a transaction to pay a batch of ETH Requests with fees. * Requests paymentType must be "ETH" or "ERC20" * @param requests List of requests - * @param version version of the batch proxy, which can be different from request pn version + * @param version The version version of the batch proxy, which can be different from request pn version * @param signerOrProvider the Web3 provider, or signer. Defaults to window.ethereum. * @param batchFee Only for batch ETH: additional fee applied to a batch, between 0 and 1000, default value = 10 * @param overrides optionally, override default transaction values, like gas. @@ -55,7 +55,7 @@ export async function payBatchProxyRequest( * Prepate the transaction to pay a batch of requests through the batch proxy contract, can be used with a Multisig contract. * Requests paymentType must be "ETH" or "ERC20" * @param requests list of ETH requests to pay - * @param version version of the batch proxy, which can be different from request pn version + * @param version The version version of the batch proxy, which can be different from request pn version * @param batchFee additional fee applied to a batch */ export function prepareBatchPaymentTransaction( @@ -153,6 +153,7 @@ export function encodePayBatchRequest(requests: ClientTypes.IRequestData[]): str /** * Get batch arguments * @param requests List of requests + * @param forcedPaymentType It force to considere the request as an ETH or an ERC20 payment * @returns List with the args required by batch Eth and Erc20 functions, * @dev tokenAddresses returned is for batch Erc20 functions */ @@ -209,8 +210,8 @@ export function getBatchArgs( /** * Get Batch contract Address - * @param request - * @param version version of the batch proxy, which can be different from request pn version + * @param request The request to pay + * @param version The version version of the batch proxy, which can be different from request pn version */ export function getBatchProxyAddress(request: ClientTypes.IRequestData, version: string): string { const pn = getPaymentNetworkExtension(request); @@ -233,9 +234,9 @@ export function getBatchProxyAddress(request: ClientTypes.IRequestData, version: /** * Processes the approval transaction of the targeted ERC20 with batch proxy. - * @param request request to pay - * @param account account that will be used to pay the request - * @param version version of the batch proxy, which can be different from request pn version + * @param request The request to pay + * @param account The account that will be used to pay the request + * @param version The version version of the batch proxy, which can be different from request pn version * @param signerOrProvider the Web3 provider, or signer. Defaults to window.ethereum. * @param overrides optionally, override default transaction values, like gas. */ @@ -254,9 +255,9 @@ export async function approveErc20BatchIfNeeded( /** * Checks if the batch proxy has the necessary allowance from a given account * to pay a given request with ERC20 batch - * @param request request to pay - * @param account account that will be used to pay the request - * @param version version of the batch proxy, which can be different from request pn version + * @param request The request to pay + * @param account The account that will be used to pay the request + * @param version The version version of the batch proxy, which can be different from request pn version * @param signerOrProvider the Web3 provider, or signer. Defaults to window.ethereum. */ export async function hasErc20BatchApproval( @@ -277,8 +278,8 @@ export async function hasErc20BatchApproval( /** * Processes the transaction to approve the batch proxy to spend signer's tokens to pay * the request in its payment currency. Can be used with a Multisig contract. - * @param request request to pay - * @param version version of the batch proxy, which can be different from request pn version + * @param request The request to pay + * @param version The version version of the batch proxy, which can be different from request pn version * @param signerOrProvider the Web3 provider, or signer. Defaults to window.ethereum. * @param overrides optionally, override default transaction values, like gas. */ @@ -297,8 +298,8 @@ export async function approveErc20Batch( /** * Prepare the transaction to approve the proxy to spend signer's tokens to pay * the request in its payment currency. Can be used with a Multisig contract. - * @param request request to pay - * @param version version of the batch proxy, which can be different from request pn version + * @param request The request to pay + * @param version The version version of the batch proxy, which can be different from request pn version * @param signerOrProvider the Web3 provider, or signer. Defaults to window.ethereum. * @param overrides optionally, override default transaction values, like gas. */ @@ -321,8 +322,8 @@ export function prepareApproveErc20Batch( /** * Encodes the transaction to approve the batch proxy to spend signer's tokens to pay * the request in its payment currency. Can be used with a Multisig contract. - * @param request request to pay - * @param version version of the batch proxy, which can be different from request pn version + * @param request The request to pay + * @param version The version version of the batch proxy, which can be different from request pn version * @param signerOrProvider the Web3 provider, or signer. Defaults to window.ethereum. */ export function encodeApproveErc20Batch( diff --git a/packages/payment-processor/src/payment/utils.ts b/packages/payment-processor/src/payment/utils.ts index 464c4e659..f107162cf 100644 --- a/packages/payment-processor/src/payment/utils.ts +++ b/packages/payment-processor/src/payment/utils.ts @@ -119,6 +119,10 @@ export function getPaymentExtensionVersion(request: ClientTypes.IRequestData): s return extension.version; } +/** + * @param pn It contains the payment network extension + * @param currency It contains the currency information + */ export const getProxyNetwork = ( pn: ExtensionTypes.IState, currency: RequestLogicTypes.ICurrency, @@ -132,6 +136,10 @@ export const getProxyNetwork = ( throw new Error('Payment currency must have a network'); }; +/** + * @param request The request to pay + * @return A list that contains the payment network extension and the currency information + */ export function getPnAndNetwork( request: ClientTypes.IRequestData, ): [ExtensionTypes.IState, string] { @@ -143,9 +151,9 @@ export function getPnAndNetwork( } /** - * @param request the request - * @param getDeploymentInformation the function to get the proxy address - * @param version the version has to be set to get batch conversion proxy + * @param request The request to pay + * @param getDeploymentInformation The function to get the proxy address + * @param version The version has to be set to get batch conversion proxy */ export const getProxyAddress = ( request: ClientTypes.IRequestData, @@ -328,8 +336,8 @@ export function getAmountToPay( /** * Compare 2 payment networks type and version in request's extension * and throw an exception if they are different - * @param pn payment network - * @param request + * @param pn The payment network extension + * @param request The request to pay */ export function comparePnTypeAndVersion( pn: ExtensionTypes.IState | undefined, From 26407ea9cfb2373ae9ad302d6988ec7a15b3f627 Mon Sep 17 00:00:00 2001 From: olivier7delf <55892112+olivier7delf@users.noreply.github.com> Date: Fri, 16 Sep 2022 08:57:34 +0200 Subject: [PATCH 105/105] add batch proxy addresses --- .../BatchConversionPayments/index.ts | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/packages/smart-contracts/src/lib/artifacts/BatchConversionPayments/index.ts b/packages/smart-contracts/src/lib/artifacts/BatchConversionPayments/index.ts index d83849d90..1f09adc85 100644 --- a/packages/smart-contracts/src/lib/artifacts/BatchConversionPayments/index.ts +++ b/packages/smart-contracts/src/lib/artifacts/BatchConversionPayments/index.ts @@ -13,6 +13,42 @@ export const batchConversionPaymentsArtifact = new ContractArtifact